diff --git a/.github/workflows/cli-release.yml b/.github/workflows/cli-release.yml new file mode 100644 index 00000000..79f9c308 --- /dev/null +++ b/.github/workflows/cli-release.yml @@ -0,0 +1,174 @@ +name: Release CLI + +on: + release: + types: [published] + workflow_dispatch: + inputs: + publish_npm: + description: "Publish to npm" + type: boolean + default: false + +jobs: + build-binaries: + name: Build binary (${{ matrix.asset_name }}) + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - os: ubuntu-22.04 + asset_name: arrhes-linux-x64 + node_version: 22 + - os: macos-13 + asset_name: arrhes-macos-x64 + node_version: 22 + - os: macos-15 + asset_name: arrhes-macos-arm64 + node_version: 22 + - os: windows-2025 + asset_name: arrhes-windows-x64.exe + node_version: 22 + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Set up Node.js + uses: actions/setup-node@v5 + with: + node-version: ${{ matrix.node_version }} + + - name: Install pnpm + run: npm install -g pnpm + + - name: Install dependencies + run: CI=true pnpm install --no-frozen-lockfile + + - name: Build metadata + run: pnpm --filter @arrhes/application-metadata build + + - name: Bundle CLI (inline all deps) + working-directory: packages/cli + run: pnpm bundle + + - name: Generate SEA blob + working-directory: packages/cli + run: node --experimental-sea-config sea-config.json + + - name: Create standalone binary (Linux / macOS) + if: runner.os != 'Windows' + working-directory: packages/cli + run: | + mkdir -p binaries + cp $(which node) binaries/${{ matrix.asset_name }} + node_modules/.bin/postject binaries/${{ matrix.asset_name }} NODE_SEA_BLOB sea-prep.blob \ + --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 + if [[ "$RUNNER_OS" == "macOS" ]]; then + # macOS: codesign BEFORE UPX (UPX is skipped on macOS — breaks signing) + codesign --sign - binaries/${{ matrix.asset_name }} + fi + + - name: Create standalone binary (Windows) + if: runner.os == 'Windows' + working-directory: packages/cli + shell: pwsh + run: | + New-Item -ItemType Directory -Force -Path binaries + $nodePath = (Get-Command node).Source + Copy-Item $nodePath binaries\${{ matrix.asset_name }} + node_modules\.bin\postject binaries\${{ matrix.asset_name }} NODE_SEA_BLOB sea-prep.blob ` + --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 + + # UPX compresses ~60-65% (123 MB → ~40 MB). + # Skipped on macOS: UPX invalidates the codesignature and Gatekeeper rejects the binary. + - name: Compress binary with UPX (Linux) + if: runner.os == 'Linux' + working-directory: packages/cli + run: | + sudo apt-get install -y upx-ucl -q + upx --best --lzma binaries/${{ matrix.asset_name }} + + - name: Compress binary with UPX (Windows) + if: runner.os == 'Windows' + working-directory: packages/cli + shell: pwsh + run: | + choco install upx -y --no-progress + upx --best --lzma binaries\${{ matrix.asset_name }} + + - name: Upload binary to release + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: packages/cli/binaries/${{ matrix.asset_name }} + asset_name: ${{ matrix.asset_name }} + asset_content_type: application/octet-stream + + # Upload install scripts once (only from the Linux runner to avoid duplicates) + - name: Upload install.sh to release + if: runner.os == 'Linux' + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: packages/cli/install.sh + asset_name: install.sh + asset_content_type: text/plain + + - name: Upload install.ps1 to release + if: runner.os == 'Linux' + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: packages/cli/install.ps1 + asset_name: install.ps1 + asset_content_type: text/plain + + publish-npm: + name: Publish to npm + runs-on: ubuntu-22.04 + needs: build-binaries + if: github.event_name == 'release' || inputs.publish_npm + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Set up Node.js + uses: actions/setup-node@v5 + with: + node-version: 22 + registry-url: https://registry.npmjs.org + + - name: Install pnpm + run: npm install -g pnpm + + - name: Install dependencies + run: CI=true pnpm install --no-frozen-lockfile + + - name: Build metadata + run: pnpm --filter @arrhes/application-metadata build + + - name: Build CLI (tsc) + working-directory: packages/cli + run: pnpm build + + - name: Set version from release tag + if: github.event_name == 'release' + working-directory: packages/cli + run: | + VERSION="${{ github.event.release.tag_name }}" + VERSION="${VERSION#v}" # strip leading 'v' + npm pkg set version="$VERSION" + + - name: Publish + working-directory: packages/cli + run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index ed3747f3..bca0930d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,10 @@ build .vscode .agents +packages/cli/binaries/ +packages/cli/dist/ +packages/cli/sea-prep.blob + .pnpm-store *.tsbuildinfo diff --git a/.workflows/build/compose.start.yml b/.workflows/build/compose.start.yml index d735cf11..16f9469f 100644 --- a/.workflows/build/compose.start.yml +++ b/.workflows/build/compose.start.yml @@ -135,6 +135,11 @@ services: worker: container_name: arrhes-prod-worker image: arrhes-worker:${ARRHES_VERSION:-dev} + logging: + driver: json-file + options: + max-size: "50m" + max-file: "5" environment: ENV: development VERBOSE: "true" diff --git a/.workflows/dev/compose.yml b/.workflows/dev/compose.yml index 3b34fe79..eb7ac3e9 100644 --- a/.workflows/dev/compose.yml +++ b/.workflows/dev/compose.yml @@ -161,6 +161,12 @@ services: condition: service_healthy redis: condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:' + process.env.PORT, r => process.exit(r.statusCode < 500 ? 0 : 1)).on('error', () => process.exit(1))\""] + interval: 10s + timeout: 5s + retries: 5 + start_period: 60s # Website - Frontend interface (React + Vite) website: @@ -191,6 +197,11 @@ services: worker: container_name: arrhes-worker image: arrhes-dev-worker:latest + logging: + driver: json-file + options: + max-size: "50m" + max-file: "5" build: context: ../.. dockerfile: .workflows/dev/packages/worker/Dockerfile diff --git a/.workflows/dev/packages/api/start.sh b/.workflows/dev/packages/api/start.sh index 6fcf9344..40870fd5 100644 --- a/.workflows/dev/packages/api/start.sh +++ b/.workflows/dev/packages/api/start.sh @@ -2,14 +2,17 @@ # ============================================================================== # Start API Server # ============================================================================== -# Starts the API development server with hot reload. -# Builds workspace dependencies first (tsc --build follows project references). +# 1. Runs a one-off schema drift check (plain tsx, no watch). +# If the DB is out of sync with the Drizzle models the script exits non-zero, +# the container stops, and `docker compose up --wait` surfaces the error. +# 2. Starts the API dev server with hot reload (tsx watch). +# tsx is started with --conditions source so that @arrhes/application-metadata +# resolves to ./src/index.ts directly — no build step needed in dev. # ============================================================================== set -e -echo "Building workspace dependencies..." -cd /workspace/packages/api -pnpm run build +echo "Checking database schema..." +SCHEMA_CHECK_ONLY=1 pnpm --filter="@arrhes/application-api" exec tsx --conditions source ./src/server.ts echo "Starting API dev server..." exec pnpm --filter="@arrhes/application-api" dev diff --git a/.workflows/dev/up.sh b/.workflows/dev/up.sh index 55c51d47..65563b50 100644 --- a/.workflows/dev/up.sh +++ b/.workflows/dev/up.sh @@ -84,15 +84,28 @@ POSTGRES_HOST_PORT=$postgres_host_port REDIS_HOST_PORT=$redis_host_port EOF -WEBSITE_HOST_PORT="$website_host_port" \ -API_HOST_PORT="$api_host_port" \ -STORAGE_HOST_PORT="$storage_host_port" \ -RUSTFS_UI_HOST_PORT="$rustfs_ui_host_port" \ -MAILPIT_UI_HOST_PORT="$mailpit_ui_host_port" \ -MAILPIT_SMTP_HOST_PORT="$mailpit_smtp_host_port" \ -POSTGRES_HOST_PORT="$postgres_host_port" \ -REDIS_HOST_PORT="$redis_host_port" \ - "${DC[@]}" up --detach --build --force-recreate +if ! WEBSITE_HOST_PORT="$website_host_port" \ + API_HOST_PORT="$api_host_port" \ + STORAGE_HOST_PORT="$storage_host_port" \ + RUSTFS_UI_HOST_PORT="$rustfs_ui_host_port" \ + MAILPIT_UI_HOST_PORT="$mailpit_ui_host_port" \ + MAILPIT_SMTP_HOST_PORT="$mailpit_smtp_host_port" \ + POSTGRES_HOST_PORT="$postgres_host_port" \ + REDIS_HOST_PORT="$redis_host_port" \ + "${DC[@]}" up --detach --build --force-recreate --wait; then + echo "" + echo "==============================================" + echo " ERROR: one or more services failed to start" + echo "==============================================" + echo "" + # Print logs for every exited container so the error is visible + for container in $("${DC[@]}" ps --all --filter status=exited --format '{{.Name}}' 2>/dev/null); do + echo "--- Logs for $container ---" + docker logs "$container" 2>&1 | tail -30 + echo "" + done + exit 1 +fi echo "" echo "==============================================" diff --git a/VERSION b/VERSION index 0408c30b..8b3a0227 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v1.2.0 \ No newline at end of file +v1.3.0 \ No newline at end of file diff --git a/biome.json b/biome.json index 88f50741..a31bcec3 100644 --- a/biome.json +++ b/biome.json @@ -6,7 +6,7 @@ "useIgnoreFile": true }, "files": { - "includes": ["**", "!**/dist", "!**/build", "!**/styled-system", "!**/.workflows"] + "includes": ["**", "!**/dist", "!**/build", "!**/styled-system", "!**/.workflows", "!scripts"] }, "assist": { "actions": { "source": { "organizeImports": "on" } } }, "formatter": { diff --git a/node_modules_old/.pnpm-workspace-state-v1.json b/node_modules_old/.pnpm-workspace-state-v1.json new file mode 100644 index 00000000..65e67dfb --- /dev/null +++ b/node_modules_old/.pnpm-workspace-state-v1.json @@ -0,0 +1,48 @@ +{ + "lastValidatedTimestamp": 1779115049031, + "projects": { + "/workspace": { + "name": "arrhes" + }, + "/workspace/packages/api": { + "name": "@arrhes/application-api" + }, + "/workspace/packages/metadata": { + "name": "@arrhes/application-metadata" + }, + "/workspace/packages/tools": { + "name": "@arrhes/application-tools" + }, + "/workspace/packages/ui": { + "name": "@arrhes/ui", + "version": "0.0.1" + }, + "/workspace/packages/website": { + "name": "@arrhes/application-website" + }, + "/workspace/packages/worker": { + "name": "@evidensy/worker" + } + }, + "pnpmfiles": [], + "settings": { + "autoInstallPeers": true, + "catalogs": {}, + "dedupeDirectDeps": false, + "dedupeInjectedDeps": true, + "dedupePeerDependents": true, + "dev": true, + "excludeLinksFromLockfile": false, + "hoistPattern": ["*"], + "hoistWorkspacePackages": true, + "injectWorkspacePackages": false, + "linkWorkspacePackages": false, + "nodeLinker": "isolated", + "optional": true, + "preferWorkspacePackages": false, + "production": true, + "publicHoistPattern": [], + "workspacePackagePatterns": ["./packages/*"] + }, + "filteredInstall": false +} diff --git a/packages/api/package.json b/packages/api/package.json index ad6ba048..08193c59 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -3,7 +3,7 @@ "type": "module", "private": false, "scripts": { - "dev": "tsx watch --env-file=.env ./src/server.ts", + "dev": "tsx watch --conditions source --env-file=.env ./src/server.ts", "build": "tsc --build", "start": "node --env-file=.env ./build/server.js", "test": "vitest run", diff --git a/packages/api/src/middlewares/checkAdminUserSessionMiddleware.ts b/packages/api/src/middlewares/checkAdminUserSessionMiddleware.ts index cd1a3b14..ab35c38f 100644 --- a/packages/api/src/middlewares/checkAdminUserSessionMiddleware.ts +++ b/packages/api/src/middlewares/checkAdminUserSessionMiddleware.ts @@ -5,10 +5,10 @@ import { parseCookies } from "../utilities/cookies/parseCookies.js" import { unsignString } from "../utilities/cookies/unsignString.js" import { Exception } from "../utilities/exception.js" import { productName } from "../utilities/variables.js" -import { checkUserSessionMiddleware } from "./checkUserSessionMiddleware.js" +import { checkAuthMiddleware } from "./checkAuthMiddleware.js" async function trySuperAdminDashboardFallback(parameters: { context: Context }) { - const userSession = await checkUserSessionMiddleware({ + const userSession = await checkAuthMiddleware({ context: parameters.context, }) if (userSession.user.isSuperAdmin !== true) { diff --git a/packages/api/src/middlewares/checkAuthMiddleware.ts b/packages/api/src/middlewares/checkAuthMiddleware.ts new file mode 100644 index 00000000..a92feebd --- /dev/null +++ b/packages/api/src/middlewares/checkAuthMiddleware.ts @@ -0,0 +1,145 @@ +import { createHash } from "node:crypto" +import { models } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import type { Context } from "hono" +import { parseCookies } from "../utilities/cookies/parseCookies.js" +import { unsignString } from "../utilities/cookies/unsignString.js" +import { Exception } from "../utilities/exception.js" +import { selectOne } from "../utilities/sql/selectOne.js" +import { productName } from "../utilities/variables.js" +import { resolveOrganizationMiddleware } from "./resolveOrganizationMiddleware.js" + +export async function checkAuthMiddleware(parameters: { context: Context }) { + try { + // 1. Try cookie auth first + const cookieAuth = await tryAuthWithCookie(parameters.context) + if (cookieAuth !== null) { + return cookieAuth + } + + // 2. Try Bearer token auth (API key) + const bearerAuth = await tryAuthWithBearer(parameters.context) + if (bearerAuth !== null) { + return bearerAuth + } + + throw new Exception({ + internalMessage: "Auth error", + cause: "No valid authentication method found", + }) + } catch (error: unknown) { + if (error instanceof Exception) { + throw new Exception({ + statusCode: 401, + internalMessage: "Auth error", + rawError: error, + }) + } + throw new Exception({ + statusCode: 401, + internalMessage: "Auth error", + rawError: error, + }) + } +} + +async function tryAuthWithCookie(context: Context) { + try { + const cookieMap = parseCookies({ + value: context.req.header("Cookie"), + }) + + const idUserSession = unsignString({ + signedValue: cookieMap[`${productName}_id_user_session`], + secret: context.var.env.COOKIES_KEY, + }) + + if (!idUserSession) { + return null + } + + const userSession = await selectOne({ + database: context.var.clients.sql, + table: models.userSession, + where: (table) => eq(table.id, idUserSession), + }) + + if (!userSession || userSession.isActive === false) { + return null + } + + const user = await selectOne({ + database: context.var.clients.sql, + table: models.user, + where: (table) => eq(table.id, userSession.idUser), + }) + + if (!user) { + return null + } + + context.set("user", user) + + // Resolve idOrganization from header/cookie/body for cookie-based auth + let idOrganization: string | undefined + try { + idOrganization = await resolveOrganizationMiddleware({ + context, + }) + } catch { + // idOrganization is optional - some routes don't need it + idOrganization = undefined + } + + return { + userSession: userSession, + user: user, + idOrganization: idOrganization, + } + } catch { + return null + } +} + +async function tryAuthWithBearer(context: Context) { + const authHeader = context.req.header("Authorization") + if (!authHeader?.startsWith("Bearer ")) { + return null + } + + const rawKey = authHeader.slice(7) + if (!rawKey) { + return null + } + + const keyHash = createHash("sha256").update(rawKey).digest("hex") + + const apiKey = await selectOne({ + database: context.var.clients.sql, + table: models.apiKey, + where: (table) => and(eq(table.keyHash, keyHash), eq(table.isActive, true)), + }) + + if (!apiKey) { + return null + } + + const user = await selectOne({ + database: context.var.clients.sql, + table: models.user, + where: (table) => eq(table.id, apiKey.idUser), + }) + + if (!user) { + return null + } + + context.set("user", user) + + // For Bearer auth, idOrganization comes from the API key record + return { + userSession: null, + user: user, + idOrganization: apiKey.idOrganization, + } +} diff --git a/packages/api/src/middlewares/checkUserSessionMiddleware.ts b/packages/api/src/middlewares/checkUserSessionMiddleware.ts deleted file mode 100644 index 6b8248f8..00000000 --- a/packages/api/src/middlewares/checkUserSessionMiddleware.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { createHash } from "node:crypto" -import { models } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import type { Context } from "hono" -import { parseCookies } from "../utilities/cookies/parseCookies.js" -import { unsignString } from "../utilities/cookies/unsignString.js" -import { Exception } from "../utilities/exception.js" -import { productName } from "../utilities/variables.js" -import { resolveOrganizationMiddleware } from "./resolveOrganizationMiddleware.js" - -export async function checkUserSessionMiddleware(parameters: { context: Context }) { - try { - // 1. Try cookie auth first - const cookieAuth = await tryAuthWithCookie(parameters.context) - if (cookieAuth) { - return cookieAuth - } - - // 2. Try Bearer token auth (API key) - const bearerAuth = await tryAuthWithBearer(parameters.context) - if (bearerAuth) { - return bearerAuth - } - - throw new Exception({ - internalMessage: "Auth error", - cause: "No valid authentication method found", - }) - } catch (error: unknown) { - if (error instanceof Exception) { - throw new Exception({ - statusCode: 401, - internalMessage: "Auth error", - rawError: error, - }) - } - throw new Exception({ - statusCode: 401, - internalMessage: "Auth error", - rawError: error, - }) - } -} - -async function tryAuthWithCookie(context: Context) { - try { - const cookieMap = parseCookies({ - value: context.req.header("Cookie"), - }) - - const idUserSession = unsignString({ - signedValue: cookieMap[`${productName}_id_user_session`], - secret: context.var.env.COOKIES_KEY, - }) - - if (!idUserSession) { - return null - } - - const userSession = await context.var.clients.sql.query.userSessionModel.findFirst({ - where: eq(models.userSession.id, idUserSession), - }) - - if (!userSession || userSession.isActive === false) { - return null - } - - const user = await context.var.clients.sql.query.userModel.findFirst({ - where: eq(models.user.id, userSession.idUser), - }) - - if (!user) { - return null - } - - context.set("user", user) - - // Resolve idOrganization from header/cookie/body for cookie-based auth - let idOrganization: string | undefined - try { - idOrganization = await resolveOrganizationMiddleware({ - context, - }) - } catch { - // idOrganization is optional - some routes don't need it - idOrganization = undefined - } - - return { - userSession: userSession, - user: user, - idOrganization: idOrganization, - } - } catch { - return null - } -} - -async function tryAuthWithBearer(context: Context) { - const authHeader = context.req.header("Authorization") - if (!authHeader?.startsWith("Bearer ")) { - return null - } - - const rawKey = authHeader.slice(7) - if (!rawKey) { - return null - } - - const keyHash = createHash("sha256").update(rawKey).digest("hex") - - const apiKey = await context.var.clients.sql.query.apiKeyModel.findFirst({ - where: and(eq(models.apiKey.keyHash, keyHash), eq(models.apiKey.isActive, true)), - }) - - if (!apiKey) { - return null - } - - const user = await context.var.clients.sql.query.userModel.findFirst({ - where: eq(models.user.id, apiKey.idUser), - }) - - if (!user) { - return null - } - - context.set("user", user) - - // For Bearer auth, idOrganization comes from the API key record - return { - userSession: null, - user: user, - idOrganization: apiKey.idOrganization, - } -} diff --git a/packages/api/src/middlewares/requireCookieSessionMiddleware.ts b/packages/api/src/middlewares/requireCookieSessionMiddleware.ts new file mode 100644 index 00000000..d0c7c5e3 --- /dev/null +++ b/packages/api/src/middlewares/requireCookieSessionMiddleware.ts @@ -0,0 +1,21 @@ +import type { Context } from "hono" +import { Exception } from "../utilities/exception.js" +import { checkAuthMiddleware } from "./checkAuthMiddleware.js" + +export async function requireCookieSessionMiddleware(parameters: { context: Context }) { + const auth = await checkAuthMiddleware({ + context: parameters.context, + }) + + if (auth.userSession === null) { + throw new Exception({ + statusCode: 401, + internalMessage: "Session required", + cause: "This route requires cookie session authentication", + }) + } + + return auth as typeof auth & { + userSession: NonNullable + } +} diff --git a/packages/api/src/middlewares/requireOrganizationMiddleware.ts b/packages/api/src/middlewares/requireOrganizationMiddleware.ts new file mode 100644 index 00000000..a4dd35a1 --- /dev/null +++ b/packages/api/src/middlewares/requireOrganizationMiddleware.ts @@ -0,0 +1,12 @@ +import { Exception } from "../utilities/exception.js" + +export async function requireOrganizationMiddleware(parameters: { idOrganization: string | undefined }) { + if (parameters.idOrganization !== undefined) { + return parameters.idOrganization + } + + throw new Exception({ + internalMessage: "Organization required", + cause: "No organization context found in the request", + }) +} diff --git a/packages/api/src/middlewares/resolveOrganizationMiddleware.ts b/packages/api/src/middlewares/resolveOrganizationMiddleware.ts index c6f81324..795e5f99 100644 --- a/packages/api/src/middlewares/resolveOrganizationMiddleware.ts +++ b/packages/api/src/middlewares/resolveOrganizationMiddleware.ts @@ -5,21 +5,28 @@ import { productName } from "../utilities/variables.js" /** * Resolves idOrganization from the request context using the following priority: - * 1. X-Organization-Id header - * 2. arrhes_id_organization cookie - * 3. body.idOrganization (legacy fallback) + * 1. URL path param `:idOrganization` (highest — used by REST routes like /v1/organizations/:idOrganization) + * 2. X-Organization-Id header + * 3. arrhes_id_organization cookie + * 4. body.idOrganization (legacy fallback) * * For Bearer token auth, the caller should use apiKey.idOrganization directly * instead of calling this middleware. */ export async function resolveOrganizationMiddleware(parameters: { context: Context }): Promise { - // 1. X-Organization-Id header + // 1. URL path param (REST routes) + const paramValue = parameters.context.req.param("idOrganization") + if (paramValue) { + return paramValue + } + + // 2. X-Organization-Id header const headerValue = parameters.context.req.header("X-Organization-Id") if (headerValue) { return headerValue } - // 2. arrhes_id_organization cookie + // 3. arrhes_id_organization cookie const cookieMap = parseCookies({ value: parameters.context.req.header("Cookie"), }) @@ -28,7 +35,7 @@ export async function resolveOrganizationMiddleware(parameters: { context: Conte return cookieValue } - // 3. body.idOrganization (legacy fallback) + // 4. body.idOrganization (legacy fallback) try { const body = await parameters.context.req.json() if (body?.idOrganization) { @@ -42,6 +49,6 @@ export async function resolveOrganizationMiddleware(parameters: { context: Conte statusCode: 400, internalMessage: "Could not resolve organization", externalMessage: "Organization identifier is required", - cause: "No idOrganization found in X-Organization-Id header, cookie, or request body", + cause: "No idOrganization found in URL param, X-Organization-Id header, cookie, or request body", }) } diff --git a/packages/api/src/routes/auth/$idOrganization/$idOrganizationRoutes.ts b/packages/api/src/routes/auth/$idOrganization/$idOrganizationRoutes.ts deleted file mode 100644 index cadfb675..00000000 --- a/packages/api/src/routes/auth/$idOrganization/$idOrganizationRoutes.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { apiKeyRoutes } from "./apiKey/apiKeyRoutes.js" -import { organizationPaymentsRoutes } from "./organizationPayment/organizationPaymentsRoutes.js" -import { organizationSettingsRoutes } from "./organizationSettings/organizationSettingsRoutes.js" -import { organizationUsersRoutes } from "./organizationUser/organizationUsersRoutes.js" -import { readOneOrganizationRoute } from "./readOneOrganization.js" -import { yearsRoutes } from "./years/yearsRoutes.js" - -export const $idOrganizationRoutes = [ - readOneOrganizationRoute, - - ...yearsRoutes, - ...organizationUsersRoutes, - ...organizationSettingsRoutes, - ...organizationPaymentsRoutes, - ...apiKeyRoutes, -] diff --git a/packages/api/src/routes/auth/$idOrganization/apiKey/createOneApiKey.ts b/packages/api/src/routes/auth/$idOrganization/apiKey/createOneApiKey.ts deleted file mode 100644 index cece533c..00000000 --- a/packages/api/src/routes/auth/$idOrganization/apiKey/createOneApiKey.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { createHash } from "node:crypto" -import { createOneApiKeyRouteDefinition, generateId, models } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { Exception } from "../../../../utilities/exception.js" -import { response } from "../../../../utilities/response.js" -import { insertOne } from "../../../../utilities/sql/insertOne.js" -import { selectOne } from "../../../../utilities/sql/selectOne.js" - -export const createOneApiKeyRoute = apiFactory.createApp().post(createOneApiKeyRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: createOneApiKeyRouteDefinition.schemas.body, - }) - - // Must be admin of the organization - const organizationUser = await selectOne({ - database: c.var.clients.sql, - table: models.organizationUser, - where: (table) => and(eq(table.idUser, user.id), eq(table.idOrganization, idOrganization)), - }) - if (organizationUser.isAdmin === false) { - throw new Exception({ - statusCode: 401, - internalMessage: "User is not admin of the organization", - externalMessage: "Vous n'êtes pas administrateur de l'organisation", - }) - } - - const rawKey = generateId() - const keyHash = createHash("sha256").update(rawKey).digest("hex") - - const createOneApiKey = await insertOne({ - database: c.var.clients.sql, - table: models.apiKey, - data: { - id: generateId(), - idOrganization: idOrganization, - idUser: user.id, - keyHash: keyHash, - name: body.name ?? new Date().toISOString(), - isDefault: false, - isActive: true, - createdAt: new Date().toISOString(), - lastUpdatedAt: null, - }, - }) - - return response({ - context: c, - statusCode: 200, - schema: createOneApiKeyRouteDefinition.schemas.return, - data: { - ...createOneApiKey, - rawKey: rawKey, - }, - }) -}) diff --git a/packages/api/src/routes/auth/$idOrganization/apiKey/deleteOneApiKey.ts b/packages/api/src/routes/auth/$idOrganization/apiKey/deleteOneApiKey.ts deleted file mode 100644 index 908d6e0f..00000000 --- a/packages/api/src/routes/auth/$idOrganization/apiKey/deleteOneApiKey.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { deleteOneApiKeyRouteDefinition, models } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { Exception } from "../../../../utilities/exception.js" -import { response } from "../../../../utilities/response.js" -import { deleteOne } from "../../../../utilities/sql/deleteOne.js" -import { selectOne } from "../../../../utilities/sql/selectOne.js" - -export const deleteOneApiKeyRoute = apiFactory.createApp().post(deleteOneApiKeyRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: deleteOneApiKeyRouteDefinition.schemas.body, - }) - - // Must be admin of the organization - const organizationUser = await selectOne({ - database: c.var.clients.sql, - table: models.organizationUser, - where: (table) => and(eq(table.idUser, user.id), eq(table.idOrganization, idOrganization)), - }) - if (organizationUser.isAdmin === false) { - throw new Exception({ - statusCode: 401, - internalMessage: "User is not admin of the organization", - externalMessage: "Vous n'êtes pas administrateur de l'organisation", - }) - } - - const deleteOneApiKey = await deleteOne({ - database: c.var.clients.sql, - table: models.apiKey, - where: (table) => and(eq(table.id, body.idApiKey), eq(table.idOrganization, idOrganization)), - }) - - return response({ - context: c, - statusCode: 200, - schema: deleteOneApiKeyRouteDefinition.schemas.return, - data: deleteOneApiKey, - }) -}) diff --git a/packages/api/src/routes/auth/$idOrganization/apiKey/readAllApiKeys.ts b/packages/api/src/routes/auth/$idOrganization/apiKey/readAllApiKeys.ts deleted file mode 100644 index 3a6e1469..00000000 --- a/packages/api/src/routes/auth/$idOrganization/apiKey/readAllApiKeys.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { models, readAllApiKeysRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { Exception } from "../../../../utilities/exception.js" -import { response } from "../../../../utilities/response.js" -import { selectOne } from "../../../../utilities/sql/selectOne.js" - -export const readAllApiKeysRoute = apiFactory.createApp().post(readAllApiKeysRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const _body = await validateBodyMiddleware({ - context: c, - schema: readAllApiKeysRouteDefinition.schemas.body, - }) - - // Must be admin of the organization - const organizationUser = await selectOne({ - database: c.var.clients.sql, - table: models.organizationUser, - where: (table) => and(eq(table.idUser, user.id), eq(table.idOrganization, idOrganization)), - }) - if (organizationUser.isAdmin === false) { - throw new Exception({ - statusCode: 401, - internalMessage: "User is not admin of the organization", - externalMessage: "Vous n'êtes pas administrateur de l'organisation", - }) - } - - const readAllApiKeys = await c.var.clients.sql.query.apiKeyModel.findMany({ - where: (table) => and(eq(table.idOrganization, idOrganization)), - }) - - return response({ - context: c, - statusCode: 200, - schema: readAllApiKeysRouteDefinition.schemas.return, - data: readAllApiKeys, - }) -}) diff --git a/packages/api/src/routes/auth/$idOrganization/organizationPayment/cancelSubscription.ts b/packages/api/src/routes/auth/$idOrganization/organizationPayment/cancelSubscription.ts deleted file mode 100644 index 6c753594..00000000 --- a/packages/api/src/routes/auth/$idOrganization/organizationPayment/cancelSubscription.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { cancelSubscriptionRouteDefinition } from "@arrhes/application-metadata" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { response } from "../../../../utilities/response.js" - -export const cancelSubscriptionRoute = apiFactory - .createApp() - .post(cancelSubscriptionRouteDefinition.path, async (c) => { - await checkUserSessionMiddleware({ - context: c, - }) - await validateBodyMiddleware({ - context: c, - schema: cancelSubscriptionRouteDefinition.schemas.body, - }) - - return response({ - context: c, - statusCode: 200, - schema: cancelSubscriptionRouteDefinition.schemas.return, - data: {}, - }) - }) diff --git a/packages/api/src/routes/auth/$idOrganization/organizationPayment/createFirstPayment.ts b/packages/api/src/routes/auth/$idOrganization/organizationPayment/createFirstPayment.ts deleted file mode 100644 index d83330d7..00000000 --- a/packages/api/src/routes/auth/$idOrganization/organizationPayment/createFirstPayment.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { createFirstPaymentRouteDefinition } from "@arrhes/application-metadata" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { Exception } from "../../../../utilities/exception.js" - -export const createFirstPaymentRoute = apiFactory - .createApp() - .post(createFirstPaymentRouteDefinition.path, async (c) => { - await checkUserSessionMiddleware({ - context: c, - }) - await validateBodyMiddleware({ - context: c, - schema: createFirstPaymentRouteDefinition.schemas.body, - }) - - throw new Exception({ - statusCode: 400, - internalMessage: "Mollie subscription integration has been retired", - externalMessage: "Cette fonctionnalité n'est plus disponible", - }) - }) diff --git a/packages/api/src/routes/auth/$idOrganization/organizationPayment/generateInvoiceGetSignedUrl.ts b/packages/api/src/routes/auth/$idOrganization/organizationPayment/generateInvoiceGetSignedUrl.ts deleted file mode 100644 index 29b6ca0c..00000000 --- a/packages/api/src/routes/auth/$idOrganization/organizationPayment/generateInvoiceGetSignedUrl.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { generateInvoiceGetSignedUrlRouteDefinition, models } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { Exception } from "../../../../utilities/exception.js" -import { response } from "../../../../utilities/response.js" -import { selectOne } from "../../../../utilities/sql/selectOne.js" -import { generateGetSignedUrl } from "../../../../utilities/storage/generateGetSignedUrl.js" - -export const generateInvoiceGetSignedUrlRoute = apiFactory - .createApp() - .post(generateInvoiceGetSignedUrlRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: generateInvoiceGetSignedUrlRouteDefinition.schemas.body, - }) - - await selectOne({ - database: c.var.clients.sql, - table: models.organizationUser, - where: (table) => and(eq(table.idUser, user.id), eq(table.idOrganization, idOrganization)), - }) - - const invoice = await selectOne({ - database: c.var.clients.sql, - table: models.invoice, - where: (table) => and(eq(table.id, body.idInvoice), eq(table.idOrganization, idOrganization)), - }) - - if (invoice.xmlStorageKey === null) { - throw new Exception({ - statusCode: 404, - internalMessage: "Invoice PDF not yet generated", - externalMessage: "La facture PDF n'est pas encore disponible", - }) - } - - const url = await generateGetSignedUrl({ - var: c.var, - storageKey: invoice.xmlStorageKey, - }) - - return response({ - context: c, - statusCode: 200, - schema: generateInvoiceGetSignedUrlRouteDefinition.schemas.return, - data: { - url, - }, - }) - }) diff --git a/packages/api/src/routes/auth/$idOrganization/organizationPayment/readAllInvoices.ts b/packages/api/src/routes/auth/$idOrganization/organizationPayment/readAllInvoices.ts deleted file mode 100644 index f93955d3..00000000 --- a/packages/api/src/routes/auth/$idOrganization/organizationPayment/readAllInvoices.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { models, readAllInvoicesRouteDefinition } from "@arrhes/application-metadata" -import { and, desc, eq, inArray, isNotNull, sum } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { response } from "../../../../utilities/response.js" -import { selectMany } from "../../../../utilities/sql/selectMany.js" -import { selectOne } from "../../../../utilities/sql/selectOne.js" - -export const readAllInvoicesRoute = apiFactory.createApp().post(readAllInvoicesRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const _body = await validateBodyMiddleware({ - context: c, - schema: readAllInvoicesRouteDefinition.schemas.body, - }) - - await selectOne({ - database: c.var.clients.sql, - table: models.organizationUser, - where: (table) => and(eq(table.idUser, user.id), eq(table.idOrganization, idOrganization)), - }) - - const invoices = await selectMany({ - database: c.var.clients.sql, - table: models.invoice, - where: (table) => eq(table.idOrganization, idOrganization), - orderBy: (table) => desc(table.startingAt), - }) - - // For draft invoices, `amountInCents` is 0 at creation and only updated when the monthly - // worker finalizes them. Compute the live amount from linked payments instead so that the - // invoices page matches the wallet "Mois en cours" total. - const draftInvoiceIds = invoices.filter((inv) => inv.status === "draft").map((inv) => inv.id) - - let draftAmounts: Map = new Map() - - if (draftInvoiceIds.length > 0) { - const rows = await c.var.clients.sql - .select({ - idInvoice: models.organizationPayment.idInvoice, - total: sum(models.organizationPayment.amountHTInCents), - }) - .from(models.organizationPayment) - .where( - and( - inArray(models.organizationPayment.idInvoice, draftInvoiceIds), - isNotNull(models.organizationPayment.serviceType), - eq(models.organizationPayment.status, "paid"), - ), - ) - .groupBy(models.organizationPayment.idInvoice) - - draftAmounts = new Map( - rows.map((row) => [ - row.idInvoice as string, - Number(row.total ?? 0), - ]), - ) - } - - const data = invoices.map((inv) => { - if (inv.status !== "draft") return inv - return { - ...inv, - amountInCents: draftAmounts.get(inv.id) ?? 0, - } - }) - - return response({ - context: c, - statusCode: 200, - schema: readAllInvoicesRouteDefinition.schemas.return, - data, - }) -}) diff --git a/packages/api/src/routes/auth/$idOrganization/organizationPayment/readAllOrganizationBillings.ts b/packages/api/src/routes/auth/$idOrganization/organizationPayment/readAllOrganizationBillings.ts deleted file mode 100644 index 17bddd73..00000000 --- a/packages/api/src/routes/auth/$idOrganization/organizationPayment/readAllOrganizationBillings.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { models, readAllOrganizationBillingsRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { - getSubscriptionMonthlyAmountInCents, - isResourceSubscriptionType, -} from "../../../../utilities/billing/subscriptionPricing.js" -import { response } from "../../../../utilities/response.js" -import { selectMany } from "../../../../utilities/sql/selectMany.js" -import { selectOne } from "../../../../utilities/sql/selectOne.js" - -export const readAllOrganizationBillingsRoute = apiFactory - .createApp() - .post(readAllOrganizationBillingsRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const _body = await validateBodyMiddleware({ - context: c, - schema: readAllOrganizationBillingsRouteDefinition.schemas.body, - }) - - await selectOne({ - database: c.var.clients.sql, - table: models.organizationUser, - where: (table) => and(eq(table.idUser, user.id), eq(table.idOrganization, idOrganization)), - }) - - const subscriptions = await selectMany({ - database: c.var.clients.sql, - table: models.organizationBilling, - where: (table) => eq(table.idOrganization, idOrganization), - }) - - return response({ - context: c, - statusCode: 200, - schema: readAllOrganizationBillingsRouteDefinition.schemas.return, - data: subscriptions.map((subscription) => ({ - ...subscription, - amountInCents: - subscription.status === "active" && isResourceSubscriptionType(subscription.type) - ? getSubscriptionMonthlyAmountInCents(subscription) - : subscription.amountInCents, - })), - }) - }) diff --git a/packages/api/src/routes/auth/$idOrganization/organizationPayment/readAllOrganizationPayments.ts b/packages/api/src/routes/auth/$idOrganization/organizationPayment/readAllOrganizationPayments.ts deleted file mode 100644 index ab5baf3d..00000000 --- a/packages/api/src/routes/auth/$idOrganization/organizationPayment/readAllOrganizationPayments.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { models, readAllOrganizationPaymentsRouteDefinition } from "@arrhes/application-metadata" -import { and, desc, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { Exception } from "../../../../utilities/exception.js" -import { response } from "../../../../utilities/response.js" -import { selectMany } from "../../../../utilities/sql/selectMany.js" -import { selectOne } from "../../../../utilities/sql/selectOne.js" - -export const readAllOrganizationPaymentsRoute = apiFactory - .createApp() - .post(readAllOrganizationPaymentsRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const _body = await validateBodyMiddleware({ - context: c, - schema: readAllOrganizationPaymentsRouteDefinition.schemas.body, - }) - - const organizationUser = await selectOne({ - database: c.var.clients.sql, - table: models.organizationUser, - where: (table) => and(eq(table.idUser, user.id), eq(table.idOrganization, idOrganization)), - }) - if (organizationUser.isAdmin === false) { - throw new Exception({ - statusCode: 401, - internalMessage: "User is not admin of the organization", - externalMessage: "Vous n'êtes pas administrateur de l'organisation", - }) - } - - const readAllOrganizationPayments = await selectMany({ - database: c.var.clients.sql, - table: models.organizationPayment, - where: (table) => eq(table.idOrganization, organizationUser.idOrganization), - orderBy: (table) => desc(table.createdAt), - }) - - return response({ - context: c, - statusCode: 200, - schema: readAllOrganizationPaymentsRouteDefinition.schemas.return, - data: readAllOrganizationPayments, - }) - }) diff --git a/packages/api/src/routes/auth/$idOrganization/organizationPayment/readOneInvoice.ts b/packages/api/src/routes/auth/$idOrganization/organizationPayment/readOneInvoice.ts deleted file mode 100644 index cb6b0720..00000000 --- a/packages/api/src/routes/auth/$idOrganization/organizationPayment/readOneInvoice.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { models, readOneInvoiceRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { response } from "../../../../utilities/response.js" -import { selectOne } from "../../../../utilities/sql/selectOne.js" - -export const readOneInvoiceRoute = apiFactory.createApp().post(readOneInvoiceRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: readOneInvoiceRouteDefinition.schemas.body, - }) - - await selectOne({ - database: c.var.clients.sql, - table: models.organizationUser, - where: (table) => and(eq(table.idUser, user.id), eq(table.idOrganization, idOrganization)), - }) - - const invoice = await selectOne({ - database: c.var.clients.sql, - table: models.invoice, - where: (table) => and(eq(table.id, body.idInvoice), eq(table.idOrganization, idOrganization)), - }) - - return response({ - context: c, - statusCode: 200, - schema: readOneInvoiceRouteDefinition.schemas.return, - data: invoice, - }) -}) diff --git a/packages/api/src/routes/auth/$idOrganization/organizationPayment/readOrganizationBilling.ts b/packages/api/src/routes/auth/$idOrganization/organizationPayment/readOrganizationBilling.ts deleted file mode 100644 index 61e6614f..00000000 --- a/packages/api/src/routes/auth/$idOrganization/organizationPayment/readOrganizationBilling.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { models, readOrganizationBillingRouteDefinition } from "@arrhes/application-metadata" -import { and, desc, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { computeMonthlyTotal } from "../../../../utilities/billing/computeMonthlyTotal.js" -import { response } from "../../../../utilities/response.js" -import { selectMany } from "../../../../utilities/sql/selectMany.js" -import { selectOne } from "../../../../utilities/sql/selectOne.js" - -export const readOrganizationBillingRoute = apiFactory - .createApp() - .post(readOrganizationBillingRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const _body = await validateBodyMiddleware({ - context: c, - schema: readOrganizationBillingRouteDefinition.schemas.body, - }) - - // Verify user is member of the organization - await selectOne({ - database: c.var.clients.sql, - table: models.organizationUser, - where: (table) => and(eq(table.idUser, user.id), eq(table.idOrganization, idOrganization)), - }) - - // Get organization - const organization = await selectOne({ - database: c.var.clients.sql, - table: models.organization, - where: (table) => eq(table.id, idOrganization), - }) - - // Get latest payment status - const payments = await selectMany({ - database: c.var.clients.sql, - table: models.organizationPayment, - where: (table) => eq(table.idOrganization, idOrganization), - orderBy: (table) => desc(table.createdAt), - }) - - const latestPayment = payments.at(0) - const totalSubscriptionAmountInCents = await computeMonthlyTotal({ - var: c.var, - idOrganization, - }) - - return response({ - context: c, - statusCode: 200, - schema: readOrganizationBillingRouteDefinition.schemas.return, - data: { - status: latestPayment?.status ?? null, - licenceAmount: organization.licenceAmount, - storageLimit: organization.storageLimit, - storageCurrentUsage: organization.storageCurrentUsage, - ocrPagesTotalAvailable: organization.ocrPagesTotalAvailable, - ocrPagesTotalUsed: organization.ocrPagesTotalUsed, - tokensTotalAvailable: organization.tokensTotalAvailable, - tokensTotalUsed: organization.tokensTotalUsed, - totalSubscriptionAmountInCents, - }, - }) - }) diff --git a/packages/api/src/routes/auth/$idOrganization/organizationPayment/updateLicenceSubscription.ts b/packages/api/src/routes/auth/$idOrganization/organizationPayment/updateLicenceSubscription.ts deleted file mode 100644 index 113a2b26..00000000 --- a/packages/api/src/routes/auth/$idOrganization/organizationPayment/updateLicenceSubscription.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { models, updateLicenceSubscriptionRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { Exception } from "../../../../utilities/exception.js" -import { response } from "../../../../utilities/response.js" -import { selectOne } from "../../../../utilities/sql/selectOne.js" -import { updateOne } from "../../../../utilities/sql/updateOne.js" - -export const updateLicenceSubscriptionRoute = apiFactory - .createApp() - .post(updateLicenceSubscriptionRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: updateLicenceSubscriptionRouteDefinition.schemas.body, - }) - - const organizationUser = await selectOne({ - database: c.var.clients.sql, - table: models.organizationUser, - where: (table) => and(eq(table.idUser, user.id), eq(table.idOrganization, idOrganization)), - }) - if (organizationUser.isAdmin === false) { - throw new Exception({ - statusCode: 401, - internalMessage: "User is not admin of the organization", - externalMessage: "Vous n'êtes pas administrateur de l'organisation", - }) - } - - if (Number.isNaN(body.newAmountInCents) || body.newAmountInCents < 0) { - throw new Exception({ - statusCode: 400, - internalMessage: "Invalid amount", - externalMessage: "Montant invalide", - }) - } - - const organization = await selectOne({ - database: c.var.clients.sql, - table: models.organization, - where: (table) => eq(table.id, idOrganization), - }) - - // Store as pending — applied on the 1st of next month by the worker - const pendingValue = body.newAmountInCents === organization.licenceAmount ? null : body.newAmountInCents - - await updateOne({ - database: c.var.clients.sql, - table: models.organization, - data: { - licenceAmountPending: pendingValue, - lastUpdatedAt: new Date().toISOString(), - lastUpdatedBy: user.id, - }, - where: (table) => eq(table.id, idOrganization), - }) - - return response({ - context: c, - statusCode: 200, - schema: updateLicenceSubscriptionRouteDefinition.schemas.return, - data: { - checkoutUrl: null, - }, - }) - }) diff --git a/packages/api/src/routes/auth/$idOrganization/organizationSettings/deleteOneOrganization.ts b/packages/api/src/routes/auth/$idOrganization/organizationSettings/deleteOneOrganization.ts deleted file mode 100644 index 78ea44c6..00000000 --- a/packages/api/src/routes/auth/$idOrganization/organizationSettings/deleteOneOrganization.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { deleteOneOrganizationRouteDefinition, models } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { Exception } from "../../../../utilities/exception.js" -import { response } from "../../../../utilities/response.js" -import { deleteOne } from "../../../../utilities/sql/deleteOne.js" -import { selectOne } from "../../../../utilities/sql/selectOne.js" - -export const deleteOneOrganizationRoute = apiFactory - .createApp() - .post(deleteOneOrganizationRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const _body = await validateBodyMiddleware({ - context: c, - schema: deleteOneOrganizationRouteDefinition.schemas.body, - }) - - const organizationUser = await selectOne({ - database: c.var.clients.sql, - table: models.organizationUser, - where: (table) => and(eq(table.idUser, user.id), eq(table.idOrganization, idOrganization)), - }) - if (organizationUser.isAdmin === false) { - throw new Exception({ - statusCode: 401, - internalMessage: "User is not admin of the organization", - externalMessage: "Vous n'êtes pas administrateur de l'organisation", - }) - } - - const deleteOneOrganization = await deleteOne({ - database: c.var.clients.sql, - table: models.organization, - where: (table) => and(eq(table.id, organizationUser.idOrganization)), - }) - - return response({ - context: c, - statusCode: 200, - schema: deleteOneOrganizationRouteDefinition.schemas.return, - data: deleteOneOrganization, - }) - }) diff --git a/packages/api/src/routes/auth/$idOrganization/organizationSettings/updateOneOrganization.ts b/packages/api/src/routes/auth/$idOrganization/organizationSettings/updateOneOrganization.ts deleted file mode 100644 index 4a032b9d..00000000 --- a/packages/api/src/routes/auth/$idOrganization/organizationSettings/updateOneOrganization.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { models, updateOneOrganizationRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { Exception } from "../../../../utilities/exception.js" -import { response } from "../../../../utilities/response.js" -import { selectOne } from "../../../../utilities/sql/selectOne.js" -import { updateOne } from "../../../../utilities/sql/updateOne.js" - -export const updateOneOrganizationRoute = apiFactory - .createApp() - .post(updateOneOrganizationRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: updateOneOrganizationRouteDefinition.schemas.body, - }) - - const organizationUser = await selectOne({ - database: c.var.clients.sql, - table: models.organizationUser, - where: (table) => and(eq(table.idUser, user.id), eq(table.idOrganization, idOrganization)), - }) - if (organizationUser.isAdmin === false) { - throw new Exception({ - statusCode: 401, - internalMessage: "User is not admin of the organization", - externalMessage: "Vous n'êtes pas administrateur de l'organisation", - }) - } - - const updateOneOrganization = await updateOne({ - database: c.var.clients.sql, - table: models.organization, - data: { - siren: body.siren, - name: body.name, - email: body.email, - lastUpdatedAt: new Date().toISOString(), - lastUpdatedBy: user.id, - }, - where: (table) => eq(table.id, organizationUser.idOrganization), - }) - - return response({ - context: c, - statusCode: 200, - schema: updateOneOrganizationRouteDefinition.schemas.return, - data: updateOneOrganization, - }) - }) diff --git a/packages/api/src/routes/auth/$idOrganization/organizationUser/$idOrganizationUser/$idOrganizationUserRoutes.ts b/packages/api/src/routes/auth/$idOrganization/organizationUser/$idOrganizationUser/$idOrganizationUserRoutes.ts deleted file mode 100644 index 8c261711..00000000 --- a/packages/api/src/routes/auth/$idOrganization/organizationUser/$idOrganizationUser/$idOrganizationUserRoutes.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { deleteOneOrganizationUserRoute } from "./deleteOneOrganizationUser.js" -import { readOneOrganizationUserRoute } from "./readOneOrganizationUser.js" -import { updateOneOrganizationUserRoute } from "./updateOneOrganizationUser.js" - -export const $idOrganizationUserRoutes = [ - deleteOneOrganizationUserRoute, - readOneOrganizationUserRoute, - updateOneOrganizationUserRoute, -] diff --git a/packages/api/src/routes/auth/$idOrganization/organizationUser/$idOrganizationUser/deleteOneOrganizationUser.ts b/packages/api/src/routes/auth/$idOrganization/organizationUser/$idOrganizationUser/deleteOneOrganizationUser.ts deleted file mode 100644 index ebdec1b2..00000000 --- a/packages/api/src/routes/auth/$idOrganization/organizationUser/$idOrganizationUser/deleteOneOrganizationUser.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { deleteOneOrganizationUserRouteDefinition, models } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { Exception } from "../../../../../utilities/exception.js" -import { response } from "../../../../../utilities/response.js" -import { deleteOne } from "../../../../../utilities/sql/deleteOne.js" -import { selectOne } from "../../../../../utilities/sql/selectOne.js" - -export const deleteOneOrganizationUserRoute = apiFactory - .createApp() - .post(deleteOneOrganizationUserRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: deleteOneOrganizationUserRouteDefinition.schemas.body, - }) - - // Check if the user is admin of the organization - const organizationUser = await selectOne({ - database: c.var.clients.sql, - table: models.organizationUser, - where: (table) => and(eq(table.idUser, user.id), eq(table.idOrganization, idOrganization)), - }) - if (organizationUser.isAdmin === false) { - throw new Exception({ - statusCode: 401, - internalMessage: "User is not admin of the organization", - externalMessage: "Vous n'êtes pas administrateur de l'organisation", - }) - } - - const deleteOneOrganizationUser = await deleteOne({ - database: c.var.clients.sql, - table: models.organizationUser, - where: (table) => and(eq(table.id, body.idOrganizationUser)), - }) - - return response({ - context: c, - statusCode: 200, - schema: deleteOneOrganizationUserRouteDefinition.schemas.return, - data: deleteOneOrganizationUser, - }) - }) diff --git a/packages/api/src/routes/auth/$idOrganization/organizationUser/$idOrganizationUser/updateOneOrganizationUser.ts b/packages/api/src/routes/auth/$idOrganization/organizationUser/$idOrganizationUser/updateOneOrganizationUser.ts deleted file mode 100644 index b1e80426..00000000 --- a/packages/api/src/routes/auth/$idOrganization/organizationUser/$idOrganizationUser/updateOneOrganizationUser.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { models, updateOneOrganizationUserRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { Exception } from "../../../../../utilities/exception.js" -import { response } from "../../../../../utilities/response.js" -import { selectOne } from "../../../../../utilities/sql/selectOne.js" -import { updateOne } from "../../../../../utilities/sql/updateOne.js" - -export const updateOneOrganizationUserRoute = apiFactory - .createApp() - .post(updateOneOrganizationUserRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: updateOneOrganizationUserRouteDefinition.schemas.body, - }) - - // Check if the user is admin of the organization - const organizationUser = await selectOne({ - database: c.var.clients.sql, - table: models.organizationUser, - where: (table) => and(eq(table.idUser, user.id), eq(table.idOrganization, idOrganization)), - }) - if (organizationUser.isAdmin === false) { - throw new Exception({ - statusCode: 401, - internalMessage: "User is not admin of the organization", - externalMessage: "Vous n'êtes pas administrateur de l'organisation", - }) - } - - if (body.idOrganizationUser === organizationUser.id) { - if (body.isAdmin === false) { - throw new Exception({ - statusCode: 400, - internalMessage: "User cannot update himself", - externalMessage: "Vous ne pouvez pas vous modifier vous-même", - }) - } - } - const updateOneOrganizationUser = await updateOne({ - database: c.var.clients.sql, - table: models.organizationUser, - data: { - isAdmin: body.isAdmin, - lastUpdatedAt: new Date().toISOString(), - lastUpdatedBy: user.id, - }, - where: (table) => eq(table.id, body.idOrganizationUser), - }) - - return response({ - context: c, - statusCode: 200, - schema: updateOneOrganizationUserRouteDefinition.schemas.return, - data: updateOneOrganizationUser, - }) - }) diff --git a/packages/api/src/routes/auth/$idOrganization/organizationUser/createOneOrganizationUser.ts b/packages/api/src/routes/auth/$idOrganization/organizationUser/createOneOrganizationUser.ts deleted file mode 100644 index e0de212d..00000000 --- a/packages/api/src/routes/auth/$idOrganization/organizationUser/createOneOrganizationUser.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { createOneOrganizationUserRouteDefinition, generateId, models } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { Exception } from "../../../../utilities/exception.js" -import { response } from "../../../../utilities/response.js" -import { insertOne } from "../../../../utilities/sql/insertOne.js" -import { selectOne } from "../../../../utilities/sql/selectOne.js" - -export const createOneOrganizationUserRoute = apiFactory - .createApp() - .post(createOneOrganizationUserRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: createOneOrganizationUserRouteDefinition.schemas.body, - }) - - // must be admin of the administration - const organizationUser = await selectOne({ - database: c.var.clients.sql, - table: models.organizationUser, - where: (table) => and(eq(table.idUser, user.id), eq(table.idOrganization, idOrganization)), - }) - if (organizationUser.isAdmin === false) { - throw new Exception({ - statusCode: 401, - internalMessage: "User is not admin of the organization", - externalMessage: "Vous n'êtes pas administrateur de l'organisation", - }) - } - - // the user must exist - const toAddUser = await selectOne({ - database: c.var.clients.sql, - table: models.user, - where: (table) => eq(table.email, body.user.email), - }) - if (toAddUser === undefined) { - throw new Exception({ - statusCode: 404, - internalMessage: "User not found", - externalMessage: "Utilisateur non trouvé", - }) - } - - const createOneOrganizationUser = await insertOne({ - database: c.var.clients.sql, - table: models.organizationUser, - data: { - id: generateId(), - idOrganization: organizationUser.idOrganization, - idUser: toAddUser.id, - isOwner: false, - isAdmin: body.isAdmin, - status: "invited", - createdAt: new Date().toISOString(), - lastUpdatedAt: null, - createdBy: user.id, - lastUpdatedBy: null, - }, - }) - - // await sendEmail({ - // to: updateUser.email, - // subject: "Invitation à collaborer sur Coulba", - // html: invitationTemplate({ - // urlInvitation: `${urlApp}/services/invitation?id=${updateUser.id}&token=${updateUser.invitationToken}`, - // urlWebsite: urlWebsite - // }) - // }) - - return response({ - context: c, - statusCode: 200, - schema: createOneOrganizationUserRouteDefinition.schemas.return, - data: createOneOrganizationUser, - }) - }) diff --git a/packages/api/src/routes/auth/$idOrganization/organizationUser/organizationUsersRoutes.ts b/packages/api/src/routes/auth/$idOrganization/organizationUser/organizationUsersRoutes.ts deleted file mode 100644 index 39f8d8c6..00000000 --- a/packages/api/src/routes/auth/$idOrganization/organizationUser/organizationUsersRoutes.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { $idOrganizationUserRoutes } from "./$idOrganizationUser/$idOrganizationUserRoutes.js" -import { createOneOrganizationUserRoute } from "./createOneOrganizationUser.js" -import { readAllOrganizationUsersRoute } from "./readAllOrganizationUsers.js" - -export const organizationUsersRoutes = [ - createOneOrganizationUserRoute, - readAllOrganizationUsersRoute, - - ...$idOrganizationUserRoutes, -] diff --git a/packages/api/src/routes/auth/$idOrganization/organizationUser/readAllOrganizationUsers.ts b/packages/api/src/routes/auth/$idOrganization/organizationUser/readAllOrganizationUsers.ts deleted file mode 100644 index 151336a7..00000000 --- a/packages/api/src/routes/auth/$idOrganization/organizationUser/readAllOrganizationUsers.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { models, readAllOrganizationUsersRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { Exception } from "../../../../utilities/exception.js" -import { response } from "../../../../utilities/response.js" -import { selectOne } from "../../../../utilities/sql/selectOne.js" - -export const readAllOrganizationUsersRoute = apiFactory - .createApp() - .post(readAllOrganizationUsersRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const _body = await validateBodyMiddleware({ - context: c, - schema: readAllOrganizationUsersRouteDefinition.schemas.body, - }) - - const organizationUser = await selectOne({ - database: c.var.clients.sql, - table: models.organizationUser, - where: (table) => and(eq(table.idUser, user.id), eq(table.idOrganization, idOrganization)), - }) - if (organizationUser.isAdmin === false) { - throw new Exception({ - statusCode: 401, - internalMessage: "User is not admin of the organization", - externalMessage: "Vous n'êtes pas administrateur de l'organisation", - }) - } - - const readAllOrganizationUsers = await c.var.clients.sql.query.organizationUserModel.findMany({ - where: (table) => and(eq(table.idOrganization, organizationUser.idOrganization)), - with: { - user: true, - }, - }) - - return response({ - context: c, - statusCode: 200, - schema: readAllOrganizationUsersRouteDefinition.schemas.return, - data: readAllOrganizationUsers, - }) - }) diff --git a/packages/api/src/routes/auth/$idOrganization/readOneOrganization.ts b/packages/api/src/routes/auth/$idOrganization/readOneOrganization.ts deleted file mode 100644 index 30a748a0..00000000 --- a/packages/api/src/routes/auth/$idOrganization/readOneOrganization.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { models, readOneOrganizationRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../utilities/apiFactory.js" -import { Exception } from "../../../utilities/exception.js" -import { response } from "../../../utilities/response.js" -import { selectOne } from "../../../utilities/sql/selectOne.js" - -export const readOneOrganizationRoute = apiFactory - .createApp() - .post(readOneOrganizationRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: readOneOrganizationRouteDefinition.schemas.body, - }) - - if (body.idOrganization !== idOrganization) { - throw new Exception({ - statusCode: 403, - internalMessage: "Body organization id does not match active organization", - externalMessage: "L'organisation demandée ne correspond pas à l'organisation active.", - }) - } - - const organizationUser = await selectOne({ - database: c.var.clients.sql, - table: models.organizationUser, - where: (table) => and(eq(table.idOrganization, body.idOrganization), eq(table.idUser, user.id)), - }) - - const readOneOrganization = await selectOne({ - database: c.var.clients.sql, - table: models.organization, - where: (table) => eq(table.id, organizationUser.idOrganization), - }) - - return response({ - context: c, - statusCode: 200, - schema: readOneOrganizationRouteDefinition.schemas.return, - data: readOneOrganization, - }) - }) diff --git a/packages/api/src/routes/auth/$idOrganization/years/readAllYears.ts b/packages/api/src/routes/auth/$idOrganization/years/readAllYears.ts deleted file mode 100644 index 9076dae2..00000000 --- a/packages/api/src/routes/auth/$idOrganization/years/readAllYears.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { models, readAllYearsRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { response } from "../../../../utilities/response.js" -import { selectMany } from "../../../../utilities/sql/selectMany.js" - -export const readAllYearsRoute = apiFactory.createApp().post(readAllYearsRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const _body = await validateBodyMiddleware({ - context: c, - schema: readAllYearsRouteDefinition.schemas.body, - }) - - const readAllYears = await selectMany({ - database: c.var.clients.sql, - table: models.year, - where: (table) => and(eq(table.idOrganization, idOrganization)), - }) - - return response({ - context: c, - statusCode: 200, - schema: readAllYearsRouteDefinition.schemas.return, - data: readAllYears, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/entries/$idEntry/deleteOneEntry.ts b/packages/api/src/routes/auth/$idYear/entries/$idEntry/deleteOneEntry.ts deleted file mode 100644 index 963b7757..00000000 --- a/packages/api/src/routes/auth/$idYear/entries/$idEntry/deleteOneEntry.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { deleteOneEntryRouteDefinition, models } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { response } from "../../../../../utilities/response.js" -import { deleteOne } from "../../../../../utilities/sql/deleteOne.js" - -export const deleteOneEntryRoute = apiFactory.createApp().post(deleteOneEntryRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: deleteOneEntryRouteDefinition.schemas.body, - }) - - const deleteOneEntry = await deleteOne({ - database: c.var.clients.sql, - table: models.entry, - where: (table) => - and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear), eq(table.id, body.idEntry)), - }) - - return response({ - context: c, - statusCode: 200, - schema: deleteOneEntryRouteDefinition.schemas.return, - data: deleteOneEntry, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/entries/$idEntry/entryLines/$idEntryLine/deleteOneEntryLine.ts b/packages/api/src/routes/auth/$idYear/entries/$idEntry/entryLines/$idEntryLine/deleteOneEntryLine.ts deleted file mode 100644 index e1d23f59..00000000 --- a/packages/api/src/routes/auth/$idYear/entries/$idEntry/entryLines/$idEntryLine/deleteOneEntryLine.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { deleteOneEntryLineRouteDefinition, models } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../../../utilities/apiFactory.js" -import { response } from "../../../../../../../utilities/response.js" -import { deleteOne } from "../../../../../../../utilities/sql/deleteOne.js" - -export const deleteOneEntryLineRoute = apiFactory - .createApp() - .post(deleteOneEntryLineRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: deleteOneEntryLineRouteDefinition.schemas.body, - }) - - const deleteOneEntryLine = await deleteOne({ - database: c.var.clients.sql, - table: models.entryLine, - where: (table) => - and( - eq(table.idOrganization, idOrganization), - eq(table.idYear, body.idYear), - eq(table.id, body.idEntryLine), - ), - }) - - return response({ - context: c, - statusCode: 200, - schema: deleteOneEntryLineRouteDefinition.schemas.return, - data: deleteOneEntryLine, - }) - }) diff --git a/packages/api/src/routes/auth/$idYear/entries/$idEntry/entryLines/$idEntryLine/readOneEntryLine.ts b/packages/api/src/routes/auth/$idYear/entries/$idEntry/entryLines/$idEntryLine/readOneEntryLine.ts deleted file mode 100644 index 3c35e80c..00000000 --- a/packages/api/src/routes/auth/$idYear/entries/$idEntry/entryLines/$idEntryLine/readOneEntryLine.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { models, readOneEntryLineRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../../../utilities/apiFactory.js" -import { response } from "../../../../../../../utilities/response.js" -import { selectOne } from "../../../../../../../utilities/sql/selectOne.js" - -export const readOneEntryLineRoute = apiFactory.createApp().post(readOneEntryLineRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: readOneEntryLineRouteDefinition.schemas.body, - }) - - const readOneEntryLine = await selectOne({ - database: c.var.clients.sql, - table: models.entryLine, - where: (table) => - and( - eq(table.idOrganization, idOrganization), - eq(table.idYear, body.idYear), - eq(table.id, body.idEntryLine), - ), - }) - - return response({ - context: c, - statusCode: 200, - schema: readOneEntryLineRouteDefinition.schemas.return, - data: readOneEntryLine, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/entries/$idEntry/entryLines/$idEntryLine/updateOneEntryLine.ts b/packages/api/src/routes/auth/$idYear/entries/$idEntry/entryLines/$idEntryLine/updateOneEntryLine.ts deleted file mode 100644 index aa9105aa..00000000 --- a/packages/api/src/routes/auth/$idYear/entries/$idEntry/entryLines/$idEntryLine/updateOneEntryLine.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { models, updateOneEntryLineRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../../../utilities/apiFactory.js" -import { response } from "../../../../../../../utilities/response.js" -import { updateOne } from "../../../../../../../utilities/sql/updateOne.js" - -export const updateOneEntryLineRoute = apiFactory - .createApp() - .post(updateOneEntryLineRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: updateOneEntryLineRouteDefinition.schemas.body, - }) - - const updatedEntryLine = await updateOne({ - database: c.var.clients.sql, - table: models.entryLine, - data: { - idEntry: body.idEntry, - idAccount: body.idAccount, - isComputedForJournalReport: body.isComputedForJournalReport, - isComputedForLedgerReport: body.isComputedForLedgerReport, - isComputedForBalanceReport: body.isComputedForBalanceReport, - isComputedForBalanceSheetReport: body.isComputedForBalanceSheetReport, - isComputedForIncomeStatementReport: body.isComputedForIncomeStatementReport, - label: body.label, - debit: body.debit, - credit: body.credit, - lastUpdatedAt: new Date().toISOString(), - lastUpdatedBy: user.id, - }, - where: (table) => - and( - eq(table.idOrganization, idOrganization), - eq(table.idYear, body.idYear), - eq(table.id, body.idEntryLine), - ), - }) - - return response({ - context: c, - statusCode: 200, - schema: updateOneEntryLineRouteDefinition.schemas.return, - data: updatedEntryLine, - }) - }) diff --git a/packages/api/src/routes/auth/$idYear/entries/$idEntry/entryLines/createOneEntryLine.ts b/packages/api/src/routes/auth/$idYear/entries/$idEntry/entryLines/createOneEntryLine.ts deleted file mode 100644 index ae6e50f1..00000000 --- a/packages/api/src/routes/auth/$idYear/entries/$idEntry/entryLines/createOneEntryLine.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { createOneEntryLineRouteDefinition, generateId, models } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../../utilities/apiFactory.js" -import { response } from "../../../../../../utilities/response.js" -import { insertOne } from "../../../../../../utilities/sql/insertOne.js" -import { selectOne } from "../../../../../../utilities/sql/selectOne.js" - -export const createOneEntryLineRoute = apiFactory - .createApp() - .post(createOneEntryLineRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: createOneEntryLineRouteDefinition.schemas.body, - }) - - const readOneEntry = await selectOne({ - database: c.var.clients.sql, - table: models.entry, - where: (table) => - and( - eq(table.idOrganization, idOrganization), - eq(table.idYear, body.idYear), - eq(table.id, body.idEntry), - ), - }) - - const createOneEntryLine = await insertOne({ - database: c.var.clients.sql, - table: models.entryLine, - data: { - id: generateId(), - idOrganization: idOrganization, - idYear: body.idYear, - idEntry: body.idEntry, - idAccount: body.idAccount, - isComputedForJournalReport: body.isComputedForJournalReport, - isComputedForLedgerReport: body.isComputedForLedgerReport, - isComputedForBalanceReport: body.isComputedForBalanceReport, - isComputedForBalanceSheetReport: body.isComputedForBalanceSheetReport, - isComputedForIncomeStatementReport: body.isComputedForIncomeStatementReport, - label: body.label ?? readOneEntry.label, - debit: body.debit ?? "0.00", - credit: body.credit ?? "0.00", - createdAt: new Date().toISOString(), - lastUpdatedAt: null, - createdBy: user.id, - lastUpdatedBy: null, - }, - }) - - return response({ - context: c, - statusCode: 200, - schema: createOneEntryLineRouteDefinition.schemas.return, - data: createOneEntryLine, - }) - }) diff --git a/packages/api/src/routes/auth/$idYear/entries/$idEntry/entryLines/readAllEntryLines.ts b/packages/api/src/routes/auth/$idYear/entries/$idEntry/entryLines/readAllEntryLines.ts deleted file mode 100644 index 69eb7935..00000000 --- a/packages/api/src/routes/auth/$idYear/entries/$idEntry/entryLines/readAllEntryLines.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { models, readAllEntryLinesRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../../utilities/apiFactory.js" -import { response } from "../../../../../../utilities/response.js" -import { selectMany } from "../../../../../../utilities/sql/selectMany.js" - -export const readAllEntryLinesRoute = apiFactory.createApp().post(readAllEntryLinesRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: readAllEntryLinesRouteDefinition.schemas.body, - }) - - const readAllEntryLines = await selectMany({ - database: c.var.clients.sql, - table: models.entryLine, - where: (table) => { - if (body.idEntry !== null && body.idEntry !== undefined) { - return and( - eq(table.idOrganization, idOrganization), - eq(table.idYear, body.idYear), - eq(table.idEntry, body.idEntry), - ) - } - return and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear)) - }, - }) - - return response({ - context: c, - statusCode: 200, - schema: readAllEntryLinesRouteDefinition.schemas.return, - data: readAllEntryLines, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/entries/$idEntry/entryLines/updateManyEntryLines.ts b/packages/api/src/routes/auth/$idYear/entries/$idEntry/entryLines/updateManyEntryLines.ts deleted file mode 100644 index 8a397eeb..00000000 --- a/packages/api/src/routes/auth/$idYear/entries/$idEntry/entryLines/updateManyEntryLines.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { models, updateManyEntryLinesRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../../utilities/apiFactory.js" -import { response } from "../../../../../../utilities/response.js" -import { selectMany } from "../../../../../../utilities/sql/selectMany.js" -import { updateOne } from "../../../../../../utilities/sql/updateOne.js" - -export const updateManyEntryLinesRoute = apiFactory - .createApp() - .post(updateManyEntryLinesRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: updateManyEntryLinesRouteDefinition.schemas.body, - }) - - const readAllEntryLines = await selectMany({ - database: c.var.clients.sql, - table: models.entryLine, - where: (table) => - and( - eq(table.idOrganization, idOrganization), - eq(table.idYear, body.idYear), - eq(table.idEntry, body.idEntry), - ), - }) - - const updatedEntryLines = await c.var.clients.sql.transaction(async (_tx) => { - const entryLines = [] - for (const entryLine of readAllEntryLines) { - const updatedEntryLine = await updateOne({ - database: c.var.clients.sql, - table: models.entryLine, - data: { - isComputedForJournalReport: body.isComputedForJournalReport, - isComputedForLedgerReport: body.isComputedForLedgerReport, - isComputedForBalanceReport: body.isComputedForBalanceReport, - isComputedForBalanceSheetReport: body.isComputedForBalanceSheetReport, - isComputedForIncomeStatementReport: body.isComputedForIncomeStatementReport, - label: body.label, - lastUpdatedAt: new Date().toISOString(), - lastUpdatedBy: user.id, - }, - where: (table) => - and( - eq(table.idOrganization, idOrganization), - eq(table.idYear, body.idYear), - eq(table.id, entryLine.id), - ), - }) - entryLines.push(updatedEntryLine) - } - - return entryLines - }) - - return response({ - context: c, - statusCode: 200, - schema: updateManyEntryLinesRouteDefinition.schemas.return, - data: updatedEntryLines, - }) - }) diff --git a/packages/api/src/routes/auth/$idYear/entries/$idEntry/entryTags/addOneEntryTag.ts b/packages/api/src/routes/auth/$idYear/entries/$idEntry/entryTags/addOneEntryTag.ts deleted file mode 100644 index 0411c51f..00000000 --- a/packages/api/src/routes/auth/$idYear/entries/$idEntry/entryTags/addOneEntryTag.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { addOneEntryTagRouteDefinition, generateId, models } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../../utilities/apiFactory.js" -import { response } from "../../../../../../utilities/response.js" -import { insertOne } from "../../../../../../utilities/sql/insertOne.js" -import { selectOne } from "../../../../../../utilities/sql/selectOne.js" - -export const addOneEntryTagRoute = apiFactory.createApp().post(addOneEntryTagRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: addOneEntryTagRouteDefinition.schemas.body, - }) - - // Verify entry exists - await selectOne({ - database: c.var.clients.sql, - table: models.entry, - where: (table) => - and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear), eq(table.id, body.idEntry)), - }) - - // Verify tag exists - await selectOne({ - database: c.var.clients.sql, - table: models.tag, - where: (table) => - and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear), eq(table.id, body.idTag)), - }) - - const addOneEntryTag = await insertOne({ - database: c.var.clients.sql, - table: models.entryTag, - data: { - id: generateId(), - idOrganization: idOrganization, - idYear: body.idYear, - idEntry: body.idEntry, - idTag: body.idTag, - createdAt: new Date().toISOString(), - }, - }) - - return response({ - context: c, - statusCode: 200, - schema: addOneEntryTagRouteDefinition.schemas.return, - data: addOneEntryTag, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/entries/$idEntry/entryTags/removeOneEntryTag.ts b/packages/api/src/routes/auth/$idYear/entries/$idEntry/entryTags/removeOneEntryTag.ts deleted file mode 100644 index 2b07f6a2..00000000 --- a/packages/api/src/routes/auth/$idYear/entries/$idEntry/entryTags/removeOneEntryTag.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { models, removeOneEntryTagRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../../utilities/apiFactory.js" -import { response } from "../../../../../../utilities/response.js" -import { deleteOne } from "../../../../../../utilities/sql/deleteOne.js" - -export const removeOneEntryTagRoute = apiFactory.createApp().post(removeOneEntryTagRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: removeOneEntryTagRouteDefinition.schemas.body, - }) - - const removeOneEntryTag = await deleteOne({ - database: c.var.clients.sql, - table: models.entryTag, - where: (table) => - and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear), eq(table.id, body.idEntryTag)), - }) - - return response({ - context: c, - statusCode: 200, - schema: removeOneEntryTagRouteDefinition.schemas.return, - data: removeOneEntryTag, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/entries/$idEntry/readOneEntry.ts b/packages/api/src/routes/auth/$idYear/entries/$idEntry/readOneEntry.ts deleted file mode 100644 index 6ac7e85b..00000000 --- a/packages/api/src/routes/auth/$idYear/entries/$idEntry/readOneEntry.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { models, readOneEntryRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { response } from "../../../../../utilities/response.js" -import { selectOne } from "../../../../../utilities/sql/selectOne.js" - -export const readOneEntryRoute = apiFactory.createApp().post(readOneEntryRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: readOneEntryRouteDefinition.schemas.body, - }) - - const readOneEntry = await selectOne({ - database: c.var.clients.sql, - table: models.entry, - where: (table) => - and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear), eq(table.id, body.idEntry)), - }) - - return response({ - context: c, - statusCode: 200, - schema: readOneEntryRouteDefinition.schemas.return, - data: readOneEntry, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/entries/$idEntry/updateOneEntry.ts b/packages/api/src/routes/auth/$idYear/entries/$idEntry/updateOneEntry.ts deleted file mode 100644 index 84477ab8..00000000 --- a/packages/api/src/routes/auth/$idYear/entries/$idEntry/updateOneEntry.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { models, updateOneEntryRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { response } from "../../../../../utilities/response.js" -import { updateOne } from "../../../../../utilities/sql/updateOne.js" - -export const updateOneEntryRoute = apiFactory.createApp().post(updateOneEntryRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: updateOneEntryRouteDefinition.schemas.body, - }) - - const updatedEntry = await c.var.clients.sql.transaction(async (tx) => { - const updatedEntry = await updateOne({ - database: tx, - table: models.entry, - data: { - idJournal: body.idJournal, - idFile: body.idFile, - label: body.label, - date: body.date, - lastUpdatedAt: new Date().toISOString(), - lastUpdatedBy: user.id, - }, - where: (table) => - and( - eq(table.idOrganization, idOrganization), - eq(table.idYear, body.idYear), - eq(table.id, body.idEntry), - ), - }) - - return updatedEntry - }) - - return response({ - context: c, - statusCode: 200, - schema: updateOneEntryRouteDefinition.schemas.return, - data: updatedEntry, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/entries/createOneEntry.ts b/packages/api/src/routes/auth/$idYear/entries/createOneEntry.ts deleted file mode 100644 index 69a3e6c6..00000000 --- a/packages/api/src/routes/auth/$idYear/entries/createOneEntry.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { createOneEntryRouteDefinition, generateId, models } from "@arrhes/application-metadata" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { response } from "../../../../utilities/response.js" -import { insertOne } from "../../../../utilities/sql/insertOne.js" - -export const createOneEntryRoute = apiFactory.createApp().post(createOneEntryRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: createOneEntryRouteDefinition.schemas.body, - }) - - const createOneEntry = await insertOne({ - database: c.var.clients.sql, - table: models.entry, - data: { - id: generateId(), - idOrganization: idOrganization, - idYear: body.idYear, - idJournal: body.idJournal, - idFile: body.idFile, - label: body.label, - date: body.date, - createdAt: new Date().toISOString(), - lastUpdatedAt: null, - createdBy: user.id, - lastUpdatedBy: null, - }, - }) - - return response({ - context: c, - statusCode: 200, - schema: createOneEntryRouteDefinition.schemas.return, - data: createOneEntry, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/entries/readAllEntries.ts b/packages/api/src/routes/auth/$idYear/entries/readAllEntries.ts deleted file mode 100644 index 997fdd8e..00000000 --- a/packages/api/src/routes/auth/$idYear/entries/readAllEntries.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { models, readAllEntriesRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { response } from "../../../../utilities/response.js" -import { selectMany } from "../../../../utilities/sql/selectMany.js" - -export const readAllEntriesRoute = apiFactory.createApp().post(readAllEntriesRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: readAllEntriesRouteDefinition.schemas.body, - }) - - const readAllEntries = await selectMany({ - database: c.var.clients.sql, - table: models.entry, - where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear)), - }) - - return response({ - context: c, - statusCode: 200, - schema: readAllEntriesRouteDefinition.schemas.return, - data: readAllEntries, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/entries/readAllEntryTags.ts b/packages/api/src/routes/auth/$idYear/entries/readAllEntryTags.ts deleted file mode 100644 index c53b34b9..00000000 --- a/packages/api/src/routes/auth/$idYear/entries/readAllEntryTags.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { models, readAllEntryTagsRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { response } from "../../../../utilities/response.js" -import { selectMany } from "../../../../utilities/sql/selectMany.js" - -export const readAllEntryTagsRoute = apiFactory.createApp().post(readAllEntryTagsRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: readAllEntryTagsRouteDefinition.schemas.body, - }) - - const readAllEntryTags = await selectMany({ - database: c.var.clients.sql, - table: models.entryTag, - where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear)), - }) - - return response({ - context: c, - statusCode: 200, - schema: readAllEntryTagsRouteDefinition.schemas.return, - data: readAllEntryTags, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/files/$idFile/deleteOneFile.ts b/packages/api/src/routes/auth/$idYear/files/$idFile/deleteOneFile.ts deleted file mode 100644 index 04bfa159..00000000 --- a/packages/api/src/routes/auth/$idYear/files/$idFile/deleteOneFile.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { deleteOneFileRouteDefinition, models } from "@arrhes/application-metadata" -import { and, eq, sql } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { response } from "../../../../../utilities/response.js" -import { deleteOne } from "../../../../../utilities/sql/deleteOne.js" -import { selectOne } from "../../../../../utilities/sql/selectOne.js" -import { updateOne } from "../../../../../utilities/sql/updateOne.js" - -export const deleteOneFileRoute = apiFactory.createApp().post(deleteOneFileRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: deleteOneFileRouteDefinition.schemas.body, - }) - - const deletedFile = await c.var.clients.sql.transaction(async (tx) => { - const readOneFile = await selectOne({ - database: tx, - table: models.file, - where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.id, body.idFile)), - }) - - if (readOneFile.size !== null && readOneFile.size > 0) { - await updateOne({ - database: tx, - table: models.organization, - data: { - storageCurrentUsage: sql`GREATEST(${models.organization.storageCurrentUsage} - ${readOneFile.size}, 0)`, - }, - where: (table) => eq(table.id, idOrganization), - }) - } - - const deleteOneFile = await deleteOne({ - database: tx, - table: models.file, - where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.id, body.idFile)), - }) - return deleteOneFile - }) - - return response({ - context: c, - statusCode: 200, - schema: deleteOneFileRouteDefinition.schemas.return, - data: deletedFile, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/files/$idFile/finalizeFileUpload.ts b/packages/api/src/routes/auth/$idYear/files/$idFile/finalizeFileUpload.ts deleted file mode 100644 index 60abc761..00000000 --- a/packages/api/src/routes/auth/$idYear/files/$idFile/finalizeFileUpload.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { finalizeFileUploadRouteDefinition, models } from "@arrhes/application-metadata" -import { HeadObjectCommand } from "@aws-sdk/client-s3" -import { and, eq, sql } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { response } from "../../../../../utilities/response.js" -import { updateOne } from "../../../../../utilities/sql/updateOne.js" - -export const finalizeFileUploadRoute = apiFactory - .createApp() - .post(finalizeFileUploadRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: finalizeFileUploadRouteDefinition.schemas.body, - }) - - const storageKey = `organizations/${idOrganization}/storage/${body.idFile}` - const storageHead = await c.var.clients.storage.send( - new HeadObjectCommand({ - Bucket: c.var.env.STORAGE_BUCKET_NAME, - Key: storageKey, - }), - ) - - const updateOneFile = await updateOne({ - database: c.var.clients.sql, - table: models.file, - data: { - storageKey: storageKey, - type: storageHead.ContentType, - size: storageHead.ContentLength, - lastUpdatedAt: new Date().toISOString(), - lastUpdatedBy: user.id, - }, - where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.id, body.idFile)), - }) - - await updateOne({ - database: c.var.clients.sql, - table: models.organization, - data: { - storageCurrentUsage: sql`${models.organization.storageCurrentUsage} + ${storageHead.ContentLength}`, - }, - where: (table) => eq(table.id, idOrganization), - }) - - return response({ - context: c, - statusCode: 200, - schema: finalizeFileUploadRouteDefinition.schemas.return, - data: updateOneFile, - }) - }) diff --git a/packages/api/src/routes/auth/$idYear/files/$idFile/generateFileDeleteSignedUrl.ts b/packages/api/src/routes/auth/$idYear/files/$idFile/generateFileDeleteSignedUrl.ts deleted file mode 100644 index 8c4f64e7..00000000 --- a/packages/api/src/routes/auth/$idYear/files/$idFile/generateFileDeleteSignedUrl.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { generateFileDeleteSignedUrlRouteDefinition, models } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { Exception } from "../../../../../utilities/exception.js" -import { response } from "../../../../../utilities/response.js" -import { selectOne } from "../../../../../utilities/sql/selectOne.js" -import { generateDeleteSignedUrl } from "../../../../../utilities/storage/generateDeleteSignedUrl.js" - -export const generateFileDeleteSignedUrlRoute = apiFactory - .createApp() - .post(generateFileDeleteSignedUrlRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: generateFileDeleteSignedUrlRouteDefinition.schemas.body, - }) - - const readOneFile = await selectOne({ - database: c.var.clients.sql, - table: models.file, - where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.id, body.idFile)), - }) - - if (readOneFile.storageKey === null) { - throw new Exception({ - internalMessage: "File storage key not found", - statusCode: 400, - externalMessage: "Le fichier associé n'existe pas", - }) - } - - const url = await generateDeleteSignedUrl({ - var: c.var, - storageKey: readOneFile.storageKey, - }) - - return response({ - context: c, - statusCode: 200, - schema: generateFileDeleteSignedUrlRouteDefinition.schemas.return, - data: { - url, - }, - }) - }) diff --git a/packages/api/src/routes/auth/$idYear/files/$idFile/generateFileGetSignedUrl.ts b/packages/api/src/routes/auth/$idYear/files/$idFile/generateFileGetSignedUrl.ts deleted file mode 100644 index d2e23054..00000000 --- a/packages/api/src/routes/auth/$idYear/files/$idFile/generateFileGetSignedUrl.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { generateFileGetSignedUrlRouteDefinition, models } from "@arrhes/application-metadata" -import { and, eq, isNull } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { Exception } from "../../../../../utilities/exception.js" -import { response } from "../../../../../utilities/response.js" -import { selectOne } from "../../../../../utilities/sql/selectOne.js" -import { generateGetSignedUrl } from "../../../../../utilities/storage/generateGetSignedUrl.js" - -export const generateFileGetSignedUrlRoute = apiFactory - .createApp() - .post(generateFileGetSignedUrlRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: generateFileGetSignedUrlRouteDefinition.schemas.body, - }) - - const readOneFile = await selectOne({ - database: c.var.clients.sql, - table: models.file, - where: (table) => - and( - eq(table.idOrganization, idOrganization), - body.idYear !== null ? eq(table.idYear, body.idYear) : isNull(table.idYear), - eq(table.id, body.idFile), - ), - }) - - if (readOneFile.storageKey === null) { - throw new Exception({ - internalMessage: "File storage key not found", - statusCode: 400, - externalMessage: "Le fichier associé n'existe pas", - }) - } - - const url = await generateGetSignedUrl({ - var: c.var, - storageKey: readOneFile.storageKey, - }) - - return response({ - context: c, - statusCode: 200, - schema: generateFileGetSignedUrlRouteDefinition.schemas.return, - data: { - url, - }, - }) - }) diff --git a/packages/api/src/routes/auth/$idYear/files/$idFile/generateFilePutSignedUrl.ts b/packages/api/src/routes/auth/$idYear/files/$idFile/generateFilePutSignedUrl.ts deleted file mode 100644 index 6901a29c..00000000 --- a/packages/api/src/routes/auth/$idYear/files/$idFile/generateFilePutSignedUrl.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { generateFilePutSignedUrlRouteDefinition, models } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { Exception } from "../../../../../utilities/exception.js" -import { response } from "../../../../../utilities/response.js" -import { selectOne } from "../../../../../utilities/sql/selectOne.js" -import { generatePutSignedUrl } from "../../../../../utilities/storage/generatePutSignedUrl.js" - -export const generateFilePutSignedUrlRoute = apiFactory - .createApp() - .post(generateFilePutSignedUrlRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: generateFilePutSignedUrlRouteDefinition.schemas.body, - }) - - if (body.size > 50_000_000) { - throw new Exception({ - internalMessage: "File size is too big", - statusCode: 400, - externalMessage: "Fichier trop volumineux", - }) - } - - const readOneFile = await selectOne({ - database: c.var.clients.sql, - table: models.file, - where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.id, body.idFile)), - }) - - const organization = await selectOne({ - database: c.var.clients.sql, - table: models.organization, - where: (table) => eq(table.id, idOrganization), - }) - - if (organization.storageCurrentUsage + body.size > organization.storageLimit) { - throw new Exception({ - internalMessage: "Storage limit exceeded", - statusCode: 400, - externalMessage: "Limite de stockage atteinte", - }) - } - - const storageKey = `organizations/${idOrganization}/storage/${body.idFile}` - - const url = await generatePutSignedUrl({ - var: c.var, - storageKey: storageKey, - contentLength: body.size, - contentType: body.type, - metadata: { - idOrganization: idOrganization, - idFile: readOneFile.id, - idUser: user.id, - }, - }) - - return response({ - context: c, - statusCode: 200, - schema: generateFilePutSignedUrlRouteDefinition.schemas.return, - data: { - file: readOneFile, - url: url, - }, - }) - }) diff --git a/packages/api/src/routes/auth/$idYear/files/$idFile/readOneFile.ts b/packages/api/src/routes/auth/$idYear/files/$idFile/readOneFile.ts deleted file mode 100644 index 29b39c0b..00000000 --- a/packages/api/src/routes/auth/$idYear/files/$idFile/readOneFile.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { models, readOneFileRouteDefinition } from "@arrhes/application-metadata" -import { and, eq, isNull } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { response } from "../../../../../utilities/response.js" -import { selectOne } from "../../../../../utilities/sql/selectOne.js" - -export const readOneFileRoute = apiFactory.createApp().post(readOneFileRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: readOneFileRouteDefinition.schemas.body, - }) - - const readOneFile = await selectOne({ - database: c.var.clients.sql, - table: models.file, - where: (table) => - and( - eq(table.idOrganization, idOrganization), - body.idYear !== null ? eq(table.idYear, body.idYear) : isNull(table.idYear), - eq(table.id, body.idFile), - ), - }) - - return response({ - context: c, - statusCode: 200, - schema: readOneFileRouteDefinition.schemas.return, - data: readOneFile, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/files/$idFile/updateOneFile.ts b/packages/api/src/routes/auth/$idYear/files/$idFile/updateOneFile.ts deleted file mode 100644 index abe8090f..00000000 --- a/packages/api/src/routes/auth/$idYear/files/$idFile/updateOneFile.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { models, updateOneFileRouteDefinition } from "@arrhes/application-metadata" -import { and, eq, isNull } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { response } from "../../../../../utilities/response.js" -import { updateOne } from "../../../../../utilities/sql/updateOne.js" - -export const updateOneFileRoute = apiFactory.createApp().post(updateOneFileRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: updateOneFileRouteDefinition.schemas.body, - }) - - const updateOneFile = await updateOne({ - database: c.var.clients.sql, - table: models.file, - data: { - reference: body.reference, - name: body.name, - idFolder: body.idFolder, - lastUpdatedAt: new Date().toISOString(), - lastUpdatedBy: user.id, - }, - where: (table) => - and( - eq(table.idOrganization, idOrganization), - body.idYear !== null ? eq(table.idYear, body.idYear) : isNull(table.idYear), - eq(table.id, body.idFile), - ), - }) - - return response({ - context: c, - statusCode: 200, - schema: updateOneFileRouteDefinition.schemas.return, - data: updateOneFile, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/files/createOneFile.ts b/packages/api/src/routes/auth/$idYear/files/createOneFile.ts deleted file mode 100644 index 5d56faa9..00000000 --- a/packages/api/src/routes/auth/$idYear/files/createOneFile.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { createOneFileRouteDefinition, generateId, models } from "@arrhes/application-metadata" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { response } from "../../../../utilities/response.js" -import { insertOne } from "../../../../utilities/sql/insertOne.js" - -export const createOneFileRoute = apiFactory.createApp().post(createOneFileRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: createOneFileRouteDefinition.schemas.body, - }) - - const createOneFile = await insertOne({ - database: c.var.clients.sql, - table: models.file, - data: { - id: generateId(), - idOrganization: idOrganization, - idYear: body.idYear, - idFolder: body.idFolder, - reference: body.reference, - name: body.name, - storageKey: null, - type: null, - size: null, - hash: body.hash, - createdAt: new Date().toISOString(), - lastUpdatedAt: null, - createdBy: user.id, - lastUpdatedBy: null, - }, - }) - - return response({ - context: c, - statusCode: 200, - schema: createOneFileRouteDefinition.schemas.return, - data: createOneFile, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/files/readAllFiles.ts b/packages/api/src/routes/auth/$idYear/files/readAllFiles.ts deleted file mode 100644 index 6d3e6555..00000000 --- a/packages/api/src/routes/auth/$idYear/files/readAllFiles.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { models, readAllFilesRouteDefinition } from "@arrhes/application-metadata" -import { and, eq, isNull } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { response } from "../../../../utilities/response.js" -import { selectMany } from "../../../../utilities/sql/selectMany.js" - -export const readAllFilesRoute = apiFactory.createApp().post(readAllFilesRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: readAllFilesRouteDefinition.schemas.body, - }) - - const readAllFiles = await selectMany({ - database: c.var.clients.sql, - table: models.file, - where: (table) => - and( - eq(table.idOrganization, idOrganization), - body.idYear !== null ? eq(table.idYear, body.idYear) : isNull(table.idYear), - ), - }) - - return response({ - context: c, - statusCode: 200, - schema: readAllFilesRouteDefinition.schemas.return, - data: readAllFiles, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/folders/$idFolder/deleteOneFolder.ts b/packages/api/src/routes/auth/$idYear/folders/$idFolder/deleteOneFolder.ts deleted file mode 100644 index f5e3033c..00000000 --- a/packages/api/src/routes/auth/$idYear/folders/$idFolder/deleteOneFolder.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { deleteOneFolderRouteDefinition, models } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { response } from "../../../../../utilities/response.js" -import { deleteOne } from "../../../../../utilities/sql/deleteOne.js" - -export const deleteOneFolderRoute = apiFactory.createApp().post(deleteOneFolderRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: deleteOneFolderRouteDefinition.schemas.body, - }) - - const deleteOneFolder = await deleteOne({ - database: c.var.clients.sql, - table: models.folder, - where: (table) => - and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear), eq(table.id, body.idFolder)), - }) - - return response({ - context: c, - statusCode: 200, - schema: deleteOneFolderRouteDefinition.schemas.return, - data: deleteOneFolder, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/folders/$idFolder/readOneFolder.ts b/packages/api/src/routes/auth/$idYear/folders/$idFolder/readOneFolder.ts deleted file mode 100644 index 6be72567..00000000 --- a/packages/api/src/routes/auth/$idYear/folders/$idFolder/readOneFolder.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { models, readOneFolderRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { response } from "../../../../../utilities/response.js" -import { selectOne } from "../../../../../utilities/sql/selectOne.js" - -export const readOneFolderRoute = apiFactory.createApp().post(readOneFolderRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: readOneFolderRouteDefinition.schemas.body, - }) - - const readOneFolder = await selectOne({ - database: c.var.clients.sql, - table: models.folder, - where: (table) => - and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear), eq(table.id, body.idFolder)), - }) - - return response({ - context: c, - statusCode: 200, - schema: readOneFolderRouteDefinition.schemas.return, - data: readOneFolder, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/folders/$idFolder/updateOneFolder.ts b/packages/api/src/routes/auth/$idYear/folders/$idFolder/updateOneFolder.ts deleted file mode 100644 index fb4a006f..00000000 --- a/packages/api/src/routes/auth/$idYear/folders/$idFolder/updateOneFolder.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { models, updateOneFolderRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { response } from "../../../../../utilities/response.js" -import { updateOne } from "../../../../../utilities/sql/updateOne.js" - -export const updateOneFolderRoute = apiFactory.createApp().post(updateOneFolderRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: updateOneFolderRouteDefinition.schemas.body, - }) - - const updateOneFolder = await updateOne({ - database: c.var.clients.sql, - table: models.folder, - data: { - name: body.name, - idFolderParent: body.idFolderParent, - lastUpdatedAt: new Date().toISOString(), - lastUpdatedBy: user.id, - }, - where: (table) => - and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear), eq(table.id, body.idFolder)), - }) - - return response({ - context: c, - statusCode: 200, - schema: updateOneFolderRouteDefinition.schemas.return, - data: updateOneFolder, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/folders/createOneFolder.ts b/packages/api/src/routes/auth/$idYear/folders/createOneFolder.ts deleted file mode 100644 index fc924ac6..00000000 --- a/packages/api/src/routes/auth/$idYear/folders/createOneFolder.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { createOneFolderRouteDefinition, generateId, models } from "@arrhes/application-metadata" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { response } from "../../../../utilities/response.js" -import { insertOne } from "../../../../utilities/sql/insertOne.js" - -export const createOneFolderRoute = apiFactory.createApp().post(createOneFolderRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: createOneFolderRouteDefinition.schemas.body, - }) - - const createOneFolder = await insertOne({ - database: c.var.clients.sql, - table: models.folder, - data: { - id: generateId(), - idOrganization: idOrganization, - idYear: body.idYear, - idFolderParent: body.idFolderParent ?? null, - name: body.name, - createdAt: new Date().toISOString(), - lastUpdatedAt: null, - createdBy: user.id, - lastUpdatedBy: null, - }, - }) - - return response({ - context: c, - statusCode: 200, - schema: createOneFolderRouteDefinition.schemas.return, - data: createOneFolder, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/folders/readAllFolders.ts b/packages/api/src/routes/auth/$idYear/folders/readAllFolders.ts deleted file mode 100644 index a2d6c1c7..00000000 --- a/packages/api/src/routes/auth/$idYear/folders/readAllFolders.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { models, readAllFoldersRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { response } from "../../../../utilities/response.js" -import { selectMany } from "../../../../utilities/sql/selectMany.js" - -export const readAllFoldersRoute = apiFactory.createApp().post(readAllFoldersRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: readAllFoldersRouteDefinition.schemas.body, - }) - - const readAllFolders = await selectMany({ - database: c.var.clients.sql, - table: models.folder, - where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear)), - }) - - return response({ - context: c, - statusCode: 200, - schema: readAllFoldersRouteDefinition.schemas.return, - data: readAllFolders, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/readOneYear.ts b/packages/api/src/routes/auth/$idYear/readOneYear.ts deleted file mode 100644 index b6d887fe..00000000 --- a/packages/api/src/routes/auth/$idYear/readOneYear.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { models, readOneYearRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../utilities/apiFactory.js" -import { response } from "../../../utilities/response.js" -import { selectOne } from "../../../utilities/sql/selectOne.js" - -export const readOneYearRoute = apiFactory.createApp().post(readOneYearRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: readOneYearRouteDefinition.schemas.body, - }) - - const readOneYear = await selectOne({ - database: c.var.clients.sql, - table: models.year, - where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.id, body.idYear)), - }) - - return response({ - context: c, - statusCode: 200, - schema: readOneYearRouteDefinition.schemas.return, - data: readOneYear, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/reports/generateBalanceSheetXml.ts b/packages/api/src/routes/auth/$idYear/reports/generateBalanceSheetXml.ts deleted file mode 100644 index fe2f2df9..00000000 --- a/packages/api/src/routes/auth/$idYear/reports/generateBalanceSheetXml.ts +++ /dev/null @@ -1,329 +0,0 @@ -import { generateBalanceSheetXmlRouteDefinition, models } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { response } from "../../../../utilities/response.js" -import { selectMany } from "../../../../utilities/sql/selectMany.js" -import { selectOne } from "../../../../utilities/sql/selectOne.js" -import { generateGetSignedUrl } from "../../../../utilities/storage/generateGetSignedUrl.js" -import { putObject } from "../../../../utilities/storage/putObject.js" - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -function getAllDescendants( - root: { - id: string - }, - all: Array<{ - id: string - idBalanceSheetParent: string | null - }>, -): Array<{ - id: string - idBalanceSheetParent: string | null -}> { - const direct = all.filter((bs) => bs.idBalanceSheetParent === root.id) - return direct.flatMap((child) => [ - child, - ...getAllDescendants(child, all), - ]) -} - -type Account = (typeof models.account)["$inferSelect"] -type EntryLine = (typeof models.entryLine)["$inferSelect"] -type BalanceSheet = (typeof models.balanceSheet)["$inferSelect"] - -function computeAssetRow( - row: BalanceSheet, - all: BalanceSheet[], - accounts: Account[], - entryLines: EntryLine[], -): { - gross: number - amortization: number - net: number -} { - const descendants = getAllDescendants(row, all) - let gross = 0 - let amortization = 0 - - accounts - .filter((a) => a.idBalanceSheetAsset === row.id || descendants.some((d) => d.id === a.idBalanceSheetAsset)) - .forEach((account) => { - let debit = 0 - let credit = 0 - entryLines - .filter((el) => el.idAccount === account.id) - .forEach((el) => { - debit += Number(el.debit) - credit += Number(el.credit) - }) - const balance = debit - credit - if (balance < 0 && account.balanceSheetAssetFlow === "debit") return - if (balance > 0 && account.balanceSheetAssetFlow === "credit") return - const signed = account.balanceSheetAssetFlow === "debit" ? Math.abs(balance) : -Math.abs(balance) - if (account.balanceSheetAssetColumn === "gross") gross += signed - if (account.balanceSheetAssetColumn === "amortization") amortization += signed - }) - - return { - gross, - amortization, - net: gross + amortization, - } -} - -function computeLiabilityRow( - row: BalanceSheet, - all: BalanceSheet[], - accounts: Account[], - entryLines: EntryLine[], -): number { - const descendants = getAllDescendants(row, all) - let net = 0 - - accounts - .filter( - (a) => a.idBalanceSheetLiability === row.id || descendants.some((d) => d.id === a.idBalanceSheetLiability), - ) - .forEach((account) => { - let debit = 0 - let credit = 0 - entryLines - .filter((el) => el.idAccount === account.id) - .forEach((el) => { - debit += Number(el.debit) - credit += Number(el.credit) - }) - const balance = credit - debit - if (balance > 0 && account.balanceSheetLiabilityFlow === "debit") return - if (balance < 0 && account.balanceSheetLiabilityFlow === "credit") return - net += balance - }) - - return net -} - -/** Sanitize a string into a valid XML local name segment (no spaces, no special chars) */ -function toXmlName(s: string): string { - return s - .normalize("NFD") - .replace(/[\u0300-\u036f]/g, "") - .replace(/[^a-zA-Z0-9]/g, "_") - .replace(/^_+/, "") - .replace(/_+/g, "_") -} - -function fmt(n: number): string { - return n.toFixed(2) -} - -// --------------------------------------------------------------------------- -// XBRL builder -// --------------------------------------------------------------------------- - -function buildBalanceSheetXbrl(params: { - organizationName: string - siren: string | null - startingAt: string - endingAt: string - accounts: Account[] - entryLines: EntryLine[] - balanceSheets: BalanceSheet[] -}): string { - const { organizationName, siren, startingAt, endingAt, accounts, entryLines, balanceSheets } = params - - const entityId = siren ?? organizationName - const entityScheme = siren ? "http://www.insee.fr/siren" : "http://www.arrhes.fr/organization" - - const endDate = endingAt.slice(0, 10) - const startDate = startingAt.slice(0, 10) - const contextId = `D-${startDate}_${endDate}` - - const facts: string[] = [] - - // Assets - const assetRows = balanceSheets.filter((bs) => bs.side === "asset") - let totalGross = 0 - let totalAmort = 0 - let totalNet = 0 - - for (const row of assetRows) { - const { gross, amortization, net } = computeAssetRow(row, balanceSheets, accounts, entryLines) - const key = `Actif${row.number}_${toXmlName(row.label)}` - facts.push(` `) - facts.push( - ` ${fmt(gross)}`, - ) - facts.push( - ` ${fmt(Math.abs(amortization))}`, - ) - facts.push( - ` ${fmt(net)}`, - ) - if (row.idBalanceSheetParent === null) { - totalGross += gross - totalAmort += amortization - totalNet += net - } - } - - facts.push(` `) - facts.push( - ` ${fmt(totalGross)}`, - ) - facts.push( - ` ${fmt(Math.abs(totalAmort))}`, - ) - facts.push( - ` ${fmt(totalNet)}`, - ) - - // Liabilities - const liabilityRows = balanceSheets.filter((bs) => bs.side === "liability") - let totalLiabilityNet = 0 - - for (const row of liabilityRows) { - const net = computeLiabilityRow(row, balanceSheets, accounts, entryLines) - const key = `Passif${row.number}_${toXmlName(row.label)}` - facts.push(` `) - facts.push( - ` ${fmt(net)}`, - ) - if (row.idBalanceSheetParent === null) totalLiabilityNet += net - } - - facts.push(` `) - facts.push( - ` ${fmt(totalLiabilityNet)}`, - ) - - return ` - - - - - - - - - ${entityId} - - - ${endDate} - - - - - iso4217:EUR - - -${facts.join("\n")} - - -` -} - -// --------------------------------------------------------------------------- -// Route -// --------------------------------------------------------------------------- - -export const generateBalanceSheetXmlRoute = apiFactory - .createApp() - .post(generateBalanceSheetXmlRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: generateBalanceSheetXmlRouteDefinition.schemas.body, - }) - - const [year, organization, entryLines, accounts, balanceSheets] = await Promise.all([ - selectOne({ - database: c.var.clients.sql, - table: models.year, - where: (t) => and(eq(t.idOrganization, idOrganization), eq(t.id, body.idYear)), - }), - selectOne({ - database: c.var.clients.sql, - table: models.organization, - where: (t) => eq(t.id, idOrganization), - }), - selectMany({ - database: c.var.clients.sql, - table: models.entryLine, - where: (t) => - and( - eq(t.idOrganization, idOrganization), - eq(t.idYear, body.idYear), - eq(t.isComputedForBalanceSheetReport, true), - ), - }), - selectMany({ - database: c.var.clients.sql, - table: models.account, - where: (t) => and(eq(t.idOrganization, idOrganization), eq(t.idYear, body.idYear)), - }), - selectMany({ - database: c.var.clients.sql, - table: models.balanceSheet, - where: (t) => and(eq(t.idOrganization, idOrganization), eq(t.idYear, body.idYear)), - }), - ]) - - const xml = buildBalanceSheetXbrl({ - organizationName: organization.name, - siren: organization.siren, - startingAt: year.startingAt, - endingAt: year.endingAt, - accounts, - entryLines, - balanceSheets, - }) - - const xmlBuffer = Buffer.from(xml, "utf-8") - const storageKey = `organizations/${idOrganization}/${body.idYear}/reports/balance-sheet.xml` - - await putObject({ - var: c.var, - body: xmlBuffer, - storageKey, - contentType: "application/xml", - contentLength: xmlBuffer.length, - metadata: { - idOrganization, - idYear: body.idYear, - idUser: user.id, - }, - }) - - const url = await generateGetSignedUrl({ - var: c.var, - storageKey, - }) - - return response({ - context: c, - statusCode: 200, - schema: generateBalanceSheetXmlRouteDefinition.schemas.return, - data: { - url, - }, - }) - }) diff --git a/packages/api/src/routes/auth/$idYear/reports/generateIncomeStatementXml.ts b/packages/api/src/routes/auth/$idYear/reports/generateIncomeStatementXml.ts deleted file mode 100644 index dbc7ccb5..00000000 --- a/packages/api/src/routes/auth/$idYear/reports/generateIncomeStatementXml.ts +++ /dev/null @@ -1,287 +0,0 @@ -import { generateIncomeStatementXmlRouteDefinition, models } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { response } from "../../../../utilities/response.js" -import { selectMany } from "../../../../utilities/sql/selectMany.js" -import { selectOne } from "../../../../utilities/sql/selectOne.js" -import { generateGetSignedUrl } from "../../../../utilities/storage/generateGetSignedUrl.js" -import { putObject } from "../../../../utilities/storage/putObject.js" - -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - -type Account = (typeof models.account)["$inferSelect"] -type EntryLine = (typeof models.entryLine)["$inferSelect"] -type IncomeStatement = (typeof models.incomeStatement)["$inferSelect"] -type Computation = (typeof models.computation)["$inferSelect"] -type ComputationIncomeStatement = (typeof models.computationIncomeStatement)["$inferSelect"] - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -/** Sanitize a string into a valid XML local name segment */ -function toXmlName(s: string): string { - return s - .normalize("NFD") - .replace(/[\u0300-\u036f]/g, "") - .replace(/[^a-zA-Z0-9]/g, "_") - .replace(/^_+/, "") - .replace(/_+/g, "_") -} - -function fmt(n: number): string { - return n.toFixed(2) -} - -function computeIncomeStatementAmounts( - incomeStatements: IncomeStatement[], - accounts: Account[], - entryLines: EntryLine[], -): Map { - const amounts = new Map() - for (const row of incomeStatements) { - let amount = 0 - accounts - .filter((a) => a.idIncomeStatement === row.id) - .forEach((account) => { - let debit = 0 - let credit = 0 - entryLines - .filter((el) => el.idAccount === account.id) - .forEach((el) => { - debit += Number(el.debit) - credit += Number(el.credit) - }) - amount += Math.abs(debit - credit) - }) - amounts.set(row.id, amount) - } - return amounts -} - -function computeComputationAmounts( - computations: Computation[], - computationIncomeStatements: ComputationIncomeStatement[], - incomeStatementAmounts: Map, -): Map { - const amounts = new Map() - for (const computation of computations) { - let amount = 0 - computationIncomeStatements - .filter((cis) => cis.idComputation === computation.id) - .forEach((cis) => { - const lineAmount = incomeStatementAmounts.get(cis.idIncomeStatement) ?? 0 - amount += cis.operation === "plus" ? Math.abs(lineAmount) : -Math.abs(lineAmount) - }) - amounts.set(computation.id, amount) - } - return amounts -} - -// --------------------------------------------------------------------------- -// XBRL builder -// --------------------------------------------------------------------------- - -function buildIncomeStatementXbrl(params: { - organizationName: string - siren: string | null - startingAt: string - endingAt: string - incomeStatements: IncomeStatement[] - computations: Computation[] - computationIncomeStatements: ComputationIncomeStatement[] - accounts: Account[] - entryLines: EntryLine[] -}): string { - const { - organizationName, - siren, - startingAt, - endingAt, - incomeStatements, - computations, - computationIncomeStatements, - accounts, - entryLines, - } = params - - const entityId = siren ?? organizationName - const entityScheme = siren ? "http://www.insee.fr/siren" : "http://www.arrhes.fr/organization" - - const startDate = startingAt.slice(0, 10) - const endDate = endingAt.slice(0, 10) - const contextId = `D-${startDate}_${endDate}` - - const incomeStatementAmounts = computeIncomeStatementAmounts(incomeStatements, accounts, entryLines) - const computationAmounts = computeComputationAmounts( - computations, - computationIncomeStatements, - incomeStatementAmounts, - ) - - const facts: string[] = [] - - // Income statement regular lines - for (const row of incomeStatements) { - const amount = incomeStatementAmounts.get(row.id) ?? 0 - const key = `CR${row.number}_${toXmlName(row.label)}` - facts.push(` `) - facts.push( - ` ${fmt(amount)}`, - ) - } - - // Computation rows (soldes intermédiaires de gestion) - for (const computation of computations) { - const amount = computationAmounts.get(computation.id) ?? 0 - const key = `Solde${computation.number}_${toXmlName(computation.label)}` - facts.push(` `) - facts.push( - ` ${fmt(amount)}`, - ) - } - - return ` - - - - - - - - - ${entityId} - - - ${startDate} - ${endDate} - - - - - iso4217:EUR - - -${facts.join("\n")} - - -` -} - -// --------------------------------------------------------------------------- -// Route -// --------------------------------------------------------------------------- - -export const generateIncomeStatementXmlRoute = apiFactory - .createApp() - .post(generateIncomeStatementXmlRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: generateIncomeStatementXmlRouteDefinition.schemas.body, - }) - - const [year, organization, entryLines, accounts, incomeStatements, computations, computationIncomeStatements] = - await Promise.all([ - selectOne({ - database: c.var.clients.sql, - table: models.year, - where: (t) => and(eq(t.idOrganization, idOrganization), eq(t.id, body.idYear)), - }), - selectOne({ - database: c.var.clients.sql, - table: models.organization, - where: (t) => eq(t.id, idOrganization), - }), - selectMany({ - database: c.var.clients.sql, - table: models.entryLine, - where: (t) => - and( - eq(t.idOrganization, idOrganization), - eq(t.idYear, body.idYear), - eq(t.isComputedForIncomeStatementReport, true), - ), - }), - selectMany({ - database: c.var.clients.sql, - table: models.account, - where: (t) => and(eq(t.idOrganization, idOrganization), eq(t.idYear, body.idYear)), - }), - selectMany({ - database: c.var.clients.sql, - table: models.incomeStatement, - where: (t) => and(eq(t.idOrganization, idOrganization), eq(t.idYear, body.idYear)), - }), - selectMany({ - database: c.var.clients.sql, - table: models.computation, - where: (t) => and(eq(t.idOrganization, idOrganization), eq(t.idYear, body.idYear)), - }), - selectMany({ - database: c.var.clients.sql, - table: models.computationIncomeStatement, - where: (t) => and(eq(t.idOrganization, idOrganization), eq(t.idYear, body.idYear)), - }), - ]) - - const xml = buildIncomeStatementXbrl({ - organizationName: organization.name, - siren: organization.siren, - startingAt: year.startingAt, - endingAt: year.endingAt, - incomeStatements, - computations, - computationIncomeStatements, - accounts, - entryLines, - }) - - const xmlBuffer = Buffer.from(xml, "utf-8") - const storageKey = `organizations/${idOrganization}/${body.idYear}/reports/income-statement.xml` - - await putObject({ - var: c.var, - body: xmlBuffer, - storageKey, - contentType: "application/xml", - contentLength: xmlBuffer.length, - metadata: { - idOrganization, - idYear: body.idYear, - idUser: user.id, - }, - }) - - const url = await generateGetSignedUrl({ - var: c.var, - storageKey, - }) - - return response({ - context: c, - statusCode: 200, - schema: generateIncomeStatementXmlRouteDefinition.schemas.return, - data: { - url, - }, - }) - }) diff --git a/packages/api/src/routes/auth/$idYear/reports/reportsRoutes.ts b/packages/api/src/routes/auth/$idYear/reports/reportsRoutes.ts deleted file mode 100644 index 37f0d71f..00000000 --- a/packages/api/src/routes/auth/$idYear/reports/reportsRoutes.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { generateBalanceSheetXmlRoute } from "./generateBalanceSheetXml.js" -import { generateIncomeStatementXmlRoute } from "./generateIncomeStatementXml.js" - -export const reportsRoutes = [ - generateBalanceSheetXmlRoute, - generateIncomeStatementXmlRoute, -] diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/accounts/$idAccount/deleteOneAccount.ts b/packages/api/src/routes/auth/$idYear/yearSettings/accounts/$idAccount/deleteOneAccount.ts deleted file mode 100644 index a07d6986..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/accounts/$idAccount/deleteOneAccount.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { deleteOneAccountRouteDefinition, models } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../../utilities/apiFactory.js" -import { response } from "../../../../../../utilities/response.js" -import { deleteOne } from "../../../../../../utilities/sql/deleteOne.js" - -export const deleteOneAccountRoute = apiFactory.createApp().post(deleteOneAccountRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: deleteOneAccountRouteDefinition.schemas.body, - }) - - const deleteOneAccount = await deleteOne({ - database: c.var.clients.sql, - table: models.account, - where: (table) => - and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear), eq(table.id, body.idAccount)), - }) - - return response({ - context: c, - statusCode: 200, - schema: deleteOneAccountRouteDefinition.schemas.return, - data: deleteOneAccount, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/accounts/$idAccount/readOneAccount.ts b/packages/api/src/routes/auth/$idYear/yearSettings/accounts/$idAccount/readOneAccount.ts deleted file mode 100644 index 89bea6c6..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/accounts/$idAccount/readOneAccount.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { models, readOneAccountRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../../utilities/apiFactory.js" -import { response } from "../../../../../../utilities/response.js" -import { selectOne } from "../../../../../../utilities/sql/selectOne.js" - -export const readOneAccountRoute = apiFactory.createApp().post(readOneAccountRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: readOneAccountRouteDefinition.schemas.body, - }) - - const readOneAccount = await selectOne({ - database: c.var.clients.sql, - table: models.account, - where: (table) => - and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear), eq(table.id, body.idAccount)), - }) - - return response({ - context: c, - statusCode: 200, - schema: readOneAccountRouteDefinition.schemas.return, - data: readOneAccount, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/accounts/$idAccount/updateOneAccount.ts b/packages/api/src/routes/auth/$idYear/yearSettings/accounts/$idAccount/updateOneAccount.ts deleted file mode 100644 index 569b807a..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/accounts/$idAccount/updateOneAccount.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { models, updateOneAccountRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../../utilities/apiFactory.js" -import { response } from "../../../../../../utilities/response.js" -import { updateOne } from "../../../../../../utilities/sql/updateOne.js" - -export const updateOneAccountRoute = apiFactory.createApp().post(updateOneAccountRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: updateOneAccountRouteDefinition.schemas.body, - }) - - const updateOneAccount = await updateOne({ - database: c.var.clients.sql, - table: models.account, - data: { - idAccountParent: body.idAccountParent, - - idBalanceSheetAsset: body.idBalanceSheetAsset, - balanceSheetAssetColumn: body.balanceSheetAssetColumn, - balanceSheetAssetFlow: body.balanceSheetAssetFlow, - - idBalanceSheetLiability: body.idBalanceSheetLiability, - balanceSheetLiabilityColumn: body.balanceSheetLiabilityColumn, - balanceSheetLiabilityFlow: body.balanceSheetLiabilityFlow, - - idIncomeStatement: body.idIncomeStatement, - - isSelectable: body.isSelectable, - number: body.number, - label: body.label, - type: body.type, - lastUpdatedAt: new Date().toISOString(), - lastUpdatedBy: user.id, - }, - where: (table) => - and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear), eq(table.id, body.idAccount)), - }) - - return response({ - context: c, - statusCode: 200, - schema: updateOneAccountRouteDefinition.schemas.return, - data: updateOneAccount, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/accounts/createOneAccount.ts b/packages/api/src/routes/auth/$idYear/yearSettings/accounts/createOneAccount.ts deleted file mode 100644 index 821c7feb..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/accounts/createOneAccount.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { createOneAccountRouteDefinition, generateId, models } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { response } from "../../../../../utilities/response.js" -import { insertOne } from "../../../../../utilities/sql/insertOne.js" -import { selectOne } from "../../../../../utilities/sql/selectOne.js" - -export const createOneAccountRoute = apiFactory.createApp().post(createOneAccountRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: createOneAccountRouteDefinition.schemas.body, - }) - - const readOneAccount = await selectOne({ - database: c.var.clients.sql, - table: models.account, - where: (table) => { - if (body.idAccountParent === null) { - return - } - return and( - eq(table.idOrganization, idOrganization), - eq(table.idYear, body.idYear), - eq(table.id, body.idAccountParent), - ) - }, - }) - - const createOneAccount = await insertOne({ - database: c.var.clients.sql, - table: models.account, - data: { - id: generateId(), - idOrganization: idOrganization, - idYear: body.idYear, - idAccountParent: body.idAccountParent, - - idBalanceSheetAsset: body.idBalanceSheetAsset ?? readOneAccount?.idBalanceSheetAsset, - balanceSheetAssetColumn: body.balanceSheetAssetColumn ?? readOneAccount?.balanceSheetAssetColumn, - balanceSheetAssetFlow: body.balanceSheetAssetFlow ?? readOneAccount?.balanceSheetAssetFlow, - - idBalanceSheetLiability: body.idBalanceSheetLiability ?? readOneAccount?.idBalanceSheetLiability, - balanceSheetLiabilityColumn: - body.balanceSheetLiabilityColumn ?? readOneAccount?.balanceSheetLiabilityColumn, - balanceSheetLiabilityFlow: body.balanceSheetLiabilityFlow ?? readOneAccount?.balanceSheetLiabilityFlow, - - idIncomeStatement: body.idIncomeStatement, - - isSelectable: body.isSelectable, - isDefault: false, - label: body.label, - number: body.number, - type: body.type, - isOptional: false, - createdAt: new Date().toISOString(), - lastUpdatedAt: null, - createdBy: user.id, - lastUpdatedBy: null, - }, - }) - - return response({ - context: c, - statusCode: 200, - schema: createOneAccountRouteDefinition.schemas.return, - data: createOneAccount, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/accounts/readAllAccounts.ts b/packages/api/src/routes/auth/$idYear/yearSettings/accounts/readAllAccounts.ts deleted file mode 100644 index 338930a8..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/accounts/readAllAccounts.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { models, readAllAccountsRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { response } from "../../../../../utilities/response.js" -import { selectMany } from "../../../../../utilities/sql/selectMany.js" - -export const readAllAccountsRoute = apiFactory.createApp().post(readAllAccountsRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: readAllAccountsRouteDefinition.schemas.body, - }) - - const readAllAccounts = await selectMany({ - database: c.var.clients.sql, - table: models.account, - where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear)), - }) - - return response({ - context: c, - statusCode: 200, - schema: readAllAccountsRouteDefinition.schemas.return, - data: readAllAccounts, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/balanceSheets/$idBalanceSheet/deleteOneBalanceSheet.ts b/packages/api/src/routes/auth/$idYear/yearSettings/balanceSheets/$idBalanceSheet/deleteOneBalanceSheet.ts deleted file mode 100644 index bdaa971a..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/balanceSheets/$idBalanceSheet/deleteOneBalanceSheet.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { deleteOneBalanceSheetRouteDefinition, models } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../../utilities/apiFactory.js" -import { response } from "../../../../../../utilities/response.js" -import { deleteOne } from "../../../../../../utilities/sql/deleteOne.js" - -export const deleteOneBalanceSheetRoute = apiFactory - .createApp() - .post(deleteOneBalanceSheetRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: deleteOneBalanceSheetRouteDefinition.schemas.body, - }) - - const deleteOneBalanceSheet = await deleteOne({ - database: c.var.clients.sql, - table: models.balanceSheet, - where: (table) => - and( - eq(table.idOrganization, idOrganization), - eq(table.idYear, body.idYear), - eq(table.id, body.idBalanceSheet), - ), - }) - - return response({ - context: c, - statusCode: 200, - schema: deleteOneBalanceSheetRouteDefinition.schemas.return, - data: deleteOneBalanceSheet, - }) - }) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/balanceSheets/$idBalanceSheet/readOneBalanceSheet.ts b/packages/api/src/routes/auth/$idYear/yearSettings/balanceSheets/$idBalanceSheet/readOneBalanceSheet.ts deleted file mode 100644 index abfcb38b..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/balanceSheets/$idBalanceSheet/readOneBalanceSheet.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { models, readOneBalanceSheetRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../../utilities/apiFactory.js" -import { response } from "../../../../../../utilities/response.js" -import { selectOne } from "../../../../../../utilities/sql/selectOne.js" - -export const readOneBalanceSheetRoute = apiFactory - .createApp() - .post(readOneBalanceSheetRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: readOneBalanceSheetRouteDefinition.schemas.body, - }) - - const readOneBalanceSheet = await selectOne({ - database: c.var.clients.sql, - table: models.balanceSheet, - where: (table) => - and( - eq(table.idOrganization, idOrganization), - eq(table.idYear, body.idYear), - eq(table.id, body.idBalanceSheet), - ), - }) - - return response({ - context: c, - statusCode: 200, - schema: readOneBalanceSheetRouteDefinition.schemas.return, - data: readOneBalanceSheet, - }) - }) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/balanceSheets/$idBalanceSheet/updateOneBalanceSheet.ts b/packages/api/src/routes/auth/$idYear/yearSettings/balanceSheets/$idBalanceSheet/updateOneBalanceSheet.ts deleted file mode 100644 index 346c0bc4..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/balanceSheets/$idBalanceSheet/updateOneBalanceSheet.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { models, updateOneBalanceSheetRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../../utilities/apiFactory.js" -import { response } from "../../../../../../utilities/response.js" -import { updateOne } from "../../../../../../utilities/sql/updateOne.js" - -export const updateOneBalanceSheetRoute = apiFactory - .createApp() - .post(updateOneBalanceSheetRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: updateOneBalanceSheetRouteDefinition.schemas.body, - }) - - const updatedBalanceSheet = await updateOne({ - database: c.var.clients.sql, - table: models.balanceSheet, - data: { - idBalanceSheetParent: body.idBalanceSheetParent, - isComputed: body.isComputed, - number: body.number, - label: body.label, - lastUpdatedAt: new Date().toISOString(), - lastUpdatedBy: user.id, - }, - where: (table) => - and( - eq(table.idOrganization, idOrganization), - eq(table.idYear, body.idYear), - eq(table.id, body.idBalanceSheet), - ), - }) - - return response({ - context: c, - statusCode: 200, - schema: updateOneBalanceSheetRouteDefinition.schemas.return, - data: updatedBalanceSheet, - }) - }) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/balanceSheets/createOneBalanceSheet.ts b/packages/api/src/routes/auth/$idYear/yearSettings/balanceSheets/createOneBalanceSheet.ts deleted file mode 100644 index ef60a50b..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/balanceSheets/createOneBalanceSheet.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { createOneBalanceSheetRouteDefinition, generateId, models } from "@arrhes/application-metadata" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { response } from "../../../../../utilities/response.js" -import { insertOne } from "../../../../../utilities/sql/insertOne.js" - -export const createOneBalanceSheetRoute = apiFactory - .createApp() - .post(createOneBalanceSheetRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: createOneBalanceSheetRouteDefinition.schemas.body, - }) - - const createOneBalanceSheet = await insertOne({ - database: c.var.clients.sql, - table: models.balanceSheet, - data: { - id: generateId(), - idOrganization: idOrganization, - idYear: body.idYear, - idBalanceSheetParent: body.idBalanceSheetParent, - isDefault: false, - isComputed: body.isComputed, - side: body.side, - number: body.number, - label: body.label, - createdAt: new Date().toISOString(), - lastUpdatedAt: null, - createdBy: user.id, - lastUpdatedBy: null, - }, - }) - - return response({ - context: c, - statusCode: 200, - schema: createOneBalanceSheetRouteDefinition.schemas.return, - data: createOneBalanceSheet, - }) - }) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/balanceSheets/readAllBalanceSheets.ts b/packages/api/src/routes/auth/$idYear/yearSettings/balanceSheets/readAllBalanceSheets.ts deleted file mode 100644 index 4728efab..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/balanceSheets/readAllBalanceSheets.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { models, readAllBalanceSheetsRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { response } from "../../../../../utilities/response.js" -import { selectMany } from "../../../../../utilities/sql/selectMany.js" - -export const readAllBalanceSheetsRoute = apiFactory - .createApp() - .post(readAllBalanceSheetsRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: readAllBalanceSheetsRouteDefinition.schemas.body, - }) - - const readAllBalanceSheets = await selectMany({ - database: c.var.clients.sql, - table: models.balanceSheet, - where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear)), - }) - - return response({ - context: c, - statusCode: 200, - schema: readAllBalanceSheetsRouteDefinition.schemas.return, - data: readAllBalanceSheets, - }) - }) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/deleteOneComputationIncomeStatement.ts b/packages/api/src/routes/auth/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/deleteOneComputationIncomeStatement.ts deleted file mode 100644 index fabff1e1..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/deleteOneComputationIncomeStatement.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { deleteOneComputationIncomeStatementRouteDefinition, models } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../../../../utilities/apiFactory.js" -import { response } from "../../../../../../../../utilities/response.js" -import { deleteOne } from "../../../../../../../../utilities/sql/deleteOne.js" - -export const deleteOneComputationIncomeStatementRoute = apiFactory - .createApp() - .post(deleteOneComputationIncomeStatementRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: deleteOneComputationIncomeStatementRouteDefinition.schemas.body, - }) - - const deleteOneComputationIncomeStatement = await deleteOne({ - database: c.var.clients.sql, - table: models.computationIncomeStatement, - where: (table) => - and( - eq(table.idOrganization, idOrganization), - eq(table.idYear, body.idYear), - eq(table.id, body.idComputationIncomeStatement), - ), - }) - - return response({ - context: c, - statusCode: 200, - schema: deleteOneComputationIncomeStatementRouteDefinition.schemas.return, - data: deleteOneComputationIncomeStatement, - }) - }) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/readOneComputationIncomeStatement.ts b/packages/api/src/routes/auth/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/readOneComputationIncomeStatement.ts deleted file mode 100644 index fe5b9df8..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/readOneComputationIncomeStatement.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { models, readOneComputationIncomeStatementRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../../../../utilities/apiFactory.js" -import { response } from "../../../../../../../../utilities/response.js" -import { selectOne } from "../../../../../../../../utilities/sql/selectOne.js" - -export const readOneComputationIncomeStatementRoute = apiFactory - .createApp() - .post(readOneComputationIncomeStatementRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: readOneComputationIncomeStatementRouteDefinition.schemas.body, - }) - - const readOneComputationIncomeStatement = await selectOne({ - database: c.var.clients.sql, - table: models.computationIncomeStatement, - where: (table) => - and( - eq(table.idOrganization, idOrganization), - eq(table.idYear, body.idYear), - eq(table.id, body.idComputationIncomeStatement), - ), - }) - - return response({ - context: c, - statusCode: 200, - schema: readOneComputationIncomeStatementRouteDefinition.schemas.return, - data: readOneComputationIncomeStatement, - }) - }) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/updateOneComputationIncomeStatement.ts b/packages/api/src/routes/auth/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/updateOneComputationIncomeStatement.ts deleted file mode 100644 index b752a645..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/updateOneComputationIncomeStatement.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { models, updateOneComputationIncomeStatementRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../../../../utilities/apiFactory.js" -import { response } from "../../../../../../../../utilities/response.js" -import { updateOne } from "../../../../../../../../utilities/sql/updateOne.js" - -export const updateOneComputationIncomeStatementRoute = apiFactory - .createApp() - .post(updateOneComputationIncomeStatementRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: updateOneComputationIncomeStatementRouteDefinition.schemas.body, - }) - - const updateOneComputationIncomeStatement = await updateOne({ - database: c.var.clients.sql, - table: models.computationIncomeStatement, - data: { - idComputation: body.idComputation, - idIncomeStatement: body.idIncomeStatement, - operation: body.operation, - lastUpdatedAt: new Date().toISOString(), - lastUpdatedBy: user.id, - }, - where: (table) => - and( - eq(table.idOrganization, idOrganization), - eq(table.idYear, body.idYear), - eq(table.id, body.idComputationIncomeStatement), - ), - }) - - return response({ - context: c, - statusCode: 200, - schema: updateOneComputationIncomeStatementRouteDefinition.schemas.return, - data: updateOneComputationIncomeStatement, - }) - }) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/createOneComputationIncomeStatement.ts b/packages/api/src/routes/auth/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/createOneComputationIncomeStatement.ts deleted file mode 100644 index c7dbb652..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/createOneComputationIncomeStatement.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { createOneComputationIncomeStatementRouteDefinition, generateId, models } from "@arrhes/application-metadata" -import { checkUserSessionMiddleware } from "../../../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../../../utilities/apiFactory.js" -import { response } from "../../../../../../../utilities/response.js" -import { insertOne } from "../../../../../../../utilities/sql/insertOne.js" - -export const createOneComputationIncomeStatementRoute = apiFactory - .createApp() - .post(createOneComputationIncomeStatementRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: createOneComputationIncomeStatementRouteDefinition.schemas.body, - }) - - const createOneComputationIncomeStatement = await insertOne({ - database: c.var.clients.sql, - table: models.computationIncomeStatement, - data: { - id: generateId(), - idOrganization: idOrganization, - idYear: body.idYear, - idComputation: body.idComputation, - idIncomeStatement: body.idIncomeStatement, - operation: body.operation, - index: body.index, - createdAt: new Date().toISOString(), - lastUpdatedAt: null, - createdBy: user.id, - lastUpdatedBy: null, - }, - }) - - return response({ - context: c, - statusCode: 200, - schema: createOneComputationIncomeStatementRouteDefinition.schemas.return, - data: createOneComputationIncomeStatement, - }) - }) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/readAllComputationIncomeStatements.ts b/packages/api/src/routes/auth/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/readAllComputationIncomeStatements.ts deleted file mode 100644 index a9f8a29b..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/readAllComputationIncomeStatements.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { models, readAllComputationIncomeStatementsRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../../../utilities/apiFactory.js" -import { response } from "../../../../../../../utilities/response.js" -import { selectMany } from "../../../../../../../utilities/sql/selectMany.js" - -export const readAllComputationIncomeStatementsRoute = apiFactory - .createApp() - .post(readAllComputationIncomeStatementsRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: readAllComputationIncomeStatementsRouteDefinition.schemas.body, - }) - - const readAllComputationIncomeStatements = await selectMany({ - database: c.var.clients.sql, - table: models.computationIncomeStatement, - where: (table) => { - if (body.idComputation !== undefined) { - return and( - eq(table.idOrganization, idOrganization), - eq(table.idYear, body.idYear), - eq(table.idComputation, body.idComputation), - ) - } - if (body.idIncomeStatement !== undefined) { - return and( - eq(table.idOrganization, idOrganization), - eq(table.idYear, body.idYear), - eq(table.idIncomeStatement, body.idIncomeStatement), - ) - } - return and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear)) - }, - }) - - return response({ - context: c, - statusCode: 200, - schema: readAllComputationIncomeStatementsRouteDefinition.schemas.return, - data: readAllComputationIncomeStatements, - }) - }) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/computations/$idComputation/deleteOneComputation.ts b/packages/api/src/routes/auth/$idYear/yearSettings/computations/$idComputation/deleteOneComputation.ts deleted file mode 100644 index 6f4d0632..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/computations/$idComputation/deleteOneComputation.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { deleteOneComputationRouteDefinition, models } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../../utilities/apiFactory.js" -import { response } from "../../../../../../utilities/response.js" -import { deleteOne } from "../../../../../../utilities/sql/deleteOne.js" - -export const deleteOneComputationRoute = apiFactory - .createApp() - .post(deleteOneComputationRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: deleteOneComputationRouteDefinition.schemas.body, - }) - - const deleteOneComputation = await deleteOne({ - database: c.var.clients.sql, - table: models.computation, - where: (table) => - and( - eq(table.idOrganization, idOrganization), - eq(table.idYear, body.idYear), - eq(table.id, body.idComputation), - ), - }) - - return response({ - context: c, - statusCode: 200, - schema: deleteOneComputationRouteDefinition.schemas.return, - data: deleteOneComputation, - }) - }) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/computations/$idComputation/readOneComputation.ts b/packages/api/src/routes/auth/$idYear/yearSettings/computations/$idComputation/readOneComputation.ts deleted file mode 100644 index c5ddff30..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/computations/$idComputation/readOneComputation.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { models, readOneComputationRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../../utilities/apiFactory.js" -import { response } from "../../../../../../utilities/response.js" -import { selectOne } from "../../../../../../utilities/sql/selectOne.js" - -export const readOneComputationRoute = apiFactory - .createApp() - .post(readOneComputationRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: readOneComputationRouteDefinition.schemas.body, - }) - - const readOneComputation = await selectOne({ - database: c.var.clients.sql, - table: models.computation, - where: (table) => - and( - eq(table.idOrganization, idOrganization), - eq(table.idYear, body.idYear), - eq(table.id, body.idComputation), - ), - }) - - return response({ - context: c, - statusCode: 200, - schema: readOneComputationRouteDefinition.schemas.return, - data: readOneComputation, - }) - }) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/computations/$idComputation/updateOneComputation.ts b/packages/api/src/routes/auth/$idYear/yearSettings/computations/$idComputation/updateOneComputation.ts deleted file mode 100644 index cefd42c3..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/computations/$idComputation/updateOneComputation.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { models, updateOneComputationRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../../utilities/apiFactory.js" -import { response } from "../../../../../../utilities/response.js" -import { updateOne } from "../../../../../../utilities/sql/updateOne.js" - -export const updateOneComputationRoute = apiFactory - .createApp() - .post(updateOneComputationRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: updateOneComputationRouteDefinition.schemas.body, - }) - - const updateOneComputation = await updateOne({ - database: c.var.clients.sql, - table: models.computation, - data: { - number: body.number, - label: body.label, - lastUpdatedAt: new Date().toISOString(), - lastUpdatedBy: user.id, - }, - where: (table) => - and( - eq(table.idOrganization, idOrganization), - eq(table.idYear, body.idYear), - eq(table.id, body.idComputation), - ), - }) - - return response({ - context: c, - statusCode: 200, - schema: updateOneComputationRouteDefinition.schemas.return, - data: updateOneComputation, - }) - }) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/computations/createOneComputation.ts b/packages/api/src/routes/auth/$idYear/yearSettings/computations/createOneComputation.ts deleted file mode 100644 index 75449fe5..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/computations/createOneComputation.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { createOneComputationRouteDefinition, generateId, models } from "@arrhes/application-metadata" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { response } from "../../../../../utilities/response.js" -import { insertOne } from "../../../../../utilities/sql/insertOne.js" - -export const createOneComputationRoute = apiFactory - .createApp() - .post(createOneComputationRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: createOneComputationRouteDefinition.schemas.body, - }) - - const createOneComputation = await insertOne({ - database: c.var.clients.sql, - table: models.computation, - data: { - id: generateId(), - idOrganization: idOrganization, - idYear: body.idYear, - index: body.index, - number: body.number, - label: body.label, - createdAt: new Date().toISOString(), - lastUpdatedAt: null, - createdBy: user.id, - lastUpdatedBy: null, - }, - }) - - return response({ - context: c, - statusCode: 200, - schema: createOneComputationRouteDefinition.schemas.return, - data: createOneComputation, - }) - }) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/computations/readAllComputations.ts b/packages/api/src/routes/auth/$idYear/yearSettings/computations/readAllComputations.ts deleted file mode 100644 index 48d4da17..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/computations/readAllComputations.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { models, readAllComputationsRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { response } from "../../../../../utilities/response.js" -import { selectMany } from "../../../../../utilities/sql/selectMany.js" - -export const readAllComputationsRoute = apiFactory - .createApp() - .post(readAllComputationsRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: readAllComputationsRouteDefinition.schemas.body, - }) - - const readAllComputations = await selectMany({ - database: c.var.clients.sql, - table: models.computation, - where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear)), - }) - - return response({ - context: c, - statusCode: 200, - schema: readAllComputationsRouteDefinition.schemas.return, - data: readAllComputations, - }) - }) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/general/closeYear.ts b/packages/api/src/routes/auth/$idYear/yearSettings/general/closeYear.ts deleted file mode 100644 index 9c672654..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/general/closeYear.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { closeYearRouteDefinition, models } from "@arrhes/application-metadata" -import { and, eq, not } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { response } from "../../../../../utilities/response.js" -import { updateOne } from "../../../../../utilities/sql/updateOne.js" - -export const closeYearRoute = apiFactory.createApp().post(closeYearRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: closeYearRouteDefinition.schemas.body, - }) - - const closeYear = await updateOne({ - database: c.var.clients.sql, - table: models.year, - data: { - isClosed: not(models.year.isClosed), - lastUpdatedAt: new Date().toISOString(), - lastUpdatedBy: user.id, - }, - where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.id, body.idYear)), - }) - - return response({ - context: c, - statusCode: 200, - schema: closeYearRouteDefinition.schemas.return, - data: closeYear, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/general/deleteOneYear.ts b/packages/api/src/routes/auth/$idYear/yearSettings/general/deleteOneYear.ts deleted file mode 100644 index a82c0536..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/general/deleteOneYear.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { deleteOneYearRouteDefinition, models } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { response } from "../../../../../utilities/response.js" -import { deleteOne } from "../../../../../utilities/sql/deleteOne.js" - -export const deleteOneYearRoute = apiFactory.createApp().post(deleteOneYearRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: deleteOneYearRouteDefinition.schemas.body, - }) - - const deleteOneYear = await deleteOne({ - database: c.var.clients.sql, - table: models.year, - where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.id, body.idYear)), - }) - - return response({ - context: c, - statusCode: 200, - schema: deleteOneYearRouteDefinition.schemas.return, - data: deleteOneYear, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/general/updateOneYear.ts b/packages/api/src/routes/auth/$idYear/yearSettings/general/updateOneYear.ts deleted file mode 100644 index f2b71762..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/general/updateOneYear.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { models, updateOneYearRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { response } from "../../../../../utilities/response.js" -import { updateOne } from "../../../../../utilities/sql/updateOne.js" - -export const updateOneYearRoute = apiFactory.createApp().post(updateOneYearRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: updateOneYearRouteDefinition.schemas.body, - }) - - const updateOneYear = await updateOne({ - database: c.var.clients.sql, - table: models.year, - data: { - idYearPrevious: body.idYearPrevious, - label: body.label, - startingAt: body.startingAt, - endingAt: body.endingAt, - lastUpdatedAt: new Date().toISOString(), - lastUpdatedBy: user.id, - }, - where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.id, body.idYear)), - }) - - return response({ - context: c, - statusCode: 200, - schema: updateOneYearRouteDefinition.schemas.return, - data: updateOneYear, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/incomeStatements/$idIncomeStatement/deleteOneIncomeStatement.ts b/packages/api/src/routes/auth/$idYear/yearSettings/incomeStatements/$idIncomeStatement/deleteOneIncomeStatement.ts deleted file mode 100644 index 6ec5242a..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/incomeStatements/$idIncomeStatement/deleteOneIncomeStatement.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { deleteOneIncomeStatementRouteDefinition, models } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../../utilities/apiFactory.js" -import { response } from "../../../../../../utilities/response.js" -import { deleteOne } from "../../../../../../utilities/sql/deleteOne.js" - -export const deleteOneIncomeStatementRoute = apiFactory - .createApp() - .post(deleteOneIncomeStatementRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: deleteOneIncomeStatementRouteDefinition.schemas.body, - }) - - const deleteOneIncomeStatement = await deleteOne({ - database: c.var.clients.sql, - table: models.incomeStatement, - where: (table) => - and( - eq(table.idOrganization, idOrganization), - eq(table.idYear, body.idYear), - eq(table.id, body.idIncomeStatement), - ), - }) - - return response({ - context: c, - statusCode: 200, - schema: deleteOneIncomeStatementRouteDefinition.schemas.return, - data: deleteOneIncomeStatement, - }) - }) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/incomeStatements/$idIncomeStatement/readOneIncomeStatement.ts b/packages/api/src/routes/auth/$idYear/yearSettings/incomeStatements/$idIncomeStatement/readOneIncomeStatement.ts deleted file mode 100644 index 94cd428a..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/incomeStatements/$idIncomeStatement/readOneIncomeStatement.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { models, readOneIncomeStatementRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../../utilities/apiFactory.js" -import { response } from "../../../../../../utilities/response.js" -import { selectOne } from "../../../../../../utilities/sql/selectOne.js" - -export const readOneIncomeStatementRoute = apiFactory - .createApp() - .post(readOneIncomeStatementRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: readOneIncomeStatementRouteDefinition.schemas.body, - }) - - const readOneIncomeStatement = await selectOne({ - database: c.var.clients.sql, - table: models.incomeStatement, - where: (table) => - and( - eq(table.idOrganization, idOrganization), - eq(table.idYear, body.idYear), - eq(table.id, body.idIncomeStatement), - ), - }) - - return response({ - context: c, - statusCode: 200, - schema: readOneIncomeStatementRouteDefinition.schemas.return, - data: readOneIncomeStatement, - }) - }) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/incomeStatements/$idIncomeStatement/updateOneIncomeStatement.ts b/packages/api/src/routes/auth/$idYear/yearSettings/incomeStatements/$idIncomeStatement/updateOneIncomeStatement.ts deleted file mode 100644 index 8be0aef2..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/incomeStatements/$idIncomeStatement/updateOneIncomeStatement.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { models, updateOneIncomeStatementRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../../utilities/apiFactory.js" -import { response } from "../../../../../../utilities/response.js" -import { updateOne } from "../../../../../../utilities/sql/updateOne.js" - -export const updateOneIncomeStatementRoute = apiFactory - .createApp() - .post(updateOneIncomeStatementRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: updateOneIncomeStatementRouteDefinition.schemas.body, - }) - - const updatedIncomeStatement = await updateOne({ - database: c.var.clients.sql, - table: models.incomeStatement, - data: { - idIncomeStatementParent: body.idIncomeStatementParent, - isComputed: body.isComputed, - number: body.number, - label: body.label, - lastUpdatedAt: new Date().toISOString(), - lastUpdatedBy: user.id, - }, - where: (table) => - and( - eq(table.idOrganization, idOrganization), - eq(table.idYear, body.idYear), - eq(table.id, body.idIncomeStatement), - ), - }) - - return response({ - context: c, - statusCode: 200, - schema: updateOneIncomeStatementRouteDefinition.schemas.return, - data: updatedIncomeStatement, - }) - }) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/incomeStatements/createOneIncomeStatement.ts b/packages/api/src/routes/auth/$idYear/yearSettings/incomeStatements/createOneIncomeStatement.ts deleted file mode 100644 index c4cba3c2..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/incomeStatements/createOneIncomeStatement.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { createOneIncomeStatementRouteDefinition, generateId, models } from "@arrhes/application-metadata" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { response } from "../../../../../utilities/response.js" -import { insertOne } from "../../../../../utilities/sql/insertOne.js" - -export const createOneIncomeStatementRoute = apiFactory - .createApp() - .post(createOneIncomeStatementRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: createOneIncomeStatementRouteDefinition.schemas.body, - }) - - const createOneIncomeStatement = await insertOne({ - database: c.var.clients.sql, - table: models.incomeStatement, - data: { - id: generateId(), - idOrganization: idOrganization, - idYear: body.idYear, - idIncomeStatementParent: body.idIncomeStatementParent, - isDefault: false, - isComputed: body.isComputed, - number: body.number, - label: body.label, - createdAt: new Date().toISOString(), - lastUpdatedAt: null, - createdBy: user.id, - lastUpdatedBy: null, - }, - }) - - return response({ - context: c, - statusCode: 200, - schema: createOneIncomeStatementRouteDefinition.schemas.return, - data: createOneIncomeStatement, - }) - }) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/incomeStatements/readAllIncomeStatements.ts b/packages/api/src/routes/auth/$idYear/yearSettings/incomeStatements/readAllIncomeStatements.ts deleted file mode 100644 index 5b3bc5bc..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/incomeStatements/readAllIncomeStatements.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { models, readAllIncomeStatementsRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { response } from "../../../../../utilities/response.js" -import { selectMany } from "../../../../../utilities/sql/selectMany.js" - -export const readAllIncomeStatementsRoute = apiFactory - .createApp() - .post(readAllIncomeStatementsRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: readAllIncomeStatementsRouteDefinition.schemas.body, - }) - - const readAllIncomeStatements = await selectMany({ - database: c.var.clients.sql, - table: models.incomeStatement, - where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear)), - }) - - return response({ - context: c, - statusCode: 200, - schema: readAllIncomeStatementsRouteDefinition.schemas.return, - data: readAllIncomeStatements, - }) - }) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/journals/$idJournal/deleteOneJournal.ts b/packages/api/src/routes/auth/$idYear/yearSettings/journals/$idJournal/deleteOneJournal.ts deleted file mode 100644 index 5d358aab..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/journals/$idJournal/deleteOneJournal.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { deleteOneJournalRouteDefinition, models } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../../utilities/apiFactory.js" -import { response } from "../../../../../../utilities/response.js" -import { deleteOne } from "../../../../../../utilities/sql/deleteOne.js" - -export const deleteOneJournalRoute = apiFactory.createApp().post(deleteOneJournalRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: deleteOneJournalRouteDefinition.schemas.body, - }) - - const deleteOneJournal = await deleteOne({ - database: c.var.clients.sql, - table: models.journal, - where: (table) => - and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear), eq(table.id, body.idJournal)), - }) - - return response({ - context: c, - statusCode: 200, - schema: deleteOneJournalRouteDefinition.schemas.return, - data: deleteOneJournal, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/journals/$idJournal/readOneJournal.ts b/packages/api/src/routes/auth/$idYear/yearSettings/journals/$idJournal/readOneJournal.ts deleted file mode 100644 index d3483f49..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/journals/$idJournal/readOneJournal.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { models, readOneJournalRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../../utilities/apiFactory.js" -import { response } from "../../../../../../utilities/response.js" -import { selectOne } from "../../../../../../utilities/sql/selectOne.js" - -export const readOneJournalRoute = apiFactory.createApp().post(readOneJournalRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: readOneJournalRouteDefinition.schemas.body, - }) - - const readOneJournal = await selectOne({ - database: c.var.clients.sql, - table: models.journal, - where: (table) => - and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear), eq(table.id, body.idJournal)), - }) - - return response({ - context: c, - statusCode: 200, - schema: readOneJournalRouteDefinition.schemas.return, - data: readOneJournal, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/journals/$idJournal/updateOneJournal.ts b/packages/api/src/routes/auth/$idYear/yearSettings/journals/$idJournal/updateOneJournal.ts deleted file mode 100644 index a11b5fc3..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/journals/$idJournal/updateOneJournal.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { models, updateOneJournalRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../../utilities/apiFactory.js" -import { response } from "../../../../../../utilities/response.js" -import { updateOne } from "../../../../../../utilities/sql/updateOne.js" - -export const updateOneJournalRoute = apiFactory.createApp().post(updateOneJournalRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: updateOneJournalRouteDefinition.schemas.body, - }) - - const updateOneJournal = await updateOne({ - database: c.var.clients.sql, - table: models.journal, - data: { - code: body.code, - label: body.label, - lastUpdatedAt: new Date().toISOString(), - lastUpdatedBy: user.id, - }, - where: (table) => - and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear), eq(table.id, body.idJournal)), - }) - - return response({ - context: c, - statusCode: 200, - schema: updateOneJournalRouteDefinition.schemas.return, - data: updateOneJournal, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/journals/createOneJournal.ts b/packages/api/src/routes/auth/$idYear/yearSettings/journals/createOneJournal.ts deleted file mode 100644 index 0a75a95d..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/journals/createOneJournal.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { createOneJournalRouteDefinition, generateId, models } from "@arrhes/application-metadata" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { response } from "../../../../../utilities/response.js" -import { insertOne } from "../../../../../utilities/sql/insertOne.js" - -export const createOneJournalRoute = apiFactory.createApp().post(createOneJournalRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: createOneJournalRouteDefinition.schemas.body, - }) - - const createOneJournal = await insertOne({ - database: c.var.clients.sql, - table: models.journal, - data: { - id: generateId(), - idOrganization: idOrganization, - idYear: body.idYear, - code: body.code, - label: body.label, - createdAt: new Date().toISOString(), - lastUpdatedAt: null, - createdBy: user.id, - lastUpdatedBy: null, - }, - }) - - return response({ - context: c, - statusCode: 200, - schema: createOneJournalRouteDefinition.schemas.return, - data: createOneJournal, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/journals/generateJournals.ts b/packages/api/src/routes/auth/$idYear/yearSettings/journals/generateJournals.ts deleted file mode 100644 index 213595a3..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/journals/generateJournals.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* -import { defaultJournals, generateId, generateJournalsRouteDefinition, models } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { Exception } from "../../../../../utilities/exception.js" -import { response } from "../../../../../utilities/response.js" -import { deleteMany } from "../../../../../utilities/sql/deleteMany.js" -import { insertMany } from "../../../../../utilities/sql/insertMany.js" - -export const generateJournalsRoute = apiFactory.createApp().post(generateJournalsRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ context: c }) - const body = await validateBodyMiddleware({ - context: c, - schema: generateJournalsRouteDefinition.schemas.body, - }) - - const generatedJournals = await c.var.clients.sql.transaction(async (tx) => { - try { - const _deletedJournals = await deleteMany({ - database: tx, - table: models.journal, - where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear)), - }) - } catch (_error: unknown) { - throw new Exception({ - internalMessage: "Failed to delete journals", - externalMessage: "Échec de la suppression des journaux", - }) - } - - const newJournals = defaultJournals.map((defaultJournal) => { - return { - id: generateId(), - idOrganization: idOrganization, - idYear: body.idYear, - code: defaultJournal.code, - label: defaultJournal.label, - createdAt: new Date().toISOString(), - lastUpdatedAt: null, - createdBy: null, - lastUpdatedBy: null, - } - }) - - const generatedJournals = await insertMany({ - database: tx, - table: models.journal, - data: newJournals, - }) - - return generatedJournals - }) - - return response({ - context: c, - statusCode: 200, - schema: generateJournalsRouteDefinition.schemas.return, - data: generatedJournals, - }) -}) -*/ diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/journals/readAllJournals.ts b/packages/api/src/routes/auth/$idYear/yearSettings/journals/readAllJournals.ts deleted file mode 100644 index fc6995f0..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/journals/readAllJournals.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { models, readAllJournalsRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { response } from "../../../../../utilities/response.js" -import { selectMany } from "../../../../../utilities/sql/selectMany.js" - -export const readAllJournalsRoute = apiFactory.createApp().post(readAllJournalsRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: readAllJournalsRouteDefinition.schemas.body, - }) - - const readAllJournals = await selectMany({ - database: c.var.clients.sql, - table: models.journal, - where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear)), - }) - - return response({ - context: c, - statusCode: 200, - schema: readAllJournalsRouteDefinition.schemas.return, - data: readAllJournals, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/tags/$idTag/deleteOneTag.ts b/packages/api/src/routes/auth/$idYear/yearSettings/tags/$idTag/deleteOneTag.ts deleted file mode 100644 index 6c93fa68..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/tags/$idTag/deleteOneTag.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { deleteOneTagRouteDefinition, models } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../../utilities/apiFactory.js" -import { response } from "../../../../../../utilities/response.js" -import { deleteOne } from "../../../../../../utilities/sql/deleteOne.js" - -export const deleteOneTagRoute = apiFactory.createApp().post(deleteOneTagRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: deleteOneTagRouteDefinition.schemas.body, - }) - - const deleteOneTag = await deleteOne({ - database: c.var.clients.sql, - table: models.tag, - where: (table) => - and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear), eq(table.id, body.idTag)), - }) - - return response({ - context: c, - statusCode: 200, - schema: deleteOneTagRouteDefinition.schemas.return, - data: deleteOneTag, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/tags/$idTag/readOneTag.ts b/packages/api/src/routes/auth/$idYear/yearSettings/tags/$idTag/readOneTag.ts deleted file mode 100644 index da5032e8..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/tags/$idTag/readOneTag.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { models, readOneTagRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../../utilities/apiFactory.js" -import { response } from "../../../../../../utilities/response.js" -import { selectOne } from "../../../../../../utilities/sql/selectOne.js" - -export const readOneTagRoute = apiFactory.createApp().post(readOneTagRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: readOneTagRouteDefinition.schemas.body, - }) - - const readOneTag = await selectOne({ - database: c.var.clients.sql, - table: models.tag, - where: (table) => - and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear), eq(table.id, body.idTag)), - }) - - return response({ - context: c, - statusCode: 200, - schema: readOneTagRouteDefinition.schemas.return, - data: readOneTag, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/tags/$idTag/updateOneTag.ts b/packages/api/src/routes/auth/$idYear/yearSettings/tags/$idTag/updateOneTag.ts deleted file mode 100644 index 976d2243..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/tags/$idTag/updateOneTag.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { models, updateOneTagRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../../utilities/apiFactory.js" -import { response } from "../../../../../../utilities/response.js" -import { updateOne } from "../../../../../../utilities/sql/updateOne.js" - -export const updateOneTagRoute = apiFactory.createApp().post(updateOneTagRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: updateOneTagRouteDefinition.schemas.body, - }) - - const updateOneTag = await updateOne({ - database: c.var.clients.sql, - table: models.tag, - data: { - label: body.label, - - lastUpdatedAt: new Date().toISOString(), - lastUpdatedBy: user.id, - }, - where: (table) => - and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear), eq(table.id, body.idTag)), - }) - - return response({ - context: c, - statusCode: 200, - schema: updateOneTagRouteDefinition.schemas.return, - data: updateOneTag, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/tags/createOneTag.ts b/packages/api/src/routes/auth/$idYear/yearSettings/tags/createOneTag.ts deleted file mode 100644 index 783a82f6..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/tags/createOneTag.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { createOneTagRouteDefinition, generateId, models } from "@arrhes/application-metadata" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { response } from "../../../../../utilities/response.js" -import { insertOne } from "../../../../../utilities/sql/insertOne.js" - -export const createOneTagRoute = apiFactory.createApp().post(createOneTagRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: createOneTagRouteDefinition.schemas.body, - }) - - const createOneTag = await insertOne({ - database: c.var.clients.sql, - table: models.tag, - data: { - id: generateId(), - idOrganization: idOrganization, - idYear: body.idYear, - - label: body.label, - - createdAt: new Date().toISOString(), - lastUpdatedAt: null, - createdBy: user.id, - lastUpdatedBy: null, - }, - }) - - return response({ - context: c, - statusCode: 200, - schema: createOneTagRouteDefinition.schemas.return, - data: createOneTag, - }) -}) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/tags/readAllTags.ts b/packages/api/src/routes/auth/$idYear/yearSettings/tags/readAllTags.ts deleted file mode 100644 index 3e2d2c63..00000000 --- a/packages/api/src/routes/auth/$idYear/yearSettings/tags/readAllTags.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { models, readAllTagsRouteDefinition } from "@arrhes/application-metadata" -import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { response } from "../../../../../utilities/response.js" -import { selectMany } from "../../../../../utilities/sql/selectMany.js" - -export const readAllTagsRoute = apiFactory.createApp().post(readAllTagsRouteDefinition.path, async (c) => { - const { idOrganization } = await checkUserSessionMiddleware({ - context: c, - }) - const body = await validateBodyMiddleware({ - context: c, - schema: readAllTagsRouteDefinition.schemas.body, - }) - - const readAllTags = await selectMany({ - database: c.var.clients.sql, - table: models.tag, - where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear)), - }) - - return response({ - context: c, - statusCode: 200, - schema: readAllTagsRouteDefinition.schemas.return, - data: readAllTags, - }) -}) diff --git a/packages/api/src/routes/auth/agent/agentFile/createOneAgentFile.ts b/packages/api/src/routes/auth/agent/agentFile/createOneAgentFile.ts index bafa2dd4..7e7bdbf7 100644 --- a/packages/api/src/routes/auth/agent/agentFile/createOneAgentFile.ts +++ b/packages/api/src/routes/auth/agent/agentFile/createOneAgentFile.ts @@ -1,7 +1,8 @@ import { createOneAgentFileRouteDefinition, generateId, models } from "@arrhes/application-metadata" import { and, eq, isNull, sql } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../middlewares/checkAuthMiddleware.js" import { checkOrganizationSubscriptionSessionMiddleware } from "../../../../middlewares/checkOrganizationSubscriptionSessionMiddleware.js" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../middlewares/requireOrganizationMiddleware.js" import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" import { apiFactory } from "../../../../utilities/apiFactory.js" import { Exception } from "../../../../utilities/exception.js" @@ -16,9 +17,12 @@ const MAX_AGENT_FILE_SIZE = 50_000_000 export const createOneAgentFileRoute = apiFactory .createApp() .post(createOneAgentFileRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ + const auth = await checkAuthMiddleware({ context: c, }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) const body = await validateBodyMiddleware({ context: c, schema: createOneAgentFileRouteDefinition.schemas.body, @@ -43,7 +47,7 @@ export const createOneAgentFileRoute = apiFactory where: (table) => eq(table.id, body.idAgentSession), }) - if (session.idUser !== user.id) { + if (session.idUser !== auth.user.id) { throw new Exception({ statusCode: 403, internalMessage: "Agent session access denied", @@ -67,19 +71,11 @@ export const createOneAgentFileRoute = apiFactory }) } - const idYear = session.idYear - - // Check for duplicate file by hash within the same organization and year + // Check for duplicate file by hash within the same organization const existingFiles = await c.var.clients.sql .select() .from(models.file) - .where( - and( - eq(models.file.idOrganization, idOrganization), - eq(models.file.idYear, idYear), - eq(models.file.hash, body.fileHash), - ), - ) + .where(and(eq(models.file.idOrganization, idOrganization), eq(models.file.hash, body.fileHash))) .limit(1) if (existingFiles.length > 0 && existingFiles[0]) { @@ -108,14 +104,13 @@ export const createOneAgentFileRoute = apiFactory }) } - // Find or create the ".agent" folder at the root of the year + // Find or create the ".agent" folder at the root const existingFolders = await c.var.clients.sql .select() .from(models.folder) .where( and( eq(models.folder.idOrganization, idOrganization), - eq(models.folder.idYear, idYear), eq(models.folder.name, ".agent"), isNull(models.folder.idFolderParent), ), @@ -133,18 +128,17 @@ export const createOneAgentFileRoute = apiFactory data: { id: generateId(), idOrganization: idOrganization, - idYear: idYear, idFolderParent: null, name: ".agent", createdAt: new Date().toISOString(), - createdBy: user.id, + createdBy: auth.user.id, }, }) agentFolderId = newFolder.id } const newFileId = generateId() - const storageKey = `organizations/${idOrganization}/${idYear}/files/${newFileId}` + const storageKey = `organizations/${idOrganization}/storage/${newFileId}` const newFile = await insertOne({ database: c.var.clients.sql, @@ -152,7 +146,6 @@ export const createOneAgentFileRoute = apiFactory data: { id: newFileId, idOrganization: idOrganization, - idYear: idYear, idFolder: agentFolderId, reference: null, name: body.fileName, @@ -161,7 +154,7 @@ export const createOneAgentFileRoute = apiFactory size: body.fileSize, hash: body.fileHash, createdAt: new Date().toISOString(), - createdBy: user.id, + createdBy: auth.user.id, }, }) @@ -181,8 +174,7 @@ export const createOneAgentFileRoute = apiFactory contentType: body.fileType, metadata: { idOrganization: idOrganization, - idYear: idYear, - idUser: user.id, + idUser: auth.user.id, }, }) diff --git a/packages/api/src/routes/auth/agent/agentMessage/agentUndo.ts b/packages/api/src/routes/auth/agent/agentMessage/agentUndo.ts index 59c5d994..71a97d3c 100644 --- a/packages/api/src/routes/auth/agent/agentMessage/agentUndo.ts +++ b/packages/api/src/routes/auth/agent/agentMessage/agentUndo.ts @@ -1,14 +1,14 @@ import { models, undoAgentActionRouteDefinition } from "@arrhes/application-metadata" import { eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" +import { checkAuthMiddleware } from "../../../../middlewares/checkAuthMiddleware.js" import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" import { Exception } from "../../../../utilities/exception.js" +import { registerRoute } from "../../../../utilities/registerRoute.js" import { response } from "../../../../utilities/response.js" import { selectOne } from "../../../../utilities/sql/selectOne.js" -export const undoAgentActionRoute = apiFactory.createApp().post(undoAgentActionRouteDefinition.path, async (c) => { - const { user } = await checkUserSessionMiddleware({ +export const undoAgentActionRoute = registerRoute(undoAgentActionRouteDefinition, async (c) => { + const { user } = await checkAuthMiddleware({ context: c, }) const body = await validateBodyMiddleware({ diff --git a/packages/api/src/routes/auth/agent/agentMessage/createOneAgentMessage.ts b/packages/api/src/routes/auth/agent/agentMessage/createOneAgentMessage.ts index 2123eefd..e2d029fd 100644 --- a/packages/api/src/routes/auth/agent/agentMessage/createOneAgentMessage.ts +++ b/packages/api/src/routes/auth/agent/agentMessage/createOneAgentMessage.ts @@ -1,7 +1,7 @@ import { createOneAgentMessageRouteDefinition, generateId, models } from "@arrhes/application-metadata" import { eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../middlewares/checkAuthMiddleware.js" import { checkOrganizationSubscriptionSessionMiddleware } from "../../../../middlewares/checkOrganizationSubscriptionSessionMiddleware.js" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" import { apiFactory } from "../../../../utilities/apiFactory.js" import { Exception } from "../../../../utilities/exception.js" @@ -12,7 +12,7 @@ import { selectOne } from "../../../../utilities/sql/selectOne.js" export const createOneAgentMessageRoute = apiFactory .createApp() .post(createOneAgentMessageRouteDefinition.path, async (c) => { - const { user } = await checkUserSessionMiddleware({ + const { user } = await checkAuthMiddleware({ context: c, }) const body = await validateBodyMiddleware({ diff --git a/packages/api/src/routes/auth/agent/agentMessage/getStreamForAgentMessage.ts b/packages/api/src/routes/auth/agent/agentMessage/getStreamForAgentMessage.ts index dc98a6bc..41fe95ca 100644 --- a/packages/api/src/routes/auth/agent/agentMessage/getStreamForAgentMessage.ts +++ b/packages/api/src/routes/auth/agent/agentMessage/getStreamForAgentMessage.ts @@ -1,7 +1,7 @@ import { getStreamForAgentMessageRouteDefinition, models } from "@arrhes/application-metadata" import { and, eq } from "drizzle-orm" import { streamText } from "hono/streaming" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" +import { checkAuthMiddleware } from "../../../../middlewares/checkAuthMiddleware.js" import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" import { apiFactory } from "../../../../utilities/apiFactory.js" import { Exception } from "../../../../utilities/exception.js" @@ -19,7 +19,7 @@ export const getStreamForAgentMessageRoute = apiFactory await next() }) .post(getStreamForAgentMessageRouteDefinition.path, async (c) => { - await checkUserSessionMiddleware({ + await checkAuthMiddleware({ context: c, }) const body = await validateBodyMiddleware({ diff --git a/packages/api/src/routes/auth/agent/agentMessage/readAllAgentMessages.ts b/packages/api/src/routes/auth/agent/agentMessage/readAllAgentMessages.ts index a479b086..67389ac1 100644 --- a/packages/api/src/routes/auth/agent/agentMessage/readAllAgentMessages.ts +++ b/packages/api/src/routes/auth/agent/agentMessage/readAllAgentMessages.ts @@ -1,6 +1,6 @@ import { models, readAllAgentMessagesRouteDefinition } from "@arrhes/application-metadata" import { and, asc, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" +import { checkAuthMiddleware } from "../../../../middlewares/checkAuthMiddleware.js" import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" import { apiFactory } from "../../../../utilities/apiFactory.js" import { response } from "../../../../utilities/response.js" @@ -9,7 +9,7 @@ import { selectMany } from "../../../../utilities/sql/selectMany.js" export const readAllAgentMessagesRoute = apiFactory .createApp() .post(readAllAgentMessagesRouteDefinition.path, async (c) => { - await checkUserSessionMiddleware({ + await checkAuthMiddleware({ context: c, }) const body = await validateBodyMiddleware({ diff --git a/packages/api/src/routes/auth/agent/agentSession/createOneAgentSession.ts b/packages/api/src/routes/auth/agent/agentSession/createOneAgentSession.ts index 959e3cbd..f092ccd8 100644 --- a/packages/api/src/routes/auth/agent/agentSession/createOneAgentSession.ts +++ b/packages/api/src/routes/auth/agent/agentSession/createOneAgentSession.ts @@ -1,6 +1,6 @@ import { createOneAgentSessionRouteDefinition, generateId, models } from "@arrhes/application-metadata" +import { checkAuthMiddleware } from "../../../../middlewares/checkAuthMiddleware.js" import { checkOrganizationSubscriptionSessionMiddleware } from "../../../../middlewares/checkOrganizationSubscriptionSessionMiddleware.js" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" import { apiFactory } from "../../../../utilities/apiFactory.js" import { response } from "../../../../utilities/response.js" @@ -9,7 +9,7 @@ import { insertOne } from "../../../../utilities/sql/insertOne.js" export const createOneAgentSessionRoute = apiFactory .createApp() .post(createOneAgentSessionRouteDefinition.path, async (c) => { - const { user } = await checkUserSessionMiddleware({ + const { user } = await checkAuthMiddleware({ context: c, }) const body = await validateBodyMiddleware({ diff --git a/packages/api/src/routes/auth/agent/agentSession/deleteOneAgentSession.ts b/packages/api/src/routes/auth/agent/agentSession/deleteOneAgentSession.ts index c6c91bbb..ef44b453 100644 --- a/packages/api/src/routes/auth/agent/agentSession/deleteOneAgentSession.ts +++ b/packages/api/src/routes/auth/agent/agentSession/deleteOneAgentSession.ts @@ -1,6 +1,6 @@ import { deleteOneAgentSessionRouteDefinition, models } from "@arrhes/application-metadata" import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" +import { checkAuthMiddleware } from "../../../../middlewares/checkAuthMiddleware.js" import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" import { apiFactory } from "../../../../utilities/apiFactory.js" import { response } from "../../../../utilities/response.js" @@ -9,7 +9,7 @@ import { deleteOne } from "../../../../utilities/sql/deleteOne.js" export const deleteOneAgentSessionRoute = apiFactory .createApp() .post(deleteOneAgentSessionRouteDefinition.path, async (c) => { - const { user } = await checkUserSessionMiddleware({ + const { user } = await checkAuthMiddleware({ context: c, }) const body = await validateBodyMiddleware({ diff --git a/packages/api/src/routes/auth/agent/agentSession/readAllAgentSessions.ts b/packages/api/src/routes/auth/agent/agentSession/readAllAgentSessions.ts index 040278a9..c0112e42 100644 --- a/packages/api/src/routes/auth/agent/agentSession/readAllAgentSessions.ts +++ b/packages/api/src/routes/auth/agent/agentSession/readAllAgentSessions.ts @@ -1,6 +1,6 @@ import { models, readAllAgentSessionsRouteDefinition } from "@arrhes/application-metadata" import { and, desc, eq, ilike, or } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" +import { checkAuthMiddleware } from "../../../../middlewares/checkAuthMiddleware.js" import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" import { apiFactory } from "../../../../utilities/apiFactory.js" import { response } from "../../../../utilities/response.js" @@ -9,7 +9,7 @@ import { selectMany } from "../../../../utilities/sql/selectMany.js" export const readAllAgentSessionsRoute = apiFactory .createApp() .post(readAllAgentSessionsRouteDefinition.path, async (c) => { - const { user } = await checkUserSessionMiddleware({ + const { user } = await checkAuthMiddleware({ context: c, }) const body = await validateBodyMiddleware({ diff --git a/packages/api/src/routes/auth/agent/agentSession/readOneAgentSession.ts b/packages/api/src/routes/auth/agent/agentSession/readOneAgentSession.ts index 11f2e952..04fbd7a4 100644 --- a/packages/api/src/routes/auth/agent/agentSession/readOneAgentSession.ts +++ b/packages/api/src/routes/auth/agent/agentSession/readOneAgentSession.ts @@ -1,6 +1,6 @@ import { models, readOneAgentSessionRouteDefinition } from "@arrhes/application-metadata" import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" +import { checkAuthMiddleware } from "../../../../middlewares/checkAuthMiddleware.js" import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" import { apiFactory } from "../../../../utilities/apiFactory.js" import { response } from "../../../../utilities/response.js" @@ -9,7 +9,7 @@ import { selectOne } from "../../../../utilities/sql/selectOne.js" export const readOneAgentSessionRoute = apiFactory .createApp() .post(readOneAgentSessionRouteDefinition.path, async (c) => { - const { user } = await checkUserSessionMiddleware({ + const { user } = await checkAuthMiddleware({ context: c, }) const body = await validateBodyMiddleware({ diff --git a/packages/api/src/routes/auth/agent/agentSession/updateOneAgentSession.ts b/packages/api/src/routes/auth/agent/agentSession/updateOneAgentSession.ts index 17652f10..6a005ca2 100644 --- a/packages/api/src/routes/auth/agent/agentSession/updateOneAgentSession.ts +++ b/packages/api/src/routes/auth/agent/agentSession/updateOneAgentSession.ts @@ -1,7 +1,7 @@ import { models, updateOneAgentSessionRouteDefinition } from "@arrhes/application-metadata" import { and, eq, inArray } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../middlewares/checkAuthMiddleware.js" import { checkOrganizationSubscriptionSessionMiddleware } from "../../../../middlewares/checkOrganizationSubscriptionSessionMiddleware.js" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" import { apiFactory } from "../../../../utilities/apiFactory.js" import { Exception } from "../../../../utilities/exception.js" @@ -13,7 +13,7 @@ import { updateOne } from "../../../../utilities/sql/updateOne.js" export const updateOneAgentSessionRoute = apiFactory .createApp() .post(updateOneAgentSessionRouteDefinition.path, async (c) => { - const { user } = await checkUserSessionMiddleware({ + const { user } = await checkAuthMiddleware({ context: c, }) const body = await validateBodyMiddleware({ @@ -43,15 +43,6 @@ export const updateOneAgentSessionRoute = apiFactory if (body.fileIds === null || body.fileIds.length === 0) { data.attachedFiles = null } else { - const idYear = body.idYear !== undefined ? body.idYear : session.idYear - if (!idYear) { - throw new Exception({ - statusCode: 400, - internalMessage: "Agent session has no year for file import", - externalMessage: "Veuillez sélectionner un exercice fiscal pour importer des fichiers", - }) - } - await checkOrganizationSubscriptionSessionMiddleware({ context: c, idOrganization: session.idOrganization, @@ -64,7 +55,6 @@ export const updateOneAgentSessionRoute = apiFactory .where( and( eq(models.file.idOrganization, session.idOrganization), - eq(models.file.idYear, idYear), inArray(models.file.id, body.fileIds), ), ) @@ -119,7 +109,7 @@ export const updateOneAgentSessionRoute = apiFactory const { ocrFile } = await processOcr({ var: c.var, idOrganization: session.idOrganization, - idYear: idYear, + idYear: session.idYear ?? "", idUser: user.id, sourceFile: file, }) diff --git a/packages/api/src/routes/auth/agent/searchReferenceable.ts b/packages/api/src/routes/auth/agent/searchReferenceable.ts index 8fde2bdb..f8ee5126 100644 --- a/packages/api/src/routes/auth/agent/searchReferenceable.ts +++ b/packages/api/src/routes/auth/agent/searchReferenceable.ts @@ -1,6 +1,6 @@ import { models, searchReferenceableRouteDefinition } from "@arrhes/application-metadata" import { and, eq, ilike, or, sql } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../middlewares/checkUserSessionMiddleware.js" +import { checkAuthMiddleware } from "../../../middlewares/checkAuthMiddleware.js" import { validateBodyMiddleware } from "../../../middlewares/validateBody.middleware.js" import { apiFactory } from "../../../utilities/apiFactory.js" import { response } from "../../../utilities/response.js" @@ -11,7 +11,7 @@ const MAX_TOTAL = 50 export const searchReferenceableRoute = apiFactory .createApp() .post(searchReferenceableRouteDefinition.path, async (c) => { - await checkUserSessionMiddleware({ + await checkAuthMiddleware({ context: c, }) const body = await validateBodyMiddleware({ @@ -142,7 +142,6 @@ export const searchReferenceableRoute = apiFactory .where( and( eq(models.file.idOrganization, idOrganization), - idYear ? eq(models.file.idYear, idYear) : undefined, or( ilike(sql`COALESCE(${models.file.name}, '')`, pattern), ilike(sql`COALESCE(${models.file.reference}, '')`, pattern), diff --git a/packages/api/src/routes/auth/authRoute.ts b/packages/api/src/routes/auth/authRoute.ts index 91b5c7cc..d7f7356f 100644 --- a/packages/api/src/routes/auth/authRoute.ts +++ b/packages/api/src/routes/auth/authRoute.ts @@ -1,9 +1,9 @@ -import { adminRoutes } from "../../routes/auth/admin/adminRoutes.js" -import { agentRoutes } from "../../routes/auth/agent/agentRoutes.js" -import { organizationsRoutes } from "../../routes/auth/organizations/organizationsRoutes.js" -import { settingsRoutes } from "../../routes/auth/settings/settingsRoutes.js" -import { supportRoutes } from "../../routes/auth/support/supportRoutes.js" import { apiFactory } from "../../utilities/apiFactory.js" +import { adminRoutes } from "./admin/adminRoutes.js" +import { agentRoutes } from "./agent/agentRoutes.js" +import { organizationsRoutes } from "./organizations/organizationsRoutes.js" +import { settingsRoutes } from "./settings/settingsRoutes.js" +import { supportRoutes } from "./support/supportRoutes.js" export const authRoute = apiFactory.createApp() diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/$idOrganizationRoutes.ts b/packages/api/src/routes/auth/organizations/$idOrganization/$idOrganizationRoutes.ts new file mode 100644 index 00000000..794ae368 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/$idOrganizationRoutes.ts @@ -0,0 +1,16 @@ +import { apiKeyRoutes } from "./organizationSettings/apiKey/apiKeyRoutes.js" +import { organizationPaymentsRoutes } from "./organizationSettings/organizationPayment/organizationPaymentsRoutes.js" +import { organizationSettingsRoutes } from "./organizationSettings/organizationSettingsRoutes.js" +import { organizationUsersRoutes } from "./organizationSettings/organizationUser/organizationUsersRoutes.js" +import { readOneOrganizationRoute } from "./readOneOrganization.js" +import { yearsRoutes } from "./years/yearsRoutes.js" + +export const $idOrganizationRoutes = [ + readOneOrganizationRoute, + + ...yearsRoutes, + ...organizationUsersRoutes, + ...organizationSettingsRoutes, + ...organizationPaymentsRoutes, + ...apiKeyRoutes, +] diff --git a/packages/api/src/routes/auth/$idOrganization/apiKey/apiKeyRoutes.ts b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/apiKey/apiKeyRoutes.ts similarity index 100% rename from packages/api/src/routes/auth/$idOrganization/apiKey/apiKeyRoutes.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/apiKey/apiKeyRoutes.ts diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/apiKey/createOneApiKey.ts b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/apiKey/createOneApiKey.ts new file mode 100644 index 00000000..003719bd --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/apiKey/createOneApiKey.ts @@ -0,0 +1,67 @@ +import { createHash } from "node:crypto" +import { createOneApiKeyRouteDefinition, generateId, models } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" +import { Exception } from "../../../../../../utilities/exception.js" +import { registerRoute } from "../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../utilities/response.js" +import { insertOne } from "../../../../../../utilities/sql/insertOne.js" +import { selectOne } from "../../../../../../utilities/sql/selectOne.js" + +export const createOneApiKeyRoute = registerRoute(createOneApiKeyRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: createOneApiKeyRouteDefinition.schemas.body, + }) + + // Must be admin of the organization + const organizationUser = await selectOne({ + database: c.var.clients.sql, + table: models.organizationUser, + where: (table) => and(eq(table.idUser, auth.user.id), eq(table.idOrganization, idOrganization)), + }) + if (organizationUser.isAdmin === false) { + throw new Exception({ + statusCode: 401, + internalMessage: "User is not admin of the organization", + externalMessage: "Vous n'êtes pas administrateur de l'organisation", + }) + } + + const rawKey = generateId() + const keyHash = createHash("sha256").update(rawKey).digest("hex") + + const createOneApiKey = await insertOne({ + database: c.var.clients.sql, + table: models.apiKey, + data: { + id: generateId(), + idOrganization: idOrganization, + idUser: auth.user.id, + keyHash: keyHash, + name: body.name ?? new Date().toISOString(), + isDefault: false, + isActive: true, + createdAt: new Date().toISOString(), + lastUpdatedAt: null, + }, + }) + + return response({ + context: c, + statusCode: 200, + schema: createOneApiKeyRouteDefinition.schemas.return, + data: { + ...createOneApiKey, + rawKey: rawKey, + }, + }) +}) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/apiKey/deleteOneApiKey.ts b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/apiKey/deleteOneApiKey.ts new file mode 100644 index 00000000..b1ce4f61 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/apiKey/deleteOneApiKey.ts @@ -0,0 +1,50 @@ +import { deleteOneApiKeyRouteDefinition, models } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" +import { Exception } from "../../../../../../utilities/exception.js" +import { registerRoute } from "../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../utilities/response.js" +import { deleteOne } from "../../../../../../utilities/sql/deleteOne.js" +import { selectOne } from "../../../../../../utilities/sql/selectOne.js" + +export const deleteOneApiKeyRoute = registerRoute(deleteOneApiKeyRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: deleteOneApiKeyRouteDefinition.schemas.body, + }) + + // Must be admin of the organization + const organizationUser = await selectOne({ + database: c.var.clients.sql, + table: models.organizationUser, + where: (table) => and(eq(table.idUser, auth.user.id), eq(table.idOrganization, idOrganization)), + }) + if (organizationUser.isAdmin === false) { + throw new Exception({ + statusCode: 401, + internalMessage: "User is not admin of the organization", + externalMessage: "Vous n'êtes pas administrateur de l'organisation", + }) + } + + const deleteOneApiKey = await deleteOne({ + database: c.var.clients.sql, + table: models.apiKey, + where: (table) => and(eq(table.id, body.idApiKey), eq(table.idOrganization, idOrganization)), + }) + + return response({ + context: c, + statusCode: 200, + schema: deleteOneApiKeyRouteDefinition.schemas.return, + data: deleteOneApiKey, + }) +}) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/apiKey/readAllApiKeys.ts b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/apiKey/readAllApiKeys.ts new file mode 100644 index 00000000..65a146e3 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/apiKey/readAllApiKeys.ts @@ -0,0 +1,47 @@ +import { models, readAllApiKeysRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" +import { Exception } from "../../../../../../utilities/exception.js" +import { registerRoute } from "../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../utilities/response.js" +import { selectOne } from "../../../../../../utilities/sql/selectOne.js" + +export const readAllApiKeysRoute = registerRoute(readAllApiKeysRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const _body = await validateBodyMiddleware({ + context: c, + schema: readAllApiKeysRouteDefinition.schemas.body, + }) + + // Must be admin of the organization + const organizationUser = await selectOne({ + database: c.var.clients.sql, + table: models.organizationUser, + where: (table) => and(eq(table.idUser, auth.user.id), eq(table.idOrganization, idOrganization)), + }) + if (organizationUser.isAdmin === false) { + throw new Exception({ + statusCode: 401, + internalMessage: "User is not admin of the organization", + externalMessage: "Vous n'êtes pas administrateur de l'organisation", + }) + } + + const readAllApiKeys = await c.var.clients.sql.query.apiKeyModel.findMany({ + where: (table) => and(eq(table.idOrganization, idOrganization)), + }) + + return response({ + context: c, + statusCode: 200, + schema: readAllApiKeysRouteDefinition.schemas.return, + data: readAllApiKeys, + }) +}) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/deleteOneOrganization.ts b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/deleteOneOrganization.ts new file mode 100644 index 00000000..6744df71 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/deleteOneOrganization.ts @@ -0,0 +1,51 @@ +import { deleteOneOrganizationRouteDefinition, models } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../utilities/apiFactory.js" +import { Exception } from "../../../../../utilities/exception.js" +import { response } from "../../../../../utilities/response.js" +import { deleteOne } from "../../../../../utilities/sql/deleteOne.js" +import { selectOne } from "../../../../../utilities/sql/selectOne.js" + +export const deleteOneOrganizationRoute = apiFactory + .createApp() + .post(deleteOneOrganizationRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const _body = await validateBodyMiddleware({ + context: c, + schema: deleteOneOrganizationRouteDefinition.schemas.body, + }) + + const organizationUser = await selectOne({ + database: c.var.clients.sql, + table: models.organizationUser, + where: (table) => and(eq(table.idUser, auth.user.id), eq(table.idOrganization, idOrganization)), + }) + if (organizationUser.isAdmin === false) { + throw new Exception({ + statusCode: 401, + internalMessage: "User is not admin of the organization", + externalMessage: "Vous n'êtes pas administrateur de l'organisation", + }) + } + + const deleteOneOrganization = await deleteOne({ + database: c.var.clients.sql, + table: models.organization, + where: (table) => and(eq(table.id, organizationUser.idOrganization)), + }) + + return response({ + context: c, + statusCode: 200, + schema: deleteOneOrganizationRouteDefinition.schemas.return, + data: deleteOneOrganization, + }) + }) diff --git a/packages/api/src/routes/auth/$idOrganization/organizationPayment/cancelOrganizationBilling.ts b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/cancelOrganizationBilling.ts similarity index 80% rename from packages/api/src/routes/auth/$idOrganization/organizationPayment/cancelOrganizationBilling.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/cancelOrganizationBilling.ts index 4830e127..54568f17 100644 --- a/packages/api/src/routes/auth/$idOrganization/organizationPayment/cancelOrganizationBilling.ts +++ b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/cancelOrganizationBilling.ts @@ -1,12 +1,13 @@ import { cancelOrganizationBillingRouteDefinition, models } from "@arrhes/application-metadata" import { and, eq, sql } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { Exception } from "../../../../utilities/exception.js" -import { response } from "../../../../utilities/response.js" -import { selectOne } from "../../../../utilities/sql/selectOne.js" -import { updateOne } from "../../../../utilities/sql/updateOne.js" +import { checkAuthMiddleware } from "../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../utilities/apiFactory.js" +import { Exception } from "../../../../../../utilities/exception.js" +import { response } from "../../../../../../utilities/response.js" +import { selectOne } from "../../../../../../utilities/sql/selectOne.js" +import { updateOne } from "../../../../../../utilities/sql/updateOne.js" // How much each resource unit removes from limits (mirrors createResourceSubscription) const UNIT_LIMITS = { @@ -40,9 +41,12 @@ function getEndOfCurrentMonth(): Date { export const cancelOrganizationBillingRoute = apiFactory .createApp() .post(cancelOrganizationBillingRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ + const auth = await checkAuthMiddleware({ context: c, }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) const body = await validateBodyMiddleware({ context: c, schema: cancelOrganizationBillingRouteDefinition.schemas.body, @@ -51,7 +55,7 @@ export const cancelOrganizationBillingRoute = apiFactory const organizationUser = await selectOne({ database: c.var.clients.sql, table: models.organizationUser, - where: (table) => and(eq(table.idUser, user.id), eq(table.idOrganization, idOrganization)), + where: (table) => and(eq(table.idUser, auth.user.id), eq(table.idOrganization, idOrganization)), }) if (organizationUser.isAdmin === false) { throw new Exception({ @@ -81,7 +85,7 @@ export const cancelOrganizationBillingRoute = apiFactory status: "cancelled", endsAt: endsAt.toISOString(), lastUpdatedAt: now.toISOString(), - lastUpdatedBy: user.id, + lastUpdatedBy: auth.user.id, }, where: (table) => eq(table.id, subscription.id), }) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/cancelSubscription.ts b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/cancelSubscription.ts new file mode 100644 index 00000000..6084ac99 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/cancelSubscription.ts @@ -0,0 +1,24 @@ +import { cancelSubscriptionRouteDefinition } from "@arrhes/application-metadata" +import { checkAuthMiddleware } from "../../../../../../middlewares/checkAuthMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../utilities/response.js" + +export const cancelSubscriptionRoute = apiFactory + .createApp() + .post(cancelSubscriptionRouteDefinition.path, async (c) => { + await checkAuthMiddleware({ + context: c, + }) + await validateBodyMiddleware({ + context: c, + schema: cancelSubscriptionRouteDefinition.schemas.body, + }) + + return response({ + context: c, + statusCode: 200, + schema: cancelSubscriptionRouteDefinition.schemas.return, + data: {}, + }) + }) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createFirstPayment.ts b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createFirstPayment.ts new file mode 100644 index 00000000..3f2bbbc4 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createFirstPayment.ts @@ -0,0 +1,23 @@ +import { createFirstPaymentRouteDefinition } from "@arrhes/application-metadata" +import { checkAuthMiddleware } from "../../../../../../middlewares/checkAuthMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../utilities/apiFactory.js" +import { Exception } from "../../../../../../utilities/exception.js" + +export const createFirstPaymentRoute = apiFactory + .createApp() + .post(createFirstPaymentRouteDefinition.path, async (c) => { + await checkAuthMiddleware({ + context: c, + }) + await validateBodyMiddleware({ + context: c, + schema: createFirstPaymentRouteDefinition.schemas.body, + }) + + throw new Exception({ + statusCode: 400, + internalMessage: "Mollie subscription integration has been retired", + externalMessage: "Cette fonctionnalité n'est plus disponible", + }) + }) diff --git a/packages/api/src/routes/auth/$idOrganization/organizationPayment/createPaymentMethodCheckout.ts b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createPaymentMethodCheckout.ts similarity index 76% rename from packages/api/src/routes/auth/$idOrganization/organizationPayment/createPaymentMethodCheckout.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createPaymentMethodCheckout.ts index 84f7f629..8be38aef 100644 --- a/packages/api/src/routes/auth/$idOrganization/organizationPayment/createPaymentMethodCheckout.ts +++ b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createPaymentMethodCheckout.ts @@ -1,15 +1,16 @@ import { createPaymentMethodCheckoutRouteDefinition, models } from "@arrhes/application-metadata" import { SequenceType } from "@mollie/api-client" import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { recordOrganizationPayment } from "../../../../utilities/billing/wallet.js" -import { Exception } from "../../../../utilities/exception.js" -import { response } from "../../../../utilities/response.js" -import { selectOne } from "../../../../utilities/sql/selectOne.js" -import { updateOne } from "../../../../utilities/sql/updateOne.js" -import { productName } from "../../../../utilities/variables.js" +import { checkAuthMiddleware } from "../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../utilities/apiFactory.js" +import { recordOrganizationPayment } from "../../../../../../utilities/billing/wallet.js" +import { Exception } from "../../../../../../utilities/exception.js" +import { response } from "../../../../../../utilities/response.js" +import { selectOne } from "../../../../../../utilities/sql/selectOne.js" +import { updateOne } from "../../../../../../utilities/sql/updateOne.js" +import { productName } from "../../../../../../utilities/variables.js" const SETUP_PAYMENT_AMOUNT_IN_CENTS = 1 @@ -20,9 +21,12 @@ function formatAmountFromCents(cents: number): string { export const createPaymentMethodCheckoutRoute = apiFactory .createApp() .post(createPaymentMethodCheckoutRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ + const auth = await checkAuthMiddleware({ context: c, }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) const _body = await validateBodyMiddleware({ context: c, schema: createPaymentMethodCheckoutRouteDefinition.schemas.body, @@ -31,7 +35,7 @@ export const createPaymentMethodCheckoutRoute = apiFactory const organizationUser = await selectOne({ database: c.var.clients.sql, table: models.organizationUser, - where: (table) => and(eq(table.idUser, user.id), eq(table.idOrganization, idOrganization)), + where: (table) => and(eq(table.idUser, auth.user.id), eq(table.idOrganization, idOrganization)), }) if (organizationUser.isAdmin === false) { @@ -75,7 +79,7 @@ export const createPaymentMethodCheckoutRoute = apiFactory data: { mollieCustomerId, lastUpdatedAt: new Date().toISOString(), - lastUpdatedBy: user.id, + lastUpdatedBy: auth.user.id, }, where: (table) => eq(table.id, organization.id), }) @@ -102,7 +106,7 @@ export const createPaymentMethodCheckoutRoute = apiFactory description: "Ajout du moyen de paiement", sequenceType: "setup", molliePaymentId: molliePayment.id, - createdBy: user.id, + createdBy: auth.user.id, }) const checkoutUrl = molliePayment.getCheckoutUrl() diff --git a/packages/api/src/routes/auth/$idOrganization/organizationPayment/createResourceSubscription.ts b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createResourceSubscription.ts similarity index 86% rename from packages/api/src/routes/auth/$idOrganization/organizationPayment/createResourceSubscription.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createResourceSubscription.ts index 3ee643e3..3716a0ad 100644 --- a/packages/api/src/routes/auth/$idOrganization/organizationPayment/createResourceSubscription.ts +++ b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createResourceSubscription.ts @@ -1,8 +1,9 @@ import { createResourceSubscriptionRouteDefinition, models } from "@arrhes/application-metadata" import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" +import { checkAuthMiddleware } from "../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../utilities/apiFactory.js" import { FREE_STORAGE_BYTES, getOcrAddonQuantity, @@ -14,19 +15,22 @@ import { getTotalTokensFromQuantity, INCLUDED_OCR_PAGES, isOneTimeServiceType, -} from "../../../../utilities/billing/subscriptionPricing.js" -import { creditOrganizationWallet, debitOrganizationWallet } from "../../../../utilities/billing/wallet.js" -import { Exception } from "../../../../utilities/exception.js" -import { response } from "../../../../utilities/response.js" -import { selectOne } from "../../../../utilities/sql/selectOne.js" -import { updateOne } from "../../../../utilities/sql/updateOne.js" +} from "../../../../../../utilities/billing/subscriptionPricing.js" +import { creditOrganizationWallet, debitOrganizationWallet } from "../../../../../../utilities/billing/wallet.js" +import { Exception } from "../../../../../../utilities/exception.js" +import { response } from "../../../../../../utilities/response.js" +import { selectOne } from "../../../../../../utilities/sql/selectOne.js" +import { updateOne } from "../../../../../../utilities/sql/updateOne.js" export const createResourceSubscriptionRoute = apiFactory .createApp() .post(createResourceSubscriptionRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ + const auth = await checkAuthMiddleware({ context: c, }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) const body = await validateBodyMiddleware({ context: c, schema: createResourceSubscriptionRouteDefinition.schemas.body, @@ -35,7 +39,7 @@ export const createResourceSubscriptionRoute = apiFactory const organizationUser = await selectOne({ database: c.var.clients.sql, table: models.organizationUser, - where: (table) => and(eq(table.idUser, user.id), eq(table.idOrganization, idOrganization)), + where: (table) => and(eq(table.idUser, auth.user.id), eq(table.idOrganization, idOrganization)), }) if (organizationUser.isAdmin === false) { throw new Exception({ @@ -139,7 +143,7 @@ export const createResourceSubscriptionRoute = apiFactory await debitOrganizationWallet({ database: c.var.clients.sql, idOrganization, - idUser: user.id, + idUser: auth.user.id, amountHTInCents: deltaAmountInCents, quantity: body.type === "ocr_pages_hundred" ? deltaQuantity * INCLUDED_OCR_PAGES : deltaQuantity, unitAmountHTInCents: getResourceSubscriptionUnitPriceInCents(body.type), @@ -157,7 +161,7 @@ export const createResourceSubscriptionRoute = apiFactory await creditOrganizationWallet({ database: c.var.clients.sql, idOrganization, - idUser: user.id, + idUser: auth.user.id, amountHTInCents: deltaAmountInCents, serviceType: body.type, description: "Réduction du stockage créditée au portefeuille", @@ -176,7 +180,7 @@ export const createResourceSubscriptionRoute = apiFactory ? Math.max(nextTokensTotal - organization.tokensTotalUsed, 0) : undefined, lastUpdatedAt: now.toISOString(), - lastUpdatedBy: user.id, + lastUpdatedBy: auth.user.id, }, where: (table) => eq(table.id, idOrganization), }) diff --git a/packages/api/src/routes/auth/$idOrganization/organizationPayment/createWalletTopUpCheckout.ts b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createWalletTopUpCheckout.ts similarity index 75% rename from packages/api/src/routes/auth/$idOrganization/organizationPayment/createWalletTopUpCheckout.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createWalletTopUpCheckout.ts index 9858a0fd..548e5771 100644 --- a/packages/api/src/routes/auth/$idOrganization/organizationPayment/createWalletTopUpCheckout.ts +++ b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createWalletTopUpCheckout.ts @@ -1,22 +1,26 @@ import { createWalletTopUpCheckoutRouteDefinition, models } from "@arrhes/application-metadata" import { SequenceType } from "@mollie/api-client" import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { formatAmountFromCents, recordOrganizationPayment } from "../../../../utilities/billing/wallet.js" -import { Exception } from "../../../../utilities/exception.js" -import { response } from "../../../../utilities/response.js" -import { selectOne } from "../../../../utilities/sql/selectOne.js" -import { updateOne } from "../../../../utilities/sql/updateOne.js" -import { productName } from "../../../../utilities/variables.js" +import { checkAuthMiddleware } from "../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../utilities/apiFactory.js" +import { formatAmountFromCents, recordOrganizationPayment } from "../../../../../../utilities/billing/wallet.js" +import { Exception } from "../../../../../../utilities/exception.js" +import { response } from "../../../../../../utilities/response.js" +import { selectOne } from "../../../../../../utilities/sql/selectOne.js" +import { updateOne } from "../../../../../../utilities/sql/updateOne.js" +import { productName } from "../../../../../../utilities/variables.js" export const createWalletTopUpCheckoutRoute = apiFactory .createApp() .post(createWalletTopUpCheckoutRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ + const auth = await checkAuthMiddleware({ context: c, }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) const body = await validateBodyMiddleware({ context: c, schema: createWalletTopUpCheckoutRouteDefinition.schemas.body, @@ -25,7 +29,7 @@ export const createWalletTopUpCheckoutRoute = apiFactory const organizationUser = await selectOne({ database: c.var.clients.sql, table: models.organizationUser, - where: (table) => and(eq(table.idUser, user.id), eq(table.idOrganization, idOrganization)), + where: (table) => and(eq(table.idUser, auth.user.id), eq(table.idOrganization, idOrganization)), }) if (organizationUser.isAdmin === false) { throw new Exception({ @@ -60,7 +64,7 @@ export const createWalletTopUpCheckoutRoute = apiFactory data: { mollieCustomerId, lastUpdatedAt: new Date().toISOString(), - lastUpdatedBy: user.id, + lastUpdatedBy: auth.user.id, }, where: (table) => eq(table.id, organization.id), }) @@ -87,7 +91,7 @@ export const createWalletTopUpCheckoutRoute = apiFactory description: "Recharge portefeuille", sequenceType: "oneoff", molliePaymentId: molliePayment.id, - createdBy: user.id, + createdBy: auth.user.id, }) const checkoutUrl = molliePayment.getCheckoutUrl() diff --git a/packages/api/src/routes/auth/$idOrganization/organizationPayment/createWalletWithdrawal.ts b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createWalletWithdrawal.ts similarity index 85% rename from packages/api/src/routes/auth/$idOrganization/organizationPayment/createWalletWithdrawal.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createWalletWithdrawal.ts index 18413910..8efb5785 100644 --- a/packages/api/src/routes/auth/$idOrganization/organizationPayment/createWalletWithdrawal.ts +++ b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createWalletWithdrawal.ts @@ -1,25 +1,29 @@ import { createWalletWithdrawalRouteDefinition, models } from "@arrhes/application-metadata" import { and, desc, eq, gte, inArray, lt, ne } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" +import { checkAuthMiddleware } from "../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../utilities/apiFactory.js" import { formatAmountFromCents, mapMollieRefundStatusToPaymentStatus, recordOrganizationPayment, -} from "../../../../utilities/billing/wallet.js" -import { Exception } from "../../../../utilities/exception.js" -import { response } from "../../../../utilities/response.js" -import { selectMany } from "../../../../utilities/sql/selectMany.js" -import { selectOne } from "../../../../utilities/sql/selectOne.js" -import { productName } from "../../../../utilities/variables.js" +} from "../../../../../../utilities/billing/wallet.js" +import { Exception } from "../../../../../../utilities/exception.js" +import { response } from "../../../../../../utilities/response.js" +import { selectMany } from "../../../../../../utilities/sql/selectMany.js" +import { selectOne } from "../../../../../../utilities/sql/selectOne.js" +import { productName } from "../../../../../../utilities/variables.js" export const createWalletWithdrawalRoute = apiFactory .createApp() .post(createWalletWithdrawalRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ + const auth = await checkAuthMiddleware({ context: c, }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) const body = await validateBodyMiddleware({ context: c, schema: createWalletWithdrawalRouteDefinition.schemas.body, @@ -28,7 +32,7 @@ export const createWalletWithdrawalRoute = apiFactory const organizationUser = await selectOne({ database: c.var.clients.sql, table: models.organizationUser, - where: (table) => and(eq(table.idUser, user.id), eq(table.idOrganization, idOrganization)), + where: (table) => and(eq(table.idUser, auth.user.id), eq(table.idOrganization, idOrganization)), }) if (organizationUser.isAdmin === false) { throw new Exception({ @@ -164,7 +168,7 @@ export const createWalletWithdrawalRoute = apiFactory amountInCents: body.amountInCents, description: "Retrait portefeuille", molliePaymentId: sourcePayment.molliePaymentId, - createdBy: user.id, + createdBy: auth.user.id, }) return response({ diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/generateInvoiceGetSignedUrl.ts b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/generateInvoiceGetSignedUrl.ts new file mode 100644 index 00000000..6f42a157 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/generateInvoiceGetSignedUrl.ts @@ -0,0 +1,59 @@ +import { generateInvoiceGetSignedUrlRouteDefinition, models } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../utilities/apiFactory.js" +import { Exception } from "../../../../../../utilities/exception.js" +import { response } from "../../../../../../utilities/response.js" +import { selectOne } from "../../../../../../utilities/sql/selectOne.js" +import { generateGetSignedUrl } from "../../../../../../utilities/storage/generateGetSignedUrl.js" + +export const generateInvoiceGetSignedUrlRoute = apiFactory + .createApp() + .post(generateInvoiceGetSignedUrlRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: generateInvoiceGetSignedUrlRouteDefinition.schemas.body, + }) + + await selectOne({ + database: c.var.clients.sql, + table: models.organizationUser, + where: (table) => and(eq(table.idUser, auth.user.id), eq(table.idOrganization, idOrganization)), + }) + + const invoice = await selectOne({ + database: c.var.clients.sql, + table: models.invoice, + where: (table) => and(eq(table.id, body.idInvoice), eq(table.idOrganization, idOrganization)), + }) + + if (invoice.xmlStorageKey === null) { + throw new Exception({ + statusCode: 404, + internalMessage: "Invoice PDF not yet generated", + externalMessage: "La facture PDF n'est pas encore disponible", + }) + } + + const url = await generateGetSignedUrl({ + var: c.var, + storageKey: invoice.xmlStorageKey, + }) + + return response({ + context: c, + statusCode: 200, + schema: generateInvoiceGetSignedUrlRouteDefinition.schemas.return, + data: { + url, + }, + }) + }) diff --git a/packages/api/src/routes/auth/$idOrganization/organizationPayment/organizationPaymentsRoutes.ts b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/organizationPaymentsRoutes.ts similarity index 100% rename from packages/api/src/routes/auth/$idOrganization/organizationPayment/organizationPaymentsRoutes.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/organizationPaymentsRoutes.ts diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readAllInvoices.ts b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readAllInvoices.ts new file mode 100644 index 00000000..84ac9ea7 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readAllInvoices.ts @@ -0,0 +1,81 @@ +import { models, readAllInvoicesRouteDefinition } from "@arrhes/application-metadata" +import { and, desc, eq, inArray, isNotNull, sum } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../utilities/response.js" +import { selectMany } from "../../../../../../utilities/sql/selectMany.js" +import { selectOne } from "../../../../../../utilities/sql/selectOne.js" + +export const readAllInvoicesRoute = registerRoute(readAllInvoicesRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const _body = await validateBodyMiddleware({ + context: c, + schema: readAllInvoicesRouteDefinition.schemas.body, + }) + + await selectOne({ + database: c.var.clients.sql, + table: models.organizationUser, + where: (table) => and(eq(table.idUser, auth.user.id), eq(table.idOrganization, idOrganization)), + }) + + const invoices = await selectMany({ + database: c.var.clients.sql, + table: models.invoice, + where: (table) => eq(table.idOrganization, idOrganization), + orderBy: (table) => desc(table.startingAt), + }) + + // For draft invoices, `amountInCents` is 0 at creation and only updated when the monthly + // worker finalizes them. Compute the live amount from linked payments instead so that the + // invoices page matches the wallet "Mois en cours" total. + const draftInvoiceIds = invoices.filter((inv) => inv.status === "draft").map((inv) => inv.id) + + let draftAmounts: Map = new Map() + + if (draftInvoiceIds.length > 0) { + const rows = await c.var.clients.sql + .select({ + idInvoice: models.organizationPayment.idInvoice, + total: sum(models.organizationPayment.amountHTInCents), + }) + .from(models.organizationPayment) + .where( + and( + inArray(models.organizationPayment.idInvoice, draftInvoiceIds), + isNotNull(models.organizationPayment.serviceType), + eq(models.organizationPayment.status, "paid"), + ), + ) + .groupBy(models.organizationPayment.idInvoice) + + draftAmounts = new Map( + rows.map((row) => [ + row.idInvoice as string, + Number(row.total ?? 0), + ]), + ) + } + + const data = invoices.map((inv) => { + if (inv.status !== "draft") return inv + return { + ...inv, + amountInCents: draftAmounts.get(inv.id) ?? 0, + } + }) + + return response({ + context: c, + statusCode: 200, + schema: readAllInvoicesRouteDefinition.schemas.return, + data, + }) +}) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readAllOrganizationBillings.ts b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readAllOrganizationBillings.ts new file mode 100644 index 00000000..3556b289 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readAllOrganizationBillings.ts @@ -0,0 +1,53 @@ +import { models, readAllOrganizationBillingsRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../utilities/apiFactory.js" +import { + getSubscriptionMonthlyAmountInCents, + isResourceSubscriptionType, +} from "../../../../../../utilities/billing/subscriptionPricing.js" +import { response } from "../../../../../../utilities/response.js" +import { selectMany } from "../../../../../../utilities/sql/selectMany.js" +import { selectOne } from "../../../../../../utilities/sql/selectOne.js" + +export const readAllOrganizationBillingsRoute = apiFactory + .createApp() + .post(readAllOrganizationBillingsRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const _body = await validateBodyMiddleware({ + context: c, + schema: readAllOrganizationBillingsRouteDefinition.schemas.body, + }) + + await selectOne({ + database: c.var.clients.sql, + table: models.organizationUser, + where: (table) => and(eq(table.idUser, auth.user.id), eq(table.idOrganization, idOrganization)), + }) + + const subscriptions = await selectMany({ + database: c.var.clients.sql, + table: models.organizationBilling, + where: (table) => eq(table.idOrganization, idOrganization), + }) + + return response({ + context: c, + statusCode: 200, + schema: readAllOrganizationBillingsRouteDefinition.schemas.return, + data: subscriptions.map((subscription) => ({ + ...subscription, + amountInCents: + subscription.status === "active" && isResourceSubscriptionType(subscription.type) + ? getSubscriptionMonthlyAmountInCents(subscription) + : subscription.amountInCents, + })), + }) + }) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readAllOrganizationPayments.ts b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readAllOrganizationPayments.ts new file mode 100644 index 00000000..442b41ae --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readAllOrganizationPayments.ts @@ -0,0 +1,52 @@ +import { models, readAllOrganizationPaymentsRouteDefinition } from "@arrhes/application-metadata" +import { and, desc, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../utilities/apiFactory.js" +import { Exception } from "../../../../../../utilities/exception.js" +import { response } from "../../../../../../utilities/response.js" +import { selectMany } from "../../../../../../utilities/sql/selectMany.js" +import { selectOne } from "../../../../../../utilities/sql/selectOne.js" + +export const readAllOrganizationPaymentsRoute = apiFactory + .createApp() + .post(readAllOrganizationPaymentsRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const _body = await validateBodyMiddleware({ + context: c, + schema: readAllOrganizationPaymentsRouteDefinition.schemas.body, + }) + + const organizationUser = await selectOne({ + database: c.var.clients.sql, + table: models.organizationUser, + where: (table) => and(eq(table.idUser, auth.user.id), eq(table.idOrganization, idOrganization)), + }) + if (organizationUser.isAdmin === false) { + throw new Exception({ + statusCode: 401, + internalMessage: "User is not admin of the organization", + externalMessage: "Vous n'êtes pas administrateur de l'organisation", + }) + } + + const readAllOrganizationPayments = await selectMany({ + database: c.var.clients.sql, + table: models.organizationPayment, + where: (table) => eq(table.idOrganization, organizationUser.idOrganization), + orderBy: (table) => desc(table.createdAt), + }) + + return response({ + context: c, + statusCode: 200, + schema: readAllOrganizationPaymentsRouteDefinition.schemas.return, + data: readAllOrganizationPayments, + }) + }) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readOneInvoice.ts b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readOneInvoice.ts new file mode 100644 index 00000000..35aca9ef --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readOneInvoice.ts @@ -0,0 +1,40 @@ +import { models, readOneInvoiceRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../utilities/response.js" +import { selectOne } from "../../../../../../utilities/sql/selectOne.js" + +export const readOneInvoiceRoute = registerRoute(readOneInvoiceRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: readOneInvoiceRouteDefinition.schemas.body, + }) + + await selectOne({ + database: c.var.clients.sql, + table: models.organizationUser, + where: (table) => and(eq(table.idUser, auth.user.id), eq(table.idOrganization, idOrganization)), + }) + + const invoice = await selectOne({ + database: c.var.clients.sql, + table: models.invoice, + where: (table) => and(eq(table.id, body.idInvoice), eq(table.idOrganization, idOrganization)), + }) + + return response({ + context: c, + statusCode: 200, + schema: readOneInvoiceRouteDefinition.schemas.return, + data: invoice, + }) +}) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readOrganizationBilling.ts b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readOrganizationBilling.ts new file mode 100644 index 00000000..4e486097 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readOrganizationBilling.ts @@ -0,0 +1,70 @@ +import { models, readOrganizationBillingRouteDefinition } from "@arrhes/application-metadata" +import { and, desc, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../utilities/apiFactory.js" +import { computeMonthlyTotal } from "../../../../../../utilities/billing/computeMonthlyTotal.js" +import { response } from "../../../../../../utilities/response.js" +import { selectMany } from "../../../../../../utilities/sql/selectMany.js" +import { selectOne } from "../../../../../../utilities/sql/selectOne.js" + +export const readOrganizationBillingRoute = apiFactory + .createApp() + .post(readOrganizationBillingRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const _body = await validateBodyMiddleware({ + context: c, + schema: readOrganizationBillingRouteDefinition.schemas.body, + }) + + // Verify auth.user is member of the organization + await selectOne({ + database: c.var.clients.sql, + table: models.organizationUser, + where: (table) => and(eq(table.idUser, auth.user.id), eq(table.idOrganization, idOrganization)), + }) + + // Get organization + const organization = await selectOne({ + database: c.var.clients.sql, + table: models.organization, + where: (table) => eq(table.id, idOrganization), + }) + + // Get latest payment status + const payments = await selectMany({ + database: c.var.clients.sql, + table: models.organizationPayment, + where: (table) => eq(table.idOrganization, idOrganization), + orderBy: (table) => desc(table.createdAt), + }) + + const latestPayment = payments.at(0) + const totalSubscriptionAmountInCents = await computeMonthlyTotal({ + var: c.var, + idOrganization, + }) + + return response({ + context: c, + statusCode: 200, + schema: readOrganizationBillingRouteDefinition.schemas.return, + data: { + status: latestPayment?.status ?? null, + licenceAmount: organization.licenceAmount, + storageLimit: organization.storageLimit, + storageCurrentUsage: organization.storageCurrentUsage, + ocrPagesTotalAvailable: organization.ocrPagesTotalAvailable, + ocrPagesTotalUsed: organization.ocrPagesTotalUsed, + tokensTotalAvailable: organization.tokensTotalAvailable, + tokensTotalUsed: organization.tokensTotalUsed, + totalSubscriptionAmountInCents, + }, + }) + }) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/updateLicenceSubscription.ts b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/updateLicenceSubscription.ts new file mode 100644 index 00000000..f8df588c --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/updateLicenceSubscription.ts @@ -0,0 +1,75 @@ +import { models, updateLicenceSubscriptionRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../utilities/apiFactory.js" +import { Exception } from "../../../../../../utilities/exception.js" +import { response } from "../../../../../../utilities/response.js" +import { selectOne } from "../../../../../../utilities/sql/selectOne.js" +import { updateOne } from "../../../../../../utilities/sql/updateOne.js" + +export const updateLicenceSubscriptionRoute = apiFactory + .createApp() + .post(updateLicenceSubscriptionRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: updateLicenceSubscriptionRouteDefinition.schemas.body, + }) + + const organizationUser = await selectOne({ + database: c.var.clients.sql, + table: models.organizationUser, + where: (table) => and(eq(table.idUser, auth.user.id), eq(table.idOrganization, idOrganization)), + }) + if (organizationUser.isAdmin === false) { + throw new Exception({ + statusCode: 401, + internalMessage: "User is not admin of the organization", + externalMessage: "Vous n'êtes pas administrateur de l'organisation", + }) + } + + if (Number.isNaN(body.newAmountInCents) || body.newAmountInCents < 0) { + throw new Exception({ + statusCode: 400, + internalMessage: "Invalid amount", + externalMessage: "Montant invalide", + }) + } + + const organization = await selectOne({ + database: c.var.clients.sql, + table: models.organization, + where: (table) => eq(table.id, idOrganization), + }) + + // Store as pending — applied on the 1st of next month by the worker + const pendingValue = body.newAmountInCents === organization.licenceAmount ? null : body.newAmountInCents + + await updateOne({ + database: c.var.clients.sql, + table: models.organization, + data: { + licenceAmountPending: pendingValue, + lastUpdatedAt: new Date().toISOString(), + lastUpdatedBy: auth.user.id, + }, + where: (table) => eq(table.id, idOrganization), + }) + + return response({ + context: c, + statusCode: 200, + schema: updateLicenceSubscriptionRouteDefinition.schemas.return, + data: { + checkoutUrl: null, + }, + }) + }) diff --git a/packages/api/src/routes/auth/$idOrganization/organizationPayment/updateOcrSubscription.ts b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/updateOcrSubscription.ts similarity index 76% rename from packages/api/src/routes/auth/$idOrganization/organizationPayment/updateOcrSubscription.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/updateOcrSubscription.ts index f91f06a6..351863b0 100644 --- a/packages/api/src/routes/auth/$idOrganization/organizationPayment/updateOcrSubscription.ts +++ b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/updateOcrSubscription.ts @@ -1,23 +1,30 @@ import { models, updateOcrSubscriptionRouteDefinition } from "@arrhes/application-metadata" import { OCR_PAGE_PRICE_IN_CENTS } from "@arrhes/application-metadata/utilities" import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { findOrCreateCurrentPeriodInvoice, getCurrentMonthRange } from "../../../../utilities/billing/billingInvoice.js" -import { INCLUDED_OCR_PAGES } from "../../../../utilities/billing/subscriptionPricing.js" -import { recordOrganizationPayment } from "../../../../utilities/billing/wallet.js" -import { Exception } from "../../../../utilities/exception.js" -import { response } from "../../../../utilities/response.js" -import { selectOne } from "../../../../utilities/sql/selectOne.js" -import { updateOne } from "../../../../utilities/sql/updateOne.js" +import { checkAuthMiddleware } from "../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../utilities/apiFactory.js" +import { + findOrCreateCurrentPeriodInvoice, + getCurrentMonthRange, +} from "../../../../../../utilities/billing/billingInvoice.js" +import { INCLUDED_OCR_PAGES } from "../../../../../../utilities/billing/subscriptionPricing.js" +import { recordOrganizationPayment } from "../../../../../../utilities/billing/wallet.js" +import { Exception } from "../../../../../../utilities/exception.js" +import { response } from "../../../../../../utilities/response.js" +import { selectOne } from "../../../../../../utilities/sql/selectOne.js" +import { updateOne } from "../../../../../../utilities/sql/updateOne.js" export const updateOcrSubscriptionRoute = apiFactory .createApp() .post(updateOcrSubscriptionRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ + const auth = await checkAuthMiddleware({ context: c, }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) const body = await validateBodyMiddleware({ context: c, schema: updateOcrSubscriptionRouteDefinition.schemas.body, @@ -26,7 +33,7 @@ export const updateOcrSubscriptionRoute = apiFactory const organizationUser = await selectOne({ database: c.var.clients.sql, table: models.organizationUser, - where: (table) => and(eq(table.idUser, user.id), eq(table.idOrganization, idOrganization)), + where: (table) => and(eq(table.idUser, auth.user.id), eq(table.idOrganization, idOrganization)), }) if (organizationUser.isAdmin === false) { throw new Exception({ @@ -85,7 +92,7 @@ export const updateOcrSubscriptionRoute = apiFactory ocrPagesTotalAvailable: Math.max(nextOcrTotal - organization.ocrPagesTotalUsed, 0), walletBalanceInCents: newWalletBalanceInCents, lastUpdatedAt: now.toISOString(), - lastUpdatedBy: user.id, + lastUpdatedBy: auth.user.id, }, where: (table) => eq(table.id, idOrganization), }) @@ -110,7 +117,7 @@ export const updateOcrSubscriptionRoute = apiFactory description: "Achat pages OCR", serviceType: "ocr_pages_hundred", idInvoice, - createdBy: user.id, + createdBy: auth.user.id, }) } }) diff --git a/packages/api/src/routes/auth/$idOrganization/organizationPayment/updateStorageSubscription.ts b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/updateStorageSubscription.ts similarity index 77% rename from packages/api/src/routes/auth/$idOrganization/organizationPayment/updateStorageSubscription.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/updateStorageSubscription.ts index a3b76828..1907fc67 100644 --- a/packages/api/src/routes/auth/$idOrganization/organizationPayment/updateStorageSubscription.ts +++ b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/updateStorageSubscription.ts @@ -1,18 +1,19 @@ import { models, updateStorageSubscriptionRouteDefinition } from "@arrhes/application-metadata" import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" +import { checkAuthMiddleware } from "../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../utilities/apiFactory.js" import { FREE_STORAGE_BYTES, getResourceSubscriptionUnitPriceInCents, getStorageAddonQuantity, -} from "../../../../utilities/billing/subscriptionPricing.js" -import { debitOrganizationWallet } from "../../../../utilities/billing/wallet.js" -import { Exception } from "../../../../utilities/exception.js" -import { response } from "../../../../utilities/response.js" -import { selectOne } from "../../../../utilities/sql/selectOne.js" -import { updateOne } from "../../../../utilities/sql/updateOne.js" +} from "../../../../../../utilities/billing/subscriptionPricing.js" +import { debitOrganizationWallet } from "../../../../../../utilities/billing/wallet.js" +import { Exception } from "../../../../../../utilities/exception.js" +import { response } from "../../../../../../utilities/response.js" +import { selectOne } from "../../../../../../utilities/sql/selectOne.js" +import { updateOne } from "../../../../../../utilities/sql/updateOne.js" function getDaysInMonth(date: Date): number { return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 0)).getUTCDate() @@ -27,9 +28,12 @@ function calculateProRataAmountCents(fullMonthlyAmountCents: number, from: Date) export const updateStorageSubscriptionRoute = apiFactory .createApp() .post(updateStorageSubscriptionRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ + const auth = await checkAuthMiddleware({ context: c, }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) const body = await validateBodyMiddleware({ context: c, schema: updateStorageSubscriptionRouteDefinition.schemas.body, @@ -38,7 +42,7 @@ export const updateStorageSubscriptionRoute = apiFactory const organizationUser = await selectOne({ database: c.var.clients.sql, table: models.organizationUser, - where: (table) => and(eq(table.idUser, user.id), eq(table.idOrganization, idOrganization)), + where: (table) => and(eq(table.idUser, auth.user.id), eq(table.idOrganization, idOrganization)), }) if (organizationUser.isAdmin === false) { throw new Exception({ @@ -80,7 +84,7 @@ export const updateStorageSubscriptionRoute = apiFactory await debitOrganizationWallet({ database: c.var.clients.sql, idOrganization, - idUser: user.id, + idUser: auth.user.id, amountHTInCents: proRataAmountCents, description: "Augmentation du stockage (prorata)", serviceType: "storage_gb", @@ -93,7 +97,7 @@ export const updateStorageSubscriptionRoute = apiFactory storageLimit: nextStorageLimit, storageLimitPending: null, lastUpdatedAt: now.toISOString(), - lastUpdatedBy: user.id, + lastUpdatedBy: auth.user.id, }, where: (table) => eq(table.id, idOrganization), }) @@ -107,7 +111,7 @@ export const updateStorageSubscriptionRoute = apiFactory data: { storageLimitPending: pendingValue, lastUpdatedAt: now.toISOString(), - lastUpdatedBy: user.id, + lastUpdatedBy: auth.user.id, }, where: (table) => eq(table.id, idOrganization), }) diff --git a/packages/api/src/routes/auth/$idOrganization/organizationPayment/updateTokensSubscription.ts b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/updateTokensSubscription.ts similarity index 75% rename from packages/api/src/routes/auth/$idOrganization/organizationPayment/updateTokensSubscription.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/updateTokensSubscription.ts index c5261331..30de174e 100644 --- a/packages/api/src/routes/auth/$idOrganization/organizationPayment/updateTokensSubscription.ts +++ b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationPayment/updateTokensSubscription.ts @@ -1,23 +1,33 @@ import { models, updateTokensSubscriptionRouteDefinition } from "@arrhes/application-metadata" import { TOKEN_PACK_PRICE_IN_CENTS } from "@arrhes/application-metadata/utilities" import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { findOrCreateCurrentPeriodInvoice, getCurrentMonthRange } from "../../../../utilities/billing/billingInvoice.js" -import { getTokenAddonQuantity, getTotalTokensFromQuantity } from "../../../../utilities/billing/subscriptionPricing.js" -import { recordOrganizationPayment } from "../../../../utilities/billing/wallet.js" -import { Exception } from "../../../../utilities/exception.js" -import { response } from "../../../../utilities/response.js" -import { selectOne } from "../../../../utilities/sql/selectOne.js" -import { updateOne } from "../../../../utilities/sql/updateOne.js" +import { checkAuthMiddleware } from "../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../utilities/apiFactory.js" +import { + findOrCreateCurrentPeriodInvoice, + getCurrentMonthRange, +} from "../../../../../../utilities/billing/billingInvoice.js" +import { + getTokenAddonQuantity, + getTotalTokensFromQuantity, +} from "../../../../../../utilities/billing/subscriptionPricing.js" +import { recordOrganizationPayment } from "../../../../../../utilities/billing/wallet.js" +import { Exception } from "../../../../../../utilities/exception.js" +import { response } from "../../../../../../utilities/response.js" +import { selectOne } from "../../../../../../utilities/sql/selectOne.js" +import { updateOne } from "../../../../../../utilities/sql/updateOne.js" export const updateTokensSubscriptionRoute = apiFactory .createApp() .post(updateTokensSubscriptionRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ + const auth = await checkAuthMiddleware({ context: c, }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) const body = await validateBodyMiddleware({ context: c, schema: updateTokensSubscriptionRouteDefinition.schemas.body, @@ -26,7 +36,7 @@ export const updateTokensSubscriptionRoute = apiFactory const organizationUser = await selectOne({ database: c.var.clients.sql, table: models.organizationUser, - where: (table) => and(eq(table.idUser, user.id), eq(table.idOrganization, idOrganization)), + where: (table) => and(eq(table.idUser, auth.user.id), eq(table.idOrganization, idOrganization)), }) if (organizationUser.isAdmin === false) { throw new Exception({ @@ -84,7 +94,7 @@ export const updateTokensSubscriptionRoute = apiFactory tokensTotalAvailable: Math.max(nextTokensTotal - organization.tokensTotalUsed, 0), walletBalanceInCents: newWalletBalanceInCents, lastUpdatedAt: now.toISOString(), - lastUpdatedBy: user.id, + lastUpdatedBy: auth.user.id, }, where: (table) => eq(table.id, idOrganization), }) @@ -107,7 +117,7 @@ export const updateTokensSubscriptionRoute = apiFactory description: "Achat tokens Assistant IA", serviceType: "agent_tokens_million", idInvoice, - createdBy: user.id, + createdBy: auth.user.id, }) } }) diff --git a/packages/api/src/routes/auth/$idOrganization/organizationSettings/organizationSettingsRoutes.ts b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationSettingsRoutes.ts similarity index 100% rename from packages/api/src/routes/auth/$idOrganization/organizationSettings/organizationSettingsRoutes.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationSettingsRoutes.ts diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationUser/createOneOrganizationUser.ts b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationUser/createOneOrganizationUser.ts new file mode 100644 index 00000000..1a208f3f --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationUser/createOneOrganizationUser.ts @@ -0,0 +1,86 @@ +import { createOneOrganizationUserRouteDefinition, generateId, models } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../utilities/apiFactory.js" +import { Exception } from "../../../../../../utilities/exception.js" +import { response } from "../../../../../../utilities/response.js" +import { insertOne } from "../../../../../../utilities/sql/insertOne.js" +import { selectOne } from "../../../../../../utilities/sql/selectOne.js" + +export const createOneOrganizationUserRoute = apiFactory + .createApp() + .post(createOneOrganizationUserRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: createOneOrganizationUserRouteDefinition.schemas.body, + }) + + // must be admin of the administration + const organizationUser = await selectOne({ + database: c.var.clients.sql, + table: models.organizationUser, + where: (table) => and(eq(table.idUser, auth.user.id), eq(table.idOrganization, idOrganization)), + }) + if (organizationUser.isAdmin === false) { + throw new Exception({ + statusCode: 401, + internalMessage: "User is not admin of the organization", + externalMessage: "Vous n'êtes pas administrateur de l'organisation", + }) + } + + // the auth.user must exist + const toAddUser = await selectOne({ + database: c.var.clients.sql, + table: models.user, + where: (table) => eq(table.email, body.user.email), + }) + if (toAddUser === undefined) { + throw new Exception({ + statusCode: 404, + internalMessage: "User not found", + externalMessage: "Utilisateur non trouvé", + }) + } + + const createOneOrganizationUser = await insertOne({ + database: c.var.clients.sql, + table: models.organizationUser, + data: { + id: generateId(), + idOrganization: organizationUser.idOrganization, + idUser: toAddUser.id, + isOwner: false, + isAdmin: body.isAdmin, + status: "invited", + createdAt: new Date().toISOString(), + lastUpdatedAt: null, + createdBy: auth.user.id, + lastUpdatedBy: null, + }, + }) + + // await sendEmail({ + // to: updateUser.email, + // subject: "Invitation à collaborer sur Coulba", + // html: invitationTemplate({ + // urlInvitation: `${urlApp}/services/invitation?id=${updateUser.id}&token=${updateUser.invitationToken}`, + // urlWebsite: urlWebsite + // }) + // }) + + return response({ + context: c, + statusCode: 200, + schema: createOneOrganizationUserRouteDefinition.schemas.return, + data: createOneOrganizationUser, + }) + }) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationUser/deleteOneOrganizationUser.ts b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationUser/deleteOneOrganizationUser.ts new file mode 100644 index 00000000..da3c303c --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationUser/deleteOneOrganizationUser.ts @@ -0,0 +1,52 @@ +import { deleteOneOrganizationUserRouteDefinition, models } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../utilities/apiFactory.js" +import { Exception } from "../../../../../../utilities/exception.js" +import { response } from "../../../../../../utilities/response.js" +import { deleteOne } from "../../../../../../utilities/sql/deleteOne.js" +import { selectOne } from "../../../../../../utilities/sql/selectOne.js" + +export const deleteOneOrganizationUserRoute = apiFactory + .createApp() + .post(deleteOneOrganizationUserRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: deleteOneOrganizationUserRouteDefinition.schemas.body, + }) + + // Check if the auth.user is admin of the organization + const organizationUser = await selectOne({ + database: c.var.clients.sql, + table: models.organizationUser, + where: (table) => and(eq(table.idUser, auth.user.id), eq(table.idOrganization, idOrganization)), + }) + if (organizationUser.isAdmin === false) { + throw new Exception({ + statusCode: 401, + internalMessage: "User is not admin of the organization", + externalMessage: "Vous n'êtes pas administrateur de l'organisation", + }) + } + + const deleteOneOrganizationUser = await deleteOne({ + database: c.var.clients.sql, + table: models.organizationUser, + where: (table) => and(eq(table.id, body.idOrganizationUser)), + }) + + return response({ + context: c, + statusCode: 200, + schema: deleteOneOrganizationUserRouteDefinition.schemas.return, + data: deleteOneOrganizationUser, + }) + }) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationUser/organizationUsersRoutes.ts b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationUser/organizationUsersRoutes.ts new file mode 100644 index 00000000..c3b077f3 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationUser/organizationUsersRoutes.ts @@ -0,0 +1,13 @@ +import { createOneOrganizationUserRoute } from "./createOneOrganizationUser.js" +import { deleteOneOrganizationUserRoute } from "./deleteOneOrganizationUser.js" +import { readAllOrganizationUsersRoute } from "./readAllOrganizationUsers.js" +import { readOneOrganizationUserRoute } from "./readOneOrganizationUser.js" +import { updateOneOrganizationUserRoute } from "./updateOneOrganizationUser.js" + +export const organizationUsersRoutes = [ + createOneOrganizationUserRoute, + readAllOrganizationUsersRoute, + deleteOneOrganizationUserRoute, + readOneOrganizationUserRoute, + updateOneOrganizationUserRoute, +] diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationUser/readAllOrganizationUsers.ts b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationUser/readAllOrganizationUsers.ts new file mode 100644 index 00000000..5d870a58 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationUser/readAllOrganizationUsers.ts @@ -0,0 +1,51 @@ +import { models, readAllOrganizationUsersRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../utilities/apiFactory.js" +import { Exception } from "../../../../../../utilities/exception.js" +import { response } from "../../../../../../utilities/response.js" +import { selectOne } from "../../../../../../utilities/sql/selectOne.js" + +export const readAllOrganizationUsersRoute = apiFactory + .createApp() + .post(readAllOrganizationUsersRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const _body = await validateBodyMiddleware({ + context: c, + schema: readAllOrganizationUsersRouteDefinition.schemas.body, + }) + + const organizationUser = await selectOne({ + database: c.var.clients.sql, + table: models.organizationUser, + where: (table) => and(eq(table.idUser, auth.user.id), eq(table.idOrganization, idOrganization)), + }) + if (organizationUser.isAdmin === false) { + throw new Exception({ + statusCode: 401, + internalMessage: "User is not admin of the organization", + externalMessage: "Vous n'êtes pas administrateur de l'organisation", + }) + } + + const readAllOrganizationUsers = await c.var.clients.sql.query.organizationUserModel.findMany({ + where: (table) => and(eq(table.idOrganization, organizationUser.idOrganization)), + with: { + user: true, + }, + }) + + return response({ + context: c, + statusCode: 200, + schema: readAllOrganizationUsersRouteDefinition.schemas.return, + data: readAllOrganizationUsers, + }) + }) diff --git a/packages/api/src/routes/auth/$idOrganization/organizationUser/$idOrganizationUser/readOneOrganizationUser.ts b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationUser/readOneOrganizationUser.ts similarity index 77% rename from packages/api/src/routes/auth/$idOrganization/organizationUser/$idOrganizationUser/readOneOrganizationUser.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationUser/readOneOrganizationUser.ts index a0585bc0..37178e2b 100644 --- a/packages/api/src/routes/auth/$idOrganization/organizationUser/$idOrganizationUser/readOneOrganizationUser.ts +++ b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationUser/readOneOrganizationUser.ts @@ -1,16 +1,16 @@ import { models, readOneOrganizationUserRouteDefinition } from "@arrhes/application-metadata" import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { Exception } from "../../../../../utilities/exception.js" -import { response } from "../../../../../utilities/response.js" -import { selectOne } from "../../../../../utilities/sql/selectOne.js" +import { checkAuthMiddleware } from "../../../../../../middlewares/checkAuthMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../utilities/apiFactory.js" +import { Exception } from "../../../../../../utilities/exception.js" +import { response } from "../../../../../../utilities/response.js" +import { selectOne } from "../../../../../../utilities/sql/selectOne.js" export const readOneOrganizationUserRoute = apiFactory .createApp() .post(readOneOrganizationUserRouteDefinition.path, async (c) => { - await checkUserSessionMiddleware({ + await checkAuthMiddleware({ context: c, }) const body = await validateBodyMiddleware({ diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationUser/updateOneOrganizationUser.ts b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationUser/updateOneOrganizationUser.ts new file mode 100644 index 00000000..2045f5fb --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/organizationUser/updateOneOrganizationUser.ts @@ -0,0 +1,66 @@ +import { models, updateOneOrganizationUserRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../utilities/apiFactory.js" +import { Exception } from "../../../../../../utilities/exception.js" +import { response } from "../../../../../../utilities/response.js" +import { selectOne } from "../../../../../../utilities/sql/selectOne.js" +import { updateOne } from "../../../../../../utilities/sql/updateOne.js" + +export const updateOneOrganizationUserRoute = apiFactory + .createApp() + .post(updateOneOrganizationUserRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: updateOneOrganizationUserRouteDefinition.schemas.body, + }) + + // Check if the auth.user is admin of the organization + const organizationUser = await selectOne({ + database: c.var.clients.sql, + table: models.organizationUser, + where: (table) => and(eq(table.idUser, auth.user.id), eq(table.idOrganization, idOrganization)), + }) + if (organizationUser.isAdmin === false) { + throw new Exception({ + statusCode: 401, + internalMessage: "User is not admin of the organization", + externalMessage: "Vous n'êtes pas administrateur de l'organisation", + }) + } + + if (body.idOrganizationUser === organizationUser.id) { + if (body.isAdmin === false) { + throw new Exception({ + statusCode: 400, + internalMessage: "User cannot update himself", + externalMessage: "Vous ne pouvez pas vous modifier vous-même", + }) + } + } + const updateOneOrganizationUser = await updateOne({ + database: c.var.clients.sql, + table: models.organizationUser, + data: { + isAdmin: body.isAdmin, + lastUpdatedAt: new Date().toISOString(), + lastUpdatedBy: auth.user.id, + }, + where: (table) => eq(table.id, body.idOrganizationUser), + }) + + return response({ + context: c, + statusCode: 200, + schema: updateOneOrganizationUserRouteDefinition.schemas.return, + data: updateOneOrganizationUser, + }) + }) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/updateOneOrganization.ts b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/updateOneOrganization.ts new file mode 100644 index 00000000..82dfafb9 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/organizationSettings/updateOneOrganization.ts @@ -0,0 +1,58 @@ +import { models, updateOneOrganizationRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../utilities/apiFactory.js" +import { Exception } from "../../../../../utilities/exception.js" +import { response } from "../../../../../utilities/response.js" +import { selectOne } from "../../../../../utilities/sql/selectOne.js" +import { updateOne } from "../../../../../utilities/sql/updateOne.js" + +export const updateOneOrganizationRoute = apiFactory + .createApp() + .post(updateOneOrganizationRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: updateOneOrganizationRouteDefinition.schemas.body, + }) + + const organizationUser = await selectOne({ + database: c.var.clients.sql, + table: models.organizationUser, + where: (table) => and(eq(table.idUser, auth.user.id), eq(table.idOrganization, idOrganization)), + }) + if (organizationUser.isAdmin === false) { + throw new Exception({ + statusCode: 401, + internalMessage: "User is not admin of the organization", + externalMessage: "Vous n'êtes pas administrateur de l'organisation", + }) + } + + const updateOneOrganization = await updateOne({ + database: c.var.clients.sql, + table: models.organization, + data: { + siren: body.siren, + name: body.name, + email: body.email, + lastUpdatedAt: new Date().toISOString(), + lastUpdatedBy: auth.user.id, + }, + where: (table) => eq(table.id, organizationUser.idOrganization), + }) + + return response({ + context: c, + statusCode: 200, + schema: updateOneOrganizationRouteDefinition.schemas.return, + data: updateOneOrganization, + }) + }) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/readOneOrganization.ts b/packages/api/src/routes/auth/organizations/$idOrganization/readOneOrganization.ts new file mode 100644 index 00000000..943dd457 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/readOneOrganization.ts @@ -0,0 +1,51 @@ +import { models, readOneOrganizationRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../utilities/apiFactory.js" +import { Exception } from "../../../../utilities/exception.js" +import { response } from "../../../../utilities/response.js" +import { selectOne } from "../../../../utilities/sql/selectOne.js" + +export const readOneOrganizationRoute = apiFactory + .createApp() + .post(readOneOrganizationRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: readOneOrganizationRouteDefinition.schemas.body, + }) + + if (body.idOrganization !== idOrganization) { + throw new Exception({ + statusCode: 403, + internalMessage: "Body organization id does not match active organization", + externalMessage: "L'organisation demandée ne correspond pas à l'organisation active.", + }) + } + + const organizationUser = await selectOne({ + database: c.var.clients.sql, + table: models.organizationUser, + where: (table) => and(eq(table.idOrganization, body.idOrganization), eq(table.idUser, auth.user.id)), + }) + + const readOneOrganization = await selectOne({ + database: c.var.clients.sql, + table: models.organization, + where: (table) => eq(table.id, organizationUser.idOrganization), + }) + + return response({ + context: c, + statusCode: 200, + schema: readOneOrganizationRouteDefinition.schemas.return, + data: readOneOrganization, + }) + }) diff --git a/packages/api/src/routes/auth/$idYear/$idYearRoutes.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/$idYearRoutes.ts similarity index 84% rename from packages/api/src/routes/auth/$idYear/$idYearRoutes.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/$idYearRoutes.ts index 1606b7c4..57b12191 100644 --- a/packages/api/src/routes/auth/$idYear/$idYearRoutes.ts +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/$idYearRoutes.ts @@ -1,16 +1,16 @@ import { entriesRoutes } from "./entries/entriesRoutes.js" +import { exportsRoutes } from "./exports/exportsRoutes.js" import { filesRoutes } from "./files/filesRoutes.js" import { foldersRoutes } from "./folders/foldersRoutes.js" import { readOneYearRoute } from "./readOneYear.js" -import { reportsRoutes } from "./reports/reportsRoutes.js" import { yearSettingsRoute } from "./yearSettings/yearSettingsRoute.js" export const $idYearRoutes = [ readOneYearRoute, ...entriesRoutes, + ...exportsRoutes, ...filesRoutes, ...foldersRoutes, - ...reportsRoutes, ...yearSettingsRoute, ] diff --git a/packages/api/src/routes/auth/$idYear/entries/$idEntry/$idEntryRoutes.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/$idEntryRoutes.ts similarity index 100% rename from packages/api/src/routes/auth/$idYear/entries/$idEntry/$idEntryRoutes.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/$idEntryRoutes.ts diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/deleteOneEntry.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/deleteOneEntry.ts new file mode 100644 index 00000000..25b1d45b --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/deleteOneEntry.ts @@ -0,0 +1,35 @@ +import { deleteOneEntryRouteDefinition, models } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../utilities/response.js" +import { deleteOne } from "../../../../../../../../utilities/sql/deleteOne.js" + +export const deleteOneEntryRoute = registerRoute(deleteOneEntryRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: deleteOneEntryRouteDefinition.schemas.body, + }) + + const deleteOneEntry = await deleteOne({ + database: c.var.clients.sql, + table: models.entry, + where: (table) => + and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear), eq(table.id, body.idEntry)), + }) + + return response({ + context: c, + statusCode: 200, + schema: deleteOneEntryRouteDefinition.schemas.return, + data: deleteOneEntry, + }) +}) diff --git a/packages/api/src/routes/auth/$idYear/entries/$idEntry/duplicateOneEntry.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/duplicateOneEntry.ts similarity index 77% rename from packages/api/src/routes/auth/$idYear/entries/$idEntry/duplicateOneEntry.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/duplicateOneEntry.ts index 65fca814..49a4a29f 100644 --- a/packages/api/src/routes/auth/$idYear/entries/$idEntry/duplicateOneEntry.ts +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/duplicateOneEntry.ts @@ -1,18 +1,22 @@ import { duplicateOneEntryRouteDefinition, generateId, models } from "@arrhes/application-metadata" import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { response } from "../../../../../utilities/response.js" -import { insertMany } from "../../../../../utilities/sql/insertMany.js" -import { insertOne } from "../../../../../utilities/sql/insertOne.js" -import { selectMany } from "../../../../../utilities/sql/selectMany.js" -import { selectOne } from "../../../../../utilities/sql/selectOne.js" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../utilities/response.js" +import { insertMany } from "../../../../../../../../utilities/sql/insertMany.js" +import { insertOne } from "../../../../../../../../utilities/sql/insertOne.js" +import { selectMany } from "../../../../../../../../utilities/sql/selectMany.js" +import { selectOne } from "../../../../../../../../utilities/sql/selectOne.js" -export const duplicateOneEntryRoute = apiFactory.createApp().post(duplicateOneEntryRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ +export const duplicateOneEntryRoute = registerRoute(duplicateOneEntryRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ context: c, }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) const body = await validateBodyMiddleware({ context: c, schema: duplicateOneEntryRouteDefinition.schemas.body, @@ -54,7 +58,7 @@ export const duplicateOneEntryRoute = apiFactory.createApp().post(duplicateOneEn date: originalEntry.date, createdAt: new Date().toISOString(), lastUpdatedAt: null, - createdBy: user.id, + createdBy: auth.user.id, lastUpdatedBy: null, }, }) @@ -78,7 +82,7 @@ export const duplicateOneEntryRoute = apiFactory.createApp().post(duplicateOneEn credit: entryLine.credit, createdAt: new Date().toISOString(), lastUpdatedAt: null, - createdBy: user.id, + createdBy: auth.user.id, lastUpdatedBy: null, })), }) diff --git a/packages/api/src/routes/auth/$idYear/entries/$idEntry/entryLines/$idEntryLine/$idEntryLineRoutes.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/$idEntryLine/$idEntryLineRoutes.ts similarity index 100% rename from packages/api/src/routes/auth/$idYear/entries/$idEntry/entryLines/$idEntryLine/$idEntryLineRoutes.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/$idEntryLine/$idEntryLineRoutes.ts diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/$idEntryLine/deleteOneEntryLine.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/$idEntryLine/deleteOneEntryLine.ts new file mode 100644 index 00000000..1a6d91ce --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/$idEntryLine/deleteOneEntryLine.ts @@ -0,0 +1,41 @@ +import { deleteOneEntryLineRouteDefinition, models } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../../../../../utilities/response.js" +import { deleteOne } from "../../../../../../../../../../utilities/sql/deleteOne.js" + +export const deleteOneEntryLineRoute = apiFactory + .createApp() + .post(deleteOneEntryLineRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: deleteOneEntryLineRouteDefinition.schemas.body, + }) + + const deleteOneEntryLine = await deleteOne({ + database: c.var.clients.sql, + table: models.entryLine, + where: (table) => + and( + eq(table.idOrganization, idOrganization), + eq(table.idYear, body.idYear), + eq(table.id, body.idEntryLine), + ), + }) + + return response({ + context: c, + statusCode: 200, + schema: deleteOneEntryLineRouteDefinition.schemas.return, + data: deleteOneEntryLine, + }) + }) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/$idEntryLine/readOneEntryLine.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/$idEntryLine/readOneEntryLine.ts new file mode 100644 index 00000000..2925ca9c --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/$idEntryLine/readOneEntryLine.ts @@ -0,0 +1,39 @@ +import { models, readOneEntryLineRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../../../utilities/response.js" +import { selectOne } from "../../../../../../../../../../utilities/sql/selectOne.js" + +export const readOneEntryLineRoute = registerRoute(readOneEntryLineRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: readOneEntryLineRouteDefinition.schemas.body, + }) + + const readOneEntryLine = await selectOne({ + database: c.var.clients.sql, + table: models.entryLine, + where: (table) => + and( + eq(table.idOrganization, idOrganization), + eq(table.idYear, body.idYear), + eq(table.id, body.idEntryLine), + ), + }) + + return response({ + context: c, + statusCode: 200, + schema: readOneEntryLineRouteDefinition.schemas.return, + data: readOneEntryLine, + }) +}) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/$idEntryLine/updateOneEntryLine.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/$idEntryLine/updateOneEntryLine.ts new file mode 100644 index 00000000..8cae690f --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/$idEntryLine/updateOneEntryLine.ts @@ -0,0 +1,55 @@ +import { models, updateOneEntryLineRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../../../../../utilities/response.js" +import { updateOne } from "../../../../../../../../../../utilities/sql/updateOne.js" + +export const updateOneEntryLineRoute = apiFactory + .createApp() + .post(updateOneEntryLineRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: updateOneEntryLineRouteDefinition.schemas.body, + }) + + const updatedEntryLine = await updateOne({ + database: c.var.clients.sql, + table: models.entryLine, + data: { + idEntry: body.idEntry, + idAccount: body.idAccount, + isComputedForJournalReport: body.isComputedForJournalReport, + isComputedForLedgerReport: body.isComputedForLedgerReport, + isComputedForBalanceReport: body.isComputedForBalanceReport, + isComputedForBalanceSheetReport: body.isComputedForBalanceSheetReport, + isComputedForIncomeStatementReport: body.isComputedForIncomeStatementReport, + label: body.label, + debit: body.debit, + credit: body.credit, + lastUpdatedAt: new Date().toISOString(), + lastUpdatedBy: auth.user.id, + }, + where: (table) => + and( + eq(table.idOrganization, idOrganization), + eq(table.idYear, body.idYear), + eq(table.id, body.idEntryLine), + ), + }) + + return response({ + context: c, + statusCode: 200, + schema: updateOneEntryLineRouteDefinition.schemas.return, + data: updatedEntryLine, + }) + }) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/createOneEntryLine.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/createOneEntryLine.ts new file mode 100644 index 00000000..a843b516 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/createOneEntryLine.ts @@ -0,0 +1,66 @@ +import { createOneEntryLineRouteDefinition, generateId, models } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../../../../utilities/response.js" +import { insertOne } from "../../../../../../../../../utilities/sql/insertOne.js" +import { selectOne } from "../../../../../../../../../utilities/sql/selectOne.js" + +export const createOneEntryLineRoute = apiFactory + .createApp() + .post(createOneEntryLineRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: createOneEntryLineRouteDefinition.schemas.body, + }) + + const readOneEntry = await selectOne({ + database: c.var.clients.sql, + table: models.entry, + where: (table) => + and( + eq(table.idOrganization, idOrganization), + eq(table.idYear, body.idYear), + eq(table.id, body.idEntry), + ), + }) + + const createOneEntryLine = await insertOne({ + database: c.var.clients.sql, + table: models.entryLine, + data: { + id: generateId(), + idOrganization: idOrganization, + idYear: body.idYear, + idEntry: body.idEntry, + idAccount: body.idAccount, + isComputedForJournalReport: body.isComputedForJournalReport, + isComputedForLedgerReport: body.isComputedForLedgerReport, + isComputedForBalanceReport: body.isComputedForBalanceReport, + isComputedForBalanceSheetReport: body.isComputedForBalanceSheetReport, + isComputedForIncomeStatementReport: body.isComputedForIncomeStatementReport, + label: body.label ?? readOneEntry.label, + debit: body.debit ?? "0.00", + credit: body.credit ?? "0.00", + createdAt: new Date().toISOString(), + lastUpdatedAt: null, + createdBy: auth.user.id, + lastUpdatedBy: null, + }, + }) + + return response({ + context: c, + statusCode: 200, + schema: createOneEntryLineRouteDefinition.schemas.return, + data: createOneEntryLine, + }) + }) diff --git a/packages/api/src/routes/auth/$idYear/entries/$idEntry/entryLines/entryLinesRoutes.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/entryLinesRoutes.ts similarity index 100% rename from packages/api/src/routes/auth/$idYear/entries/$idEntry/entryLines/entryLinesRoutes.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/entryLinesRoutes.ts diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/readAllEntryLines.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/readAllEntryLines.ts new file mode 100644 index 00000000..935bbbe0 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/readAllEntryLines.ts @@ -0,0 +1,43 @@ +import { models, readAllEntryLinesRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../../utilities/response.js" +import { selectMany } from "../../../../../../../../../utilities/sql/selectMany.js" + +export const readAllEntryLinesRoute = registerRoute(readAllEntryLinesRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: readAllEntryLinesRouteDefinition.schemas.body, + }) + + const readAllEntryLines = await selectMany({ + database: c.var.clients.sql, + table: models.entryLine, + where: (table) => { + if (body.idEntry !== null && body.idEntry !== undefined) { + return and( + eq(table.idOrganization, idOrganization), + eq(table.idYear, body.idYear), + eq(table.idEntry, body.idEntry), + ) + } + return and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear)) + }, + }) + + return response({ + context: c, + statusCode: 200, + schema: readAllEntryLinesRouteDefinition.schemas.return, + data: readAllEntryLines, + }) +}) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/updateManyEntryLines.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/updateManyEntryLines.ts new file mode 100644 index 00000000..e6b90d9f --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/updateManyEntryLines.ts @@ -0,0 +1,71 @@ +import { models, updateManyEntryLinesRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../../../../utilities/response.js" +import { selectMany } from "../../../../../../../../../utilities/sql/selectMany.js" +import { updateOne } from "../../../../../../../../../utilities/sql/updateOne.js" + +export const updateManyEntryLinesRoute = apiFactory + .createApp() + .post(updateManyEntryLinesRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: updateManyEntryLinesRouteDefinition.schemas.body, + }) + + const readAllEntryLines = await selectMany({ + database: c.var.clients.sql, + table: models.entryLine, + where: (table) => + and( + eq(table.idOrganization, idOrganization), + eq(table.idYear, body.idYear), + eq(table.idEntry, body.idEntry), + ), + }) + + const updatedEntryLines = await c.var.clients.sql.transaction(async (_tx) => { + const entryLines = [] + for (const entryLine of readAllEntryLines) { + const updatedEntryLine = await updateOne({ + database: c.var.clients.sql, + table: models.entryLine, + data: { + isComputedForJournalReport: body.isComputedForJournalReport, + isComputedForLedgerReport: body.isComputedForLedgerReport, + isComputedForBalanceReport: body.isComputedForBalanceReport, + isComputedForBalanceSheetReport: body.isComputedForBalanceSheetReport, + isComputedForIncomeStatementReport: body.isComputedForIncomeStatementReport, + label: body.label, + lastUpdatedAt: new Date().toISOString(), + lastUpdatedBy: auth.user.id, + }, + where: (table) => + and( + eq(table.idOrganization, idOrganization), + eq(table.idYear, body.idYear), + eq(table.id, entryLine.id), + ), + }) + entryLines.push(updatedEntryLine) + } + + return entryLines + }) + + return response({ + context: c, + statusCode: 200, + schema: updateManyEntryLinesRouteDefinition.schemas.return, + data: updatedEntryLines, + }) + }) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryTags/addOneEntryTag.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryTags/addOneEntryTag.ts new file mode 100644 index 00000000..d69bb416 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryTags/addOneEntryTag.ts @@ -0,0 +1,58 @@ +import { addOneEntryTagRouteDefinition, generateId, models } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../../utilities/response.js" +import { insertOne } from "../../../../../../../../../utilities/sql/insertOne.js" +import { selectOne } from "../../../../../../../../../utilities/sql/selectOne.js" + +export const addOneEntryTagRoute = registerRoute(addOneEntryTagRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: addOneEntryTagRouteDefinition.schemas.body, + }) + + // Verify entry exists + await selectOne({ + database: c.var.clients.sql, + table: models.entry, + where: (table) => + and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear), eq(table.id, body.idEntry)), + }) + + // Verify tag exists + await selectOne({ + database: c.var.clients.sql, + table: models.tag, + where: (table) => + and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear), eq(table.id, body.idTag)), + }) + + const addOneEntryTag = await insertOne({ + database: c.var.clients.sql, + table: models.entryTag, + data: { + id: generateId(), + idOrganization: idOrganization, + idYear: body.idYear, + idEntry: body.idEntry, + idTag: body.idTag, + createdAt: new Date().toISOString(), + }, + }) + + return response({ + context: c, + statusCode: 200, + schema: addOneEntryTagRouteDefinition.schemas.return, + data: addOneEntryTag, + }) +}) diff --git a/packages/api/src/routes/auth/$idYear/entries/$idEntry/entryTags/entryTagsRoutes.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryTags/entryTagsRoutes.ts similarity index 100% rename from packages/api/src/routes/auth/$idYear/entries/$idEntry/entryTags/entryTagsRoutes.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryTags/entryTagsRoutes.ts diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryTags/removeOneEntryTag.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryTags/removeOneEntryTag.ts new file mode 100644 index 00000000..f26571cd --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryTags/removeOneEntryTag.ts @@ -0,0 +1,35 @@ +import { models, removeOneEntryTagRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../../utilities/response.js" +import { deleteOne } from "../../../../../../../../../utilities/sql/deleteOne.js" + +export const removeOneEntryTagRoute = registerRoute(removeOneEntryTagRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: removeOneEntryTagRouteDefinition.schemas.body, + }) + + const removeOneEntryTag = await deleteOne({ + database: c.var.clients.sql, + table: models.entryTag, + where: (table) => + and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear), eq(table.id, body.idEntryTag)), + }) + + return response({ + context: c, + statusCode: 200, + schema: removeOneEntryTagRouteDefinition.schemas.return, + data: removeOneEntryTag, + }) +}) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/readOneEntry.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/readOneEntry.ts new file mode 100644 index 00000000..a8a90a44 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/readOneEntry.ts @@ -0,0 +1,35 @@ +import { models, readOneEntryRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../utilities/response.js" +import { selectOne } from "../../../../../../../../utilities/sql/selectOne.js" + +export const readOneEntryRoute = registerRoute(readOneEntryRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: readOneEntryRouteDefinition.schemas.body, + }) + + const readOneEntry = await selectOne({ + database: c.var.clients.sql, + table: models.entry, + where: (table) => + and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear), eq(table.id, body.idEntry)), + }) + + return response({ + context: c, + statusCode: 200, + schema: readOneEntryRouteDefinition.schemas.return, + data: readOneEntry, + }) +}) diff --git a/packages/api/src/routes/auth/$idYear/entries/$idEntry/reverseOneEntry.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/reverseOneEntry.ts similarity index 77% rename from packages/api/src/routes/auth/$idYear/entries/$idEntry/reverseOneEntry.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/reverseOneEntry.ts index 69884bc0..bfd911dc 100644 --- a/packages/api/src/routes/auth/$idYear/entries/$idEntry/reverseOneEntry.ts +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/reverseOneEntry.ts @@ -1,18 +1,22 @@ import { generateId, models, reverseOneEntryRouteDefinition } from "@arrhes/application-metadata" import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { response } from "../../../../../utilities/response.js" -import { insertMany } from "../../../../../utilities/sql/insertMany.js" -import { insertOne } from "../../../../../utilities/sql/insertOne.js" -import { selectMany } from "../../../../../utilities/sql/selectMany.js" -import { selectOne } from "../../../../../utilities/sql/selectOne.js" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../utilities/response.js" +import { insertMany } from "../../../../../../../../utilities/sql/insertMany.js" +import { insertOne } from "../../../../../../../../utilities/sql/insertOne.js" +import { selectMany } from "../../../../../../../../utilities/sql/selectMany.js" +import { selectOne } from "../../../../../../../../utilities/sql/selectOne.js" -export const reverseOneEntryRoute = apiFactory.createApp().post(reverseOneEntryRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ +export const reverseOneEntryRoute = registerRoute(reverseOneEntryRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ context: c, }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) const body = await validateBodyMiddleware({ context: c, schema: reverseOneEntryRouteDefinition.schemas.body, @@ -54,7 +58,7 @@ export const reverseOneEntryRoute = apiFactory.createApp().post(reverseOneEntryR date: originalEntry.date, createdAt: new Date().toISOString(), lastUpdatedAt: null, - createdBy: user.id, + createdBy: auth.user.id, lastUpdatedBy: null, }, }) @@ -78,7 +82,7 @@ export const reverseOneEntryRoute = apiFactory.createApp().post(reverseOneEntryR credit: entryLine.debit, createdAt: new Date().toISOString(), lastUpdatedAt: null, - createdBy: user.id, + createdBy: auth.user.id, lastUpdatedBy: null, })), }) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/updateOneEntry.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/updateOneEntry.ts new file mode 100644 index 00000000..185fc2a5 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/updateOneEntry.ts @@ -0,0 +1,51 @@ +import { models, updateOneEntryRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../utilities/response.js" +import { updateOne } from "../../../../../../../../utilities/sql/updateOne.js" + +export const updateOneEntryRoute = registerRoute(updateOneEntryRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: updateOneEntryRouteDefinition.schemas.body, + }) + + const updatedEntry = await c.var.clients.sql.transaction(async (tx) => { + const updatedEntry = await updateOne({ + database: tx, + table: models.entry, + data: { + idJournal: body.idJournal, + idFile: body.idFile, + label: body.label, + date: body.date, + lastUpdatedAt: new Date().toISOString(), + lastUpdatedBy: auth.user.id, + }, + where: (table) => + and( + eq(table.idOrganization, idOrganization), + eq(table.idYear, body.idYear), + eq(table.id, body.idEntry), + ), + }) + + return updatedEntry + }) + + return response({ + context: c, + statusCode: 200, + schema: updateOneEntryRouteDefinition.schemas.return, + data: updatedEntry, + }) +}) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/createOneEntry.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/createOneEntry.ts new file mode 100644 index 00000000..de1a102e --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/createOneEntry.ts @@ -0,0 +1,45 @@ +import { createOneEntryRouteDefinition, generateId, models } from "@arrhes/application-metadata" +import { checkAuthMiddleware } from "../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../utilities/response.js" +import { insertOne } from "../../../../../../../utilities/sql/insertOne.js" + +export const createOneEntryRoute = registerRoute(createOneEntryRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: createOneEntryRouteDefinition.schemas.body, + }) + + const createOneEntry = await insertOne({ + database: c.var.clients.sql, + table: models.entry, + data: { + id: generateId(), + idOrganization: idOrganization, + idYear: body.idYear, + idJournal: body.idJournal, + idFile: body.idFile, + label: body.label, + date: body.date, + createdAt: new Date().toISOString(), + lastUpdatedAt: null, + createdBy: auth.user.id, + lastUpdatedBy: null, + }, + }) + + return response({ + context: c, + statusCode: 200, + schema: createOneEntryRouteDefinition.schemas.return, + data: createOneEntry, + }) +}) diff --git a/packages/api/src/routes/auth/$idYear/entries/createOneEntryFromTemplate.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/createOneEntryFromTemplate.ts similarity index 75% rename from packages/api/src/routes/auth/$idYear/entries/createOneEntryFromTemplate.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/createOneEntryFromTemplate.ts index f8e8f79a..40f1e7b0 100644 --- a/packages/api/src/routes/auth/$idYear/entries/createOneEntryFromTemplate.ts +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/createOneEntryFromTemplate.ts @@ -1,17 +1,21 @@ import { createOneEntryFromTemplateRouteDefinition, generateId, models } from "@arrhes/application-metadata" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { response } from "../../../../utilities/response.js" -import { insertMany } from "../../../../utilities/sql/insertMany.js" -import { insertOne } from "../../../../utilities/sql/insertOne.js" +import { checkAuthMiddleware } from "../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../../utilities/response.js" +import { insertMany } from "../../../../../../../utilities/sql/insertMany.js" +import { insertOne } from "../../../../../../../utilities/sql/insertOne.js" export const createOneEntryFromTemplateRoute = apiFactory .createApp() .post(createOneEntryFromTemplateRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ + const auth = await checkAuthMiddleware({ context: c, }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) const body = await validateBodyMiddleware({ context: c, schema: createOneEntryFromTemplateRouteDefinition.schemas.body, @@ -31,7 +35,7 @@ export const createOneEntryFromTemplateRoute = apiFactory date: body.date, createdAt: new Date().toISOString(), lastUpdatedAt: null, - createdBy: user.id, + createdBy: auth.user.id, lastUpdatedBy: null, }, }) @@ -56,7 +60,7 @@ export const createOneEntryFromTemplateRoute = apiFactory credit: line.credit ?? "0.00", createdAt: new Date().toISOString(), lastUpdatedAt: null, - createdBy: user.id, + createdBy: auth.user.id, lastUpdatedBy: null, })), }) diff --git a/packages/api/src/routes/auth/$idYear/entries/entriesRoutes.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/entriesRoutes.ts similarity index 100% rename from packages/api/src/routes/auth/$idYear/entries/entriesRoutes.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/entriesRoutes.ts diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/readAllEntries.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/readAllEntries.ts new file mode 100644 index 00000000..fda96c6d --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/readAllEntries.ts @@ -0,0 +1,34 @@ +import { models, readAllEntriesRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../utilities/response.js" +import { selectMany } from "../../../../../../../utilities/sql/selectMany.js" + +export const readAllEntriesRoute = registerRoute(readAllEntriesRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: readAllEntriesRouteDefinition.schemas.body, + }) + + const readAllEntries = await selectMany({ + database: c.var.clients.sql, + table: models.entry, + where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear)), + }) + + return response({ + context: c, + statusCode: 200, + schema: readAllEntriesRouteDefinition.schemas.return, + data: readAllEntries, + }) +}) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/readAllEntryTags.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/readAllEntryTags.ts new file mode 100644 index 00000000..d6700ec0 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/entries/readAllEntryTags.ts @@ -0,0 +1,34 @@ +import { models, readAllEntryTagsRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../utilities/response.js" +import { selectMany } from "../../../../../../../utilities/sql/selectMany.js" + +export const readAllEntryTagsRoute = registerRoute(readAllEntryTagsRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: readAllEntryTagsRouteDefinition.schemas.body, + }) + + const readAllEntryTags = await selectMany({ + database: c.var.clients.sql, + table: models.entryTag, + where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear)), + }) + + return response({ + context: c, + statusCode: 200, + schema: readAllEntryTagsRouteDefinition.schemas.return, + data: readAllEntryTags, + }) +}) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/exports/exportsRoutes.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/exports/exportsRoutes.ts new file mode 100644 index 00000000..3686ce6b --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/exports/exportsRoutes.ts @@ -0,0 +1,9 @@ +import { generateBalanceSheetXmlRoute } from "./generateBalanceSheetXml.js" +import { generateFecRoute } from "./generateFec.js" +import { generateIncomeStatementXmlRoute } from "./generateIncomeStatementXml.js" + +export const exportsRoutes = [ + generateBalanceSheetXmlRoute, + generateFecRoute, + generateIncomeStatementXmlRoute, +] diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/exports/generateBalanceSheetXml.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/exports/generateBalanceSheetXml.ts new file mode 100644 index 00000000..3c06e24a --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/exports/generateBalanceSheetXml.ts @@ -0,0 +1,333 @@ +import { generateBalanceSheetXmlRouteDefinition, models } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../../utilities/response.js" +import { selectMany } from "../../../../../../../utilities/sql/selectMany.js" +import { selectOne } from "../../../../../../../utilities/sql/selectOne.js" +import { generateGetSignedUrl } from "../../../../../../../utilities/storage/generateGetSignedUrl.js" +import { putObject } from "../../../../../../../utilities/storage/putObject.js" + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function getAllDescendants( + root: { + id: string + }, + all: Array<{ + id: string + idBalanceSheetParent: string | null + }>, +): Array<{ + id: string + idBalanceSheetParent: string | null +}> { + const direct = all.filter((bs) => bs.idBalanceSheetParent === root.id) + return direct.flatMap((child) => [ + child, + ...getAllDescendants(child, all), + ]) +} + +type Account = (typeof models.account)["$inferSelect"] +type EntryLine = (typeof models.entryLine)["$inferSelect"] +type BalanceSheet = (typeof models.balanceSheet)["$inferSelect"] + +function computeAssetRow( + row: BalanceSheet, + all: BalanceSheet[], + accounts: Account[], + entryLines: EntryLine[], +): { + gross: number + amortization: number + net: number +} { + const descendants = getAllDescendants(row, all) + let gross = 0 + let amortization = 0 + + accounts + .filter((a) => a.idBalanceSheetAsset === row.id || descendants.some((d) => d.id === a.idBalanceSheetAsset)) + .forEach((account) => { + let debit = 0 + let credit = 0 + entryLines + .filter((el) => el.idAccount === account.id) + .forEach((el) => { + debit += Number(el.debit) + credit += Number(el.credit) + }) + const balance = debit - credit + if (balance < 0 && account.balanceSheetAssetFlow === "debit") return + if (balance > 0 && account.balanceSheetAssetFlow === "credit") return + const signed = account.balanceSheetAssetFlow === "debit" ? Math.abs(balance) : -Math.abs(balance) + if (account.balanceSheetAssetColumn === "gross") gross += signed + if (account.balanceSheetAssetColumn === "amortization") amortization += signed + }) + + return { + gross, + amortization, + net: gross + amortization, + } +} + +function computeLiabilityRow( + row: BalanceSheet, + all: BalanceSheet[], + accounts: Account[], + entryLines: EntryLine[], +): number { + const descendants = getAllDescendants(row, all) + let net = 0 + + accounts + .filter( + (a) => a.idBalanceSheetLiability === row.id || descendants.some((d) => d.id === a.idBalanceSheetLiability), + ) + .forEach((account) => { + let debit = 0 + let credit = 0 + entryLines + .filter((el) => el.idAccount === account.id) + .forEach((el) => { + debit += Number(el.debit) + credit += Number(el.credit) + }) + const balance = credit - debit + if (balance > 0 && account.balanceSheetLiabilityFlow === "debit") return + if (balance < 0 && account.balanceSheetLiabilityFlow === "credit") return + net += balance + }) + + return net +} + +/** Sanitize a string into a valid XML local name segment (no spaces, no special chars) */ +function toXmlName(s: string): string { + return s + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .replace(/[^a-zA-Z0-9]/g, "_") + .replace(/^_+/, "") + .replace(/_+/g, "_") +} + +function fmt(n: number): string { + return n.toFixed(2) +} + +// --------------------------------------------------------------------------- +// XBRL builder +// --------------------------------------------------------------------------- + +function buildBalanceSheetXbrl(params: { + organizationName: string + siren: string | null + startingAt: string + endingAt: string + accounts: Account[] + entryLines: EntryLine[] + balanceSheets: BalanceSheet[] +}): string { + const { organizationName, siren, startingAt, endingAt, accounts, entryLines, balanceSheets } = params + + const entityId = siren ?? organizationName + const entityScheme = siren ? "http://www.insee.fr/siren" : "http://www.arrhes.fr/organization" + + const endDate = endingAt.slice(0, 10) + const startDate = startingAt.slice(0, 10) + const contextId = `D-${startDate}_${endDate}` + + const facts: string[] = [] + + // Assets + const assetRows = balanceSheets.filter((bs) => bs.side === "asset") + let totalGross = 0 + let totalAmort = 0 + let totalNet = 0 + + for (const row of assetRows) { + const { gross, amortization, net } = computeAssetRow(row, balanceSheets, accounts, entryLines) + const key = `Actif${row.number}_${toXmlName(row.label)}` + facts.push(` `) + facts.push( + ` ${fmt(gross)}`, + ) + facts.push( + ` ${fmt(Math.abs(amortization))}`, + ) + facts.push( + ` ${fmt(net)}`, + ) + if (row.idBalanceSheetParent === null) { + totalGross += gross + totalAmort += amortization + totalNet += net + } + } + + facts.push(` `) + facts.push( + ` ${fmt(totalGross)}`, + ) + facts.push( + ` ${fmt(Math.abs(totalAmort))}`, + ) + facts.push( + ` ${fmt(totalNet)}`, + ) + + // Liabilities + const liabilityRows = balanceSheets.filter((bs) => bs.side === "liability") + let totalLiabilityNet = 0 + + for (const row of liabilityRows) { + const net = computeLiabilityRow(row, balanceSheets, accounts, entryLines) + const key = `Passif${row.number}_${toXmlName(row.label)}` + facts.push(` `) + facts.push( + ` ${fmt(net)}`, + ) + if (row.idBalanceSheetParent === null) totalLiabilityNet += net + } + + facts.push(` `) + facts.push( + ` ${fmt(totalLiabilityNet)}`, + ) + + return ` + + + + + + + + + ${entityId} + + + ${endDate} + + + + + iso4217:EUR + + +${facts.join("\n")} + + +` +} + +// --------------------------------------------------------------------------- +// Route +// --------------------------------------------------------------------------- + +export const generateBalanceSheetXmlRoute = apiFactory + .createApp() + .post(generateBalanceSheetXmlRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: generateBalanceSheetXmlRouteDefinition.schemas.body, + }) + + const [year, organization, entryLines, accounts, balanceSheets] = await Promise.all([ + selectOne({ + database: c.var.clients.sql, + table: models.year, + where: (t) => and(eq(t.idOrganization, idOrganization), eq(t.id, body.idYear)), + }), + selectOne({ + database: c.var.clients.sql, + table: models.organization, + where: (t) => eq(t.id, idOrganization), + }), + selectMany({ + database: c.var.clients.sql, + table: models.entryLine, + where: (t) => + and( + eq(t.idOrganization, idOrganization), + eq(t.idYear, body.idYear), + eq(t.isComputedForBalanceSheetReport, true), + ), + }), + selectMany({ + database: c.var.clients.sql, + table: models.account, + where: (t) => and(eq(t.idOrganization, idOrganization), eq(t.idYear, body.idYear)), + }), + selectMany({ + database: c.var.clients.sql, + table: models.balanceSheet, + where: (t) => and(eq(t.idOrganization, idOrganization), eq(t.idYear, body.idYear)), + }), + ]) + + const xml = buildBalanceSheetXbrl({ + organizationName: organization.name, + siren: organization.siren, + startingAt: year.startingAt, + endingAt: year.endingAt, + accounts, + entryLines, + balanceSheets, + }) + + const xmlBuffer = Buffer.from(xml, "utf-8") + const storageKey = `organizations/${idOrganization}/${body.idYear}/exports/balance-sheet.xml` + + await putObject({ + var: c.var, + body: xmlBuffer, + storageKey, + contentType: "application/xml", + contentLength: xmlBuffer.length, + metadata: { + idOrganization, + idYear: body.idYear, + idUser: auth.user.id, + }, + }) + + const url = await generateGetSignedUrl({ + var: c.var, + storageKey, + }) + + return response({ + context: c, + statusCode: 200, + schema: generateBalanceSheetXmlRouteDefinition.schemas.return, + data: { + url, + }, + }) + }) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/exports/generateFec.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/exports/generateFec.ts new file mode 100644 index 00000000..ea9d4ae6 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/exports/generateFec.ts @@ -0,0 +1,236 @@ +import { generateFecRouteDefinition, models } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../../utilities/response.js" +import { selectMany } from "../../../../../../../utilities/sql/selectMany.js" +import { selectOne } from "../../../../../../../utilities/sql/selectOne.js" +import { generateGetSignedUrl } from "../../../../../../../utilities/storage/generateGetSignedUrl.js" +import { putObject } from "../../../../../../../utilities/storage/putObject.js" + +// --------------------------------------------------------------------------- +// FEC helpers +// --------------------------------------------------------------------------- + +/** + * Format a date string as YYYYMMDD for FEC compliance. + */ +function formatFecDate(rawDate: string | Date): string { + const date = new Date(rawDate) + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, "0") + const day = String(date.getDate()).padStart(2, "0") + return `${year}${month}${day}` +} + +/** + * Format a numeric amount for FEC: comma decimal separator, no thousands, 2 decimals. + * e.g. 1234.5 -> "1234,50" + */ +function formatFecAmount(value: number | string): string { + const num = Number(value) + const abs = Math.abs(num) < 0.009 ? 0 : num + return abs.toFixed(2).replace(".", ",") +} + +const FEC_HEADERS = [ + "JournalCode", + "JournalLib", + "EcritureNum", + "EcritureDate", + "CompteNum", + "CompteLib", + "CompAuxNum", + "CompAuxLib", + "PieceRef", + "PieceDate", + "EcritureLib", + "Debit", + "Credit", + "EcritureLet", + "DateLet", + "ValidDate", + "Montantdevise", + "Idevise", +] as const + +// --------------------------------------------------------------------------- +// Route +// --------------------------------------------------------------------------- + +export const generateFecRoute = apiFactory.createApp().post(generateFecRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: generateFecRouteDefinition.schemas.body, + }) + + const [organization, entries, entryLines, accounts, journals] = await Promise.all([ + selectOne({ + database: c.var.clients.sql, + table: models.organization, + where: (t) => eq(t.id, idOrganization), + }), + selectMany({ + database: c.var.clients.sql, + table: models.entry, + where: (t) => and(eq(t.idOrganization, idOrganization), eq(t.idYear, body.idYear)), + }), + selectMany({ + database: c.var.clients.sql, + table: models.entryLine, + where: (t) => and(eq(t.idOrganization, idOrganization), eq(t.idYear, body.idYear)), + }), + selectMany({ + database: c.var.clients.sql, + table: models.account, + where: (t) => and(eq(t.idOrganization, idOrganization), eq(t.idYear, body.idYear)), + }), + selectMany({ + database: c.var.clients.sql, + table: models.journal, + where: (t) => eq(t.idOrganization, idOrganization), + }), + ]) + + const accountsMap = new Map( + accounts.map((a) => [ + a.id, + { + number: a.number, + label: a.label, + }, + ]), + ) + const journalsMap = new Map( + journals.map((j) => [ + j.id, + { + code: j.code, + label: j.label, + }, + ]), + ) + const entriesMap = new Map( + entries.map((e) => [ + e.id, + e, + ]), + ) + + // Build sequential EcritureNum per entry, sorted by date then id + const sortedEntries = [ + ...entries, + ].sort((a, b) => { + const dateCompare = a.date.localeCompare(b.date) + if (dateCompare !== 0) return dateCompare + return a.id.localeCompare(b.id) + }) + const ecritureNumMap = new Map() + for (let i = 0; i < sortedEntries.length; i++) { + ecritureNumMap.set(sortedEntries[i].id, String(i + 1)) + } + + // Sort rows by entry date, then EcritureNum, then row id + const sortedRows = [ + ...entryLines, + ].sort((a, b) => { + const entryA = entriesMap.get(a.idEntry) + const entryB = entriesMap.get(b.idEntry) + if (!entryA || !entryB) return 0 + const dateCompare = entryA.date.localeCompare(entryB.date) + if (dateCompare !== 0) return dateCompare + const numCompare = (ecritureNumMap.get(a.idEntry) ?? "").localeCompare( + ecritureNumMap.get(b.idEntry) ?? "", + undefined, + { + numeric: true, + }, + ) + if (numCompare !== 0) return numCompare + return a.id.localeCompare(b.id) + }) + + const rows = sortedRows + .map((row) => { + const entry = entriesMap.get(row.idEntry) + if (!entry) return null + + const account = accountsMap.get(row.idAccount) + const journal = entry.idJournal ? journalsMap.get(entry.idJournal) : null + + const ecritureDate = formatFecDate(entry.date) + const pieceDate = formatFecDate(entry.date) + const validDate = formatFecDate(entry.createdAt) + + return [ + journal?.code ?? "", + journal?.label ?? "", + ecritureNumMap.get(entry.id) ?? "", + ecritureDate, + account?.number ?? "", + account?.label ?? "", + "", // CompAuxNum - not supported + "", // CompAuxLib - not supported + entry.label, // PieceRef + pieceDate, + row.label ?? entry.label, // EcritureLib + formatFecAmount(row.debit), + formatFecAmount(row.credit), + "", // EcritureLet - not supported + "", // DateLet - not supported + validDate, + "", // Montantdevise + "", // Idevise + ].join("\t") + }) + .filter((row): row is string => row !== null) + + const BOM = "\uFEFF" + const fecContent = + BOM + + [ + FEC_HEADERS.join("\t"), + ...rows, + ].join("\n") + + const siren = organization.siren ?? "" + const filename = `${siren}FEC${formatFecDate(new Date())}.txt` + const fecBuffer = Buffer.from(fecContent, "utf-8") + const storageKey = `organizations/${idOrganization}/${body.idYear}/exports/fec.txt` + + await putObject({ + var: c.var, + body: fecBuffer, + storageKey, + contentType: "text/plain;charset=utf-8", + contentLength: fecBuffer.length, + metadata: { + idOrganization, + idYear: body.idYear, + idUser: auth.user.id, + filename, + }, + }) + + const url = await generateGetSignedUrl({ + var: c.var, + storageKey, + }) + + return response({ + context: c, + statusCode: 200, + schema: generateFecRouteDefinition.schemas.return, + data: { + url, + }, + }) +}) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/exports/generateIncomeStatementXml.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/exports/generateIncomeStatementXml.ts new file mode 100644 index 00000000..69d94348 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/exports/generateIncomeStatementXml.ts @@ -0,0 +1,291 @@ +import { generateIncomeStatementXmlRouteDefinition, models } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../../utilities/response.js" +import { selectMany } from "../../../../../../../utilities/sql/selectMany.js" +import { selectOne } from "../../../../../../../utilities/sql/selectOne.js" +import { generateGetSignedUrl } from "../../../../../../../utilities/storage/generateGetSignedUrl.js" +import { putObject } from "../../../../../../../utilities/storage/putObject.js" + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +type Account = (typeof models.account)["$inferSelect"] +type EntryLine = (typeof models.entryLine)["$inferSelect"] +type IncomeStatement = (typeof models.incomeStatement)["$inferSelect"] +type Computation = (typeof models.computation)["$inferSelect"] +type ComputationIncomeStatement = (typeof models.computationIncomeStatement)["$inferSelect"] + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** Sanitize a string into a valid XML local name segment */ +function toXmlName(s: string): string { + return s + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .replace(/[^a-zA-Z0-9]/g, "_") + .replace(/^_+/, "") + .replace(/_+/g, "_") +} + +function fmt(n: number): string { + return n.toFixed(2) +} + +function computeIncomeStatementAmounts( + incomeStatements: IncomeStatement[], + accounts: Account[], + entryLines: EntryLine[], +): Map { + const amounts = new Map() + for (const row of incomeStatements) { + let amount = 0 + accounts + .filter((a) => a.idIncomeStatement === row.id) + .forEach((account) => { + let debit = 0 + let credit = 0 + entryLines + .filter((el) => el.idAccount === account.id) + .forEach((el) => { + debit += Number(el.debit) + credit += Number(el.credit) + }) + amount += Math.abs(debit - credit) + }) + amounts.set(row.id, amount) + } + return amounts +} + +function computeComputationAmounts( + computations: Computation[], + computationIncomeStatements: ComputationIncomeStatement[], + incomeStatementAmounts: Map, +): Map { + const amounts = new Map() + for (const computation of computations) { + let amount = 0 + computationIncomeStatements + .filter((cis) => cis.idComputation === computation.id) + .forEach((cis) => { + const lineAmount = incomeStatementAmounts.get(cis.idIncomeStatement) ?? 0 + amount += cis.operation === "plus" ? Math.abs(lineAmount) : -Math.abs(lineAmount) + }) + amounts.set(computation.id, amount) + } + return amounts +} + +// --------------------------------------------------------------------------- +// XBRL builder +// --------------------------------------------------------------------------- + +function buildIncomeStatementXbrl(params: { + organizationName: string + siren: string | null + startingAt: string + endingAt: string + incomeStatements: IncomeStatement[] + computations: Computation[] + computationIncomeStatements: ComputationIncomeStatement[] + accounts: Account[] + entryLines: EntryLine[] +}): string { + const { + organizationName, + siren, + startingAt, + endingAt, + incomeStatements, + computations, + computationIncomeStatements, + accounts, + entryLines, + } = params + + const entityId = siren ?? organizationName + const entityScheme = siren ? "http://www.insee.fr/siren" : "http://www.arrhes.fr/organization" + + const startDate = startingAt.slice(0, 10) + const endDate = endingAt.slice(0, 10) + const contextId = `D-${startDate}_${endDate}` + + const incomeStatementAmounts = computeIncomeStatementAmounts(incomeStatements, accounts, entryLines) + const computationAmounts = computeComputationAmounts( + computations, + computationIncomeStatements, + incomeStatementAmounts, + ) + + const facts: string[] = [] + + // Income statement regular lines + for (const row of incomeStatements) { + const amount = incomeStatementAmounts.get(row.id) ?? 0 + const key = `CR${row.number}_${toXmlName(row.label)}` + facts.push(` `) + facts.push( + ` ${fmt(amount)}`, + ) + } + + // Computation rows (soldes intermédiaires de gestion) + for (const computation of computations) { + const amount = computationAmounts.get(computation.id) ?? 0 + const key = `Solde${computation.number}_${toXmlName(computation.label)}` + facts.push(` `) + facts.push( + ` ${fmt(amount)}`, + ) + } + + return ` + + + + + + + + + ${entityId} + + + ${startDate} + ${endDate} + + + + + iso4217:EUR + + +${facts.join("\n")} + + +` +} + +// --------------------------------------------------------------------------- +// Route +// --------------------------------------------------------------------------- + +export const generateIncomeStatementXmlRoute = apiFactory + .createApp() + .post(generateIncomeStatementXmlRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: generateIncomeStatementXmlRouteDefinition.schemas.body, + }) + + const [year, organization, entryLines, accounts, incomeStatements, computations, computationIncomeStatements] = + await Promise.all([ + selectOne({ + database: c.var.clients.sql, + table: models.year, + where: (t) => and(eq(t.idOrganization, idOrganization), eq(t.id, body.idYear)), + }), + selectOne({ + database: c.var.clients.sql, + table: models.organization, + where: (t) => eq(t.id, idOrganization), + }), + selectMany({ + database: c.var.clients.sql, + table: models.entryLine, + where: (t) => + and( + eq(t.idOrganization, idOrganization), + eq(t.idYear, body.idYear), + eq(t.isComputedForIncomeStatementReport, true), + ), + }), + selectMany({ + database: c.var.clients.sql, + table: models.account, + where: (t) => and(eq(t.idOrganization, idOrganization), eq(t.idYear, body.idYear)), + }), + selectMany({ + database: c.var.clients.sql, + table: models.incomeStatement, + where: (t) => and(eq(t.idOrganization, idOrganization), eq(t.idYear, body.idYear)), + }), + selectMany({ + database: c.var.clients.sql, + table: models.computation, + where: (t) => and(eq(t.idOrganization, idOrganization), eq(t.idYear, body.idYear)), + }), + selectMany({ + database: c.var.clients.sql, + table: models.computationIncomeStatement, + where: (t) => and(eq(t.idOrganization, idOrganization), eq(t.idYear, body.idYear)), + }), + ]) + + const xml = buildIncomeStatementXbrl({ + organizationName: organization.name, + siren: organization.siren, + startingAt: year.startingAt, + endingAt: year.endingAt, + incomeStatements, + computations, + computationIncomeStatements, + accounts, + entryLines, + }) + + const xmlBuffer = Buffer.from(xml, "utf-8") + const storageKey = `organizations/${idOrganization}/${body.idYear}/exports/income-statement.xml` + + await putObject({ + var: c.var, + body: xmlBuffer, + storageKey, + contentType: "application/xml", + contentLength: xmlBuffer.length, + metadata: { + idOrganization, + idYear: body.idYear, + idUser: auth.user.id, + }, + }) + + const url = await generateGetSignedUrl({ + var: c.var, + storageKey, + }) + + return response({ + context: c, + statusCode: 200, + schema: generateIncomeStatementXmlRouteDefinition.schemas.return, + data: { + url, + }, + }) + }) diff --git a/packages/api/src/routes/auth/$idYear/files/$idFile/$idFileRoutes.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/$idFile/$idFileRoutes.ts similarity index 100% rename from packages/api/src/routes/auth/$idYear/files/$idFile/$idFileRoutes.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/$idFile/$idFileRoutes.ts diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/$idFile/deleteOneFile.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/$idFile/deleteOneFile.ts new file mode 100644 index 00000000..bd2a317c --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/$idFile/deleteOneFile.ts @@ -0,0 +1,56 @@ +import { deleteOneFileRouteDefinition, models } from "@arrhes/application-metadata" +import { and, eq, sql } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../utilities/response.js" +import { deleteOne } from "../../../../../../../../utilities/sql/deleteOne.js" +import { selectOne } from "../../../../../../../../utilities/sql/selectOne.js" +import { updateOne } from "../../../../../../../../utilities/sql/updateOne.js" + +export const deleteOneFileRoute = registerRoute(deleteOneFileRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: deleteOneFileRouteDefinition.schemas.body, + }) + + const deletedFile = await c.var.clients.sql.transaction(async (tx) => { + const readOneFile = await selectOne({ + database: tx, + table: models.file, + where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.id, body.idFile)), + }) + + if (readOneFile.size !== null && readOneFile.size > 0) { + await updateOne({ + database: tx, + table: models.organization, + data: { + storageCurrentUsage: sql`GREATEST(${models.organization.storageCurrentUsage} - ${readOneFile.size}, 0)`, + }, + where: (table) => eq(table.id, idOrganization), + }) + } + + const deleteOneFile = await deleteOne({ + database: tx, + table: models.file, + where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.id, body.idFile)), + }) + return deleteOneFile + }) + + return response({ + context: c, + statusCode: 200, + schema: deleteOneFileRouteDefinition.schemas.return, + data: deletedFile, + }) +}) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/$idFile/finalizeFileUpload.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/$idFile/finalizeFileUpload.ts new file mode 100644 index 00000000..8e66cd4c --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/$idFile/finalizeFileUpload.ts @@ -0,0 +1,61 @@ +import { finalizeFileUploadRouteDefinition, models } from "@arrhes/application-metadata" +import { HeadObjectCommand } from "@aws-sdk/client-s3" +import { and, eq, sql } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../../../utilities/response.js" +import { updateOne } from "../../../../../../../../utilities/sql/updateOne.js" + +export const finalizeFileUploadRoute = apiFactory + .createApp() + .post(finalizeFileUploadRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: finalizeFileUploadRouteDefinition.schemas.body, + }) + + const storageKey = `organizations/${idOrganization}/storage/${body.idFile}` + const storageHead = await c.var.clients.storage.send( + new HeadObjectCommand({ + Bucket: c.var.env.STORAGE_BUCKET_NAME, + Key: storageKey, + }), + ) + + const updateOneFile = await updateOne({ + database: c.var.clients.sql, + table: models.file, + data: { + storageKey: storageKey, + type: storageHead.ContentType, + size: storageHead.ContentLength, + lastUpdatedAt: new Date().toISOString(), + lastUpdatedBy: auth.user.id, + }, + where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.id, body.idFile)), + }) + + await updateOne({ + database: c.var.clients.sql, + table: models.organization, + data: { + storageCurrentUsage: sql`${models.organization.storageCurrentUsage} + ${storageHead.ContentLength}`, + }, + where: (table) => eq(table.id, idOrganization), + }) + + return response({ + context: c, + statusCode: 200, + schema: finalizeFileUploadRouteDefinition.schemas.return, + data: updateOneFile, + }) + }) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/$idFile/generateFileDeleteSignedUrl.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/$idFile/generateFileDeleteSignedUrl.ts new file mode 100644 index 00000000..5ec33cd3 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/$idFile/generateFileDeleteSignedUrl.ts @@ -0,0 +1,53 @@ +import { generateFileDeleteSignedUrlRouteDefinition, models } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../utilities/apiFactory.js" +import { Exception } from "../../../../../../../../utilities/exception.js" +import { response } from "../../../../../../../../utilities/response.js" +import { selectOne } from "../../../../../../../../utilities/sql/selectOne.js" +import { generateDeleteSignedUrl } from "../../../../../../../../utilities/storage/generateDeleteSignedUrl.js" + +export const generateFileDeleteSignedUrlRoute = apiFactory + .createApp() + .post(generateFileDeleteSignedUrlRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: generateFileDeleteSignedUrlRouteDefinition.schemas.body, + }) + + const readOneFile = await selectOne({ + database: c.var.clients.sql, + table: models.file, + where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.id, body.idFile)), + }) + + if (readOneFile.storageKey === null) { + throw new Exception({ + internalMessage: "File storage key not found", + statusCode: 400, + externalMessage: "Le fichier associé n'existe pas", + }) + } + + const url = await generateDeleteSignedUrl({ + var: c.var, + storageKey: readOneFile.storageKey, + }) + + return response({ + context: c, + statusCode: 200, + schema: generateFileDeleteSignedUrlRouteDefinition.schemas.return, + data: { + url, + }, + }) + }) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/$idFile/generateFileGetSignedUrl.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/$idFile/generateFileGetSignedUrl.ts new file mode 100644 index 00000000..215d6840 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/$idFile/generateFileGetSignedUrl.ts @@ -0,0 +1,53 @@ +import { generateFileGetSignedUrlRouteDefinition, models } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../utilities/apiFactory.js" +import { Exception } from "../../../../../../../../utilities/exception.js" +import { response } from "../../../../../../../../utilities/response.js" +import { selectOne } from "../../../../../../../../utilities/sql/selectOne.js" +import { generateGetSignedUrl } from "../../../../../../../../utilities/storage/generateGetSignedUrl.js" + +export const generateFileGetSignedUrlRoute = apiFactory + .createApp() + .post(generateFileGetSignedUrlRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: generateFileGetSignedUrlRouteDefinition.schemas.body, + }) + + const readOneFile = await selectOne({ + database: c.var.clients.sql, + table: models.file, + where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.id, body.idFile)), + }) + + if (readOneFile.storageKey === null) { + throw new Exception({ + internalMessage: "File storage key not found", + statusCode: 400, + externalMessage: "Le fichier associé n'existe pas", + }) + } + + const url = await generateGetSignedUrl({ + var: c.var, + storageKey: readOneFile.storageKey, + }) + + return response({ + context: c, + statusCode: 200, + schema: generateFileGetSignedUrlRouteDefinition.schemas.return, + data: { + url, + }, + }) + }) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/$idFile/generateFilePutSignedUrl.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/$idFile/generateFilePutSignedUrl.ts new file mode 100644 index 00000000..079caca5 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/$idFile/generateFilePutSignedUrl.ts @@ -0,0 +1,77 @@ +import { generateFilePutSignedUrlRouteDefinition, models } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../utilities/apiFactory.js" +import { Exception } from "../../../../../../../../utilities/exception.js" +import { response } from "../../../../../../../../utilities/response.js" +import { selectOne } from "../../../../../../../../utilities/sql/selectOne.js" +import { generatePutSignedUrl } from "../../../../../../../../utilities/storage/generatePutSignedUrl.js" + +export const generateFilePutSignedUrlRoute = apiFactory + .createApp() + .post(generateFilePutSignedUrlRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: generateFilePutSignedUrlRouteDefinition.schemas.body, + }) + + if (body.size > 50_000_000) { + throw new Exception({ + internalMessage: "File size is too big", + statusCode: 400, + externalMessage: "Fichier trop volumineux", + }) + } + + const readOneFile = await selectOne({ + database: c.var.clients.sql, + table: models.file, + where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.id, body.idFile)), + }) + + const organization = await selectOne({ + database: c.var.clients.sql, + table: models.organization, + where: (table) => eq(table.id, idOrganization), + }) + + if (organization.storageCurrentUsage + body.size > organization.storageLimit) { + throw new Exception({ + internalMessage: "Storage limit exceeded", + statusCode: 400, + externalMessage: "Limite de stockage atteinte", + }) + } + + const storageKey = `organizations/${idOrganization}/storage/${body.idFile}` + + const url = await generatePutSignedUrl({ + var: c.var, + storageKey: storageKey, + contentLength: body.size, + contentType: body.type, + metadata: { + idOrganization: idOrganization, + idFile: readOneFile.id, + idUser: auth.user.id, + }, + }) + + return response({ + context: c, + statusCode: 200, + schema: generateFilePutSignedUrlRouteDefinition.schemas.return, + data: { + file: readOneFile, + url: url, + }, + }) + }) diff --git a/packages/api/src/routes/auth/$idYear/files/$idFile/ocrFile.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/$idFile/ocrFile.ts similarity index 78% rename from packages/api/src/routes/auth/$idYear/files/$idFile/ocrFile.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/$idFile/ocrFile.ts index 96e77332..2ae9d70e 100644 --- a/packages/api/src/routes/auth/$idYear/files/$idFile/ocrFile.ts +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/$idFile/ocrFile.ts @@ -1,16 +1,17 @@ import { generateId, models, ocrFileRouteDefinition } from "@arrhes/application-metadata" -import { and, eq, isNull, sql } from "drizzle-orm" -import { checkOrganizationSubscriptionSessionMiddleware } from "../../../../../middlewares/checkOrganizationSubscriptionSessionMiddleware.js" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { Exception } from "../../../../../utilities/exception.js" -import { response } from "../../../../../utilities/response.js" -import { insertOne } from "../../../../../utilities/sql/insertOne.js" -import { selectOne } from "../../../../../utilities/sql/selectOne.js" -import { updateOne } from "../../../../../utilities/sql/updateOne.js" -import { getObject } from "../../../../../utilities/storage/getObject.js" -import { putObject } from "../../../../../utilities/storage/putObject.js" +import { and, eq, sql } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { checkOrganizationSubscriptionSessionMiddleware } from "../../../../../../../../middlewares/checkOrganizationSubscriptionSessionMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { Exception } from "../../../../../../../../utilities/exception.js" +import { registerRoute } from "../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../utilities/response.js" +import { insertOne } from "../../../../../../../../utilities/sql/insertOne.js" +import { selectOne } from "../../../../../../../../utilities/sql/selectOne.js" +import { updateOne } from "../../../../../../../../utilities/sql/updateOne.js" +import { getObject } from "../../../../../../../../utilities/storage/getObject.js" +import { putObject } from "../../../../../../../../utilities/storage/putObject.js" function fixCommonMojibake(text: string) { // Heuristic: repair common UTF-8 text interpreted as Latin-1 (e.g. "Numéro" -> "Numéro"). @@ -22,10 +23,13 @@ function fixCommonMojibake(text: string) { return repaired.includes("�") ? text : repaired } -export const ocrFileRoute = apiFactory.createApp().post(ocrFileRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ +export const ocrFileRoute = registerRoute(ocrFileRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ context: c, }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) const body = await validateBodyMiddleware({ context: c, schema: ocrFileRouteDefinition.schemas.body, @@ -45,12 +49,7 @@ export const ocrFileRoute = apiFactory.createApp().post(ocrFileRouteDefinition.p const sourceFile = await selectOne({ database: c.var.clients.sql, table: models.file, - where: (table) => - and( - eq(table.idOrganization, idOrganization), - body.idYear !== null ? eq(table.idYear, body.idYear) : isNull(table.idYear), - eq(table.id, body.idFile), - ), + where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.id, body.idFile)), }) if (sourceFile.storageKey === null) { @@ -166,10 +165,10 @@ export const ocrFileRoute = apiFactory.createApp().post(ocrFileRouteDefinition.p const markdownBuffer = Buffer.from(normalizedMarkdownContent, "utf-8") const newFileId = generateId() - const originalName = sourceFile.name ?? sourceFile.reference ?? "document" + const originalName = sourceFile.name || sourceFile.reference || "document" const baseName = originalName.replace(/\.[^.]+$/, "") const markdownName = `${baseName}.md` - const storageKey = `organizations/${idOrganization}/${body.idYear}/files/${newFileId}` + const storageKey = `organizations/${idOrganization}/storage/${newFileId}` const newFile = await insertOne({ database: c.var.clients.sql, @@ -177,7 +176,6 @@ export const ocrFileRoute = apiFactory.createApp().post(ocrFileRouteDefinition.p data: { id: newFileId, idOrganization: idOrganization, - idYear: body.idYear, idFolder: sourceFile.idFolder, reference: sourceFile.reference, name: markdownName, @@ -185,7 +183,7 @@ export const ocrFileRoute = apiFactory.createApp().post(ocrFileRouteDefinition.p type: "text/markdown", size: markdownBuffer.length, createdAt: new Date().toISOString(), - createdBy: user.id, + createdBy: auth.user.id, }, }) @@ -196,12 +194,7 @@ export const ocrFileRoute = apiFactory.createApp().post(ocrFileRouteDefinition.p contentType: "text/markdown; charset=utf-8", metadata: { idOrganization: idOrganization, - ...(body.idYear !== null - ? { - idYear: body.idYear, - } - : {}), - idUser: user.id, + idUser: auth.user.id, }, body: markdownBuffer, }) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/$idFile/readOneFile.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/$idFile/readOneFile.ts new file mode 100644 index 00000000..94a05cc3 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/$idFile/readOneFile.ts @@ -0,0 +1,34 @@ +import { models, readOneFileRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../utilities/response.js" +import { selectOne } from "../../../../../../../../utilities/sql/selectOne.js" + +export const readOneFileRoute = registerRoute(readOneFileRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: readOneFileRouteDefinition.schemas.body, + }) + + const readOneFile = await selectOne({ + database: c.var.clients.sql, + table: models.file, + where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.id, body.idFile)), + }) + + return response({ + context: c, + statusCode: 200, + schema: readOneFileRouteDefinition.schemas.return, + data: readOneFile, + }) +}) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/$idFile/updateOneFile.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/$idFile/updateOneFile.ts new file mode 100644 index 00000000..53c98eba --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/$idFile/updateOneFile.ts @@ -0,0 +1,42 @@ +import { models, updateOneFileRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../utilities/response.js" +import { updateOne } from "../../../../../../../../utilities/sql/updateOne.js" + +export const updateOneFileRoute = registerRoute(updateOneFileRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: updateOneFileRouteDefinition.schemas.body, + }) + + const updateOneFile = await updateOne({ + database: c.var.clients.sql, + table: models.file, + data: { + reference: body.reference, + name: body.name, + date: body.date, + idFolder: body.idFolder, + lastUpdatedAt: new Date().toISOString(), + lastUpdatedBy: auth.user.id, + }, + where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.id, body.idFile)), + }) + + return response({ + context: c, + statusCode: 200, + schema: updateOneFileRouteDefinition.schemas.return, + data: updateOneFile, + }) +}) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/createOneFile.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/createOneFile.ts new file mode 100644 index 00000000..de5a9fc8 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/createOneFile.ts @@ -0,0 +1,47 @@ +import { createOneFileRouteDefinition, generateId, models } from "@arrhes/application-metadata" +import { checkAuthMiddleware } from "../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../utilities/response.js" +import { insertOne } from "../../../../../../../utilities/sql/insertOne.js" + +export const createOneFileRoute = registerRoute(createOneFileRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: createOneFileRouteDefinition.schemas.body, + }) + + const createOneFile = await insertOne({ + database: c.var.clients.sql, + table: models.file, + data: { + id: generateId(), + idOrganization: idOrganization, + idFolder: body.idFolder, + reference: body.reference, + name: body.name, + storageKey: null, + type: null, + size: null, + hash: body.hash, + createdAt: new Date().toISOString(), + lastUpdatedAt: null, + createdBy: auth.user.id, + lastUpdatedBy: null, + }, + }) + + return response({ + context: c, + statusCode: 200, + schema: createOneFileRouteDefinition.schemas.return, + data: createOneFile, + }) +}) diff --git a/packages/api/src/routes/auth/$idYear/files/filesRoutes.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/filesRoutes.ts similarity index 100% rename from packages/api/src/routes/auth/$idYear/files/filesRoutes.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/filesRoutes.ts diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/readAllFiles.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/readAllFiles.ts new file mode 100644 index 00000000..590d5421 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/files/readAllFiles.ts @@ -0,0 +1,34 @@ +import { models, readAllFilesRouteDefinition } from "@arrhes/application-metadata" +import { eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../utilities/response.js" +import { selectMany } from "../../../../../../../utilities/sql/selectMany.js" + +export const readAllFilesRoute = registerRoute(readAllFilesRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + await validateBodyMiddleware({ + context: c, + schema: readAllFilesRouteDefinition.schemas.body, + }) + + const readAllFiles = await selectMany({ + database: c.var.clients.sql, + table: models.file, + where: (table) => eq(table.idOrganization, idOrganization), + }) + + return response({ + context: c, + statusCode: 200, + schema: readAllFilesRouteDefinition.schemas.return, + data: readAllFiles, + }) +}) diff --git a/packages/api/src/routes/auth/$idYear/folders/$idFolder/$idFolderRoutes.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/folders/$idFolder/$idFolderRoutes.ts similarity index 100% rename from packages/api/src/routes/auth/$idYear/folders/$idFolder/$idFolderRoutes.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/folders/$idFolder/$idFolderRoutes.ts diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/folders/$idFolder/deleteOneFolder.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/folders/$idFolder/deleteOneFolder.ts new file mode 100644 index 00000000..32bc66b4 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/folders/$idFolder/deleteOneFolder.ts @@ -0,0 +1,34 @@ +import { deleteOneFolderRouteDefinition, models } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../utilities/response.js" +import { deleteOne } from "../../../../../../../../utilities/sql/deleteOne.js" + +export const deleteOneFolderRoute = registerRoute(deleteOneFolderRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: deleteOneFolderRouteDefinition.schemas.body, + }) + + const deleteOneFolder = await deleteOne({ + database: c.var.clients.sql, + table: models.folder, + where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.id, body.idFolder)), + }) + + return response({ + context: c, + statusCode: 200, + schema: deleteOneFolderRouteDefinition.schemas.return, + data: deleteOneFolder, + }) +}) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/folders/$idFolder/readOneFolder.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/folders/$idFolder/readOneFolder.ts new file mode 100644 index 00000000..f3b41ad3 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/folders/$idFolder/readOneFolder.ts @@ -0,0 +1,34 @@ +import { models, readOneFolderRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../utilities/response.js" +import { selectOne } from "../../../../../../../../utilities/sql/selectOne.js" + +export const readOneFolderRoute = registerRoute(readOneFolderRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: readOneFolderRouteDefinition.schemas.body, + }) + + const readOneFolder = await selectOne({ + database: c.var.clients.sql, + table: models.folder, + where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.id, body.idFolder)), + }) + + return response({ + context: c, + statusCode: 200, + schema: readOneFolderRouteDefinition.schemas.return, + data: readOneFolder, + }) +}) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/folders/$idFolder/updateOneFolder.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/folders/$idFolder/updateOneFolder.ts new file mode 100644 index 00000000..087ace3f --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/folders/$idFolder/updateOneFolder.ts @@ -0,0 +1,40 @@ +import { models, updateOneFolderRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../utilities/response.js" +import { updateOne } from "../../../../../../../../utilities/sql/updateOne.js" + +export const updateOneFolderRoute = registerRoute(updateOneFolderRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: updateOneFolderRouteDefinition.schemas.body, + }) + + const updateOneFolder = await updateOne({ + database: c.var.clients.sql, + table: models.folder, + data: { + name: body.name, + idFolderParent: body.idFolderParent, + lastUpdatedAt: new Date().toISOString(), + lastUpdatedBy: auth.user.id, + }, + where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.id, body.idFolder)), + }) + + return response({ + context: c, + statusCode: 200, + schema: updateOneFolderRouteDefinition.schemas.return, + data: updateOneFolder, + }) +}) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/folders/createOneFolder.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/folders/createOneFolder.ts new file mode 100644 index 00000000..5ef7b50c --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/folders/createOneFolder.ts @@ -0,0 +1,42 @@ +import { createOneFolderRouteDefinition, generateId, models } from "@arrhes/application-metadata" +import { checkAuthMiddleware } from "../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../utilities/response.js" +import { insertOne } from "../../../../../../../utilities/sql/insertOne.js" + +export const createOneFolderRoute = registerRoute(createOneFolderRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: createOneFolderRouteDefinition.schemas.body, + }) + + const createOneFolder = await insertOne({ + database: c.var.clients.sql, + table: models.folder, + data: { + id: generateId(), + idOrganization: idOrganization, + idFolderParent: body.idFolderParent ?? null, + name: body.name, + createdAt: new Date().toISOString(), + lastUpdatedAt: null, + createdBy: auth.user.id, + lastUpdatedBy: null, + }, + }) + + return response({ + context: c, + statusCode: 200, + schema: createOneFolderRouteDefinition.schemas.return, + data: createOneFolder, + }) +}) diff --git a/packages/api/src/routes/auth/$idYear/folders/foldersRoutes.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/folders/foldersRoutes.ts similarity index 100% rename from packages/api/src/routes/auth/$idYear/folders/foldersRoutes.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/folders/foldersRoutes.ts diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/folders/readAllFolders.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/folders/readAllFolders.ts new file mode 100644 index 00000000..e87efd9e --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/folders/readAllFolders.ts @@ -0,0 +1,34 @@ +import { models, readAllFoldersRouteDefinition } from "@arrhes/application-metadata" +import { eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../utilities/response.js" +import { selectMany } from "../../../../../../../utilities/sql/selectMany.js" + +export const readAllFoldersRoute = registerRoute(readAllFoldersRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const _body = await validateBodyMiddleware({ + context: c, + schema: readAllFoldersRouteDefinition.schemas.body, + }) + + const readAllFolders = await selectMany({ + database: c.var.clients.sql, + table: models.folder, + where: (table) => eq(table.idOrganization, idOrganization), + }) + + return response({ + context: c, + statusCode: 200, + schema: readAllFoldersRouteDefinition.schemas.return, + data: readAllFolders, + }) +}) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/readOneYear.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/readOneYear.ts new file mode 100644 index 00000000..9966c11c --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/readOneYear.ts @@ -0,0 +1,34 @@ +import { models, readOneYearRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../utilities/response.js" +import { selectOne } from "../../../../../../utilities/sql/selectOne.js" + +export const readOneYearRoute = registerRoute(readOneYearRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: readOneYearRouteDefinition.schemas.body, + }) + + const readOneYear = await selectOne({ + database: c.var.clients.sql, + table: models.year, + where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.id, body.idYear)), + }) + + return response({ + context: c, + statusCode: 200, + schema: readOneYearRouteDefinition.schemas.return, + data: readOneYear, + }) +}) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/accounts/$idAccount/$idAccountRoutes.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/$idAccountRoutes.ts similarity index 100% rename from packages/api/src/routes/auth/$idYear/yearSettings/accounts/$idAccount/$idAccountRoutes.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/$idAccountRoutes.ts diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/deleteOneAccount.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/deleteOneAccount.ts new file mode 100644 index 00000000..6f2acbeb --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/deleteOneAccount.ts @@ -0,0 +1,35 @@ +import { deleteOneAccountRouteDefinition, models } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../../utilities/response.js" +import { deleteOne } from "../../../../../../../../../utilities/sql/deleteOne.js" + +export const deleteOneAccountRoute = registerRoute(deleteOneAccountRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: deleteOneAccountRouteDefinition.schemas.body, + }) + + const deleteOneAccount = await deleteOne({ + database: c.var.clients.sql, + table: models.account, + where: (table) => + and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear), eq(table.id, body.idAccount)), + }) + + return response({ + context: c, + statusCode: 200, + schema: deleteOneAccountRouteDefinition.schemas.return, + data: deleteOneAccount, + }) +}) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/readOneAccount.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/readOneAccount.ts new file mode 100644 index 00000000..bc22bb33 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/readOneAccount.ts @@ -0,0 +1,35 @@ +import { models, readOneAccountRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../../utilities/response.js" +import { selectOne } from "../../../../../../../../../utilities/sql/selectOne.js" + +export const readOneAccountRoute = registerRoute(readOneAccountRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: readOneAccountRouteDefinition.schemas.body, + }) + + const readOneAccount = await selectOne({ + database: c.var.clients.sql, + table: models.account, + where: (table) => + and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear), eq(table.id, body.idAccount)), + }) + + return response({ + context: c, + statusCode: 200, + schema: readOneAccountRouteDefinition.schemas.return, + data: readOneAccount, + }) +}) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/updateOneAccount.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/updateOneAccount.ts new file mode 100644 index 00000000..b7b836fc --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/updateOneAccount.ts @@ -0,0 +1,55 @@ +import { models, updateOneAccountRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../../utilities/response.js" +import { updateOne } from "../../../../../../../../../utilities/sql/updateOne.js" + +export const updateOneAccountRoute = registerRoute(updateOneAccountRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: updateOneAccountRouteDefinition.schemas.body, + }) + + const updateOneAccount = await updateOne({ + database: c.var.clients.sql, + table: models.account, + data: { + idAccountParent: body.idAccountParent, + + idBalanceSheetAsset: body.idBalanceSheetAsset, + balanceSheetAssetColumn: body.balanceSheetAssetColumn, + balanceSheetAssetFlow: body.balanceSheetAssetFlow, + + idBalanceSheetLiability: body.idBalanceSheetLiability, + balanceSheetLiabilityColumn: body.balanceSheetLiabilityColumn, + balanceSheetLiabilityFlow: body.balanceSheetLiabilityFlow, + + idIncomeStatement: body.idIncomeStatement, + + isSelectable: body.isSelectable, + number: body.number, + label: body.label, + type: body.type, + lastUpdatedAt: new Date().toISOString(), + lastUpdatedBy: auth.user.id, + }, + where: (table) => + and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear), eq(table.id, body.idAccount)), + }) + + return response({ + context: c, + statusCode: 200, + schema: updateOneAccountRouteDefinition.schemas.return, + data: updateOneAccount, + }) +}) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/accounts/accountsRoutes.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/accountsRoutes.ts similarity index 100% rename from packages/api/src/routes/auth/$idYear/yearSettings/accounts/accountsRoutes.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/accountsRoutes.ts diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/createOneAccount.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/createOneAccount.ts new file mode 100644 index 00000000..9597c2fc --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/createOneAccount.ts @@ -0,0 +1,77 @@ +import { createOneAccountRouteDefinition, generateId, models } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../utilities/response.js" +import { insertOne } from "../../../../../../../../utilities/sql/insertOne.js" +import { selectOne } from "../../../../../../../../utilities/sql/selectOne.js" + +export const createOneAccountRoute = registerRoute(createOneAccountRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: createOneAccountRouteDefinition.schemas.body, + }) + + const readOneAccount = await selectOne({ + database: c.var.clients.sql, + table: models.account, + where: (table) => { + if (body.idAccountParent === null) { + return + } + return and( + eq(table.idOrganization, idOrganization), + eq(table.idYear, body.idYear), + eq(table.id, body.idAccountParent), + ) + }, + }) + + const createOneAccount = await insertOne({ + database: c.var.clients.sql, + table: models.account, + data: { + id: generateId(), + idOrganization: idOrganization, + idYear: body.idYear, + idAccountParent: body.idAccountParent, + + idBalanceSheetAsset: body.idBalanceSheetAsset ?? readOneAccount?.idBalanceSheetAsset, + balanceSheetAssetColumn: body.balanceSheetAssetColumn ?? readOneAccount?.balanceSheetAssetColumn, + balanceSheetAssetFlow: body.balanceSheetAssetFlow ?? readOneAccount?.balanceSheetAssetFlow, + + idBalanceSheetLiability: body.idBalanceSheetLiability ?? readOneAccount?.idBalanceSheetLiability, + balanceSheetLiabilityColumn: + body.balanceSheetLiabilityColumn ?? readOneAccount?.balanceSheetLiabilityColumn, + balanceSheetLiabilityFlow: body.balanceSheetLiabilityFlow ?? readOneAccount?.balanceSheetLiabilityFlow, + + idIncomeStatement: body.idIncomeStatement, + + isSelectable: body.isSelectable, + isDefault: false, + label: body.label, + number: body.number, + type: body.type, + isOptional: false, + createdAt: new Date().toISOString(), + lastUpdatedAt: null, + createdBy: auth.user.id, + lastUpdatedBy: null, + }, + }) + + return response({ + context: c, + statusCode: 200, + schema: createOneAccountRouteDefinition.schemas.return, + data: createOneAccount, + }) +}) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/accounts/generateAccounts.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/generateAccounts.ts similarity index 82% rename from packages/api/src/routes/auth/$idYear/yearSettings/accounts/generateAccounts.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/generateAccounts.ts index 9dd010ca..4bd5519a 100644 --- a/packages/api/src/routes/auth/$idYear/yearSettings/accounts/generateAccounts.ts +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/generateAccounts.ts @@ -10,14 +10,15 @@ import { } from "@arrhes/application-metadata" import { and, eq } from "drizzle-orm" import type * as v from "valibot" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { Exception } from "../../../../../utilities/exception.js" -import { response } from "../../../../../utilities/response.js" -import { deleteMany } from "../../../../../utilities/sql/deleteMany.js" -import { insertMany } from "../../../../../utilities/sql/insertMany.js" -import { selectOne } from "../../../../../utilities/sql/selectOne.js" +import { checkUserSessionMiddleware } from "../../../../../../../../../../middlewares/checkUserSessionMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../utilities/apiFactory.js" +import { registerRoute } from "../../../../../../../../utilities/registerRoute.js" +import { Exception } from "../../../../../../../../utilities/exception.js" +import { response } from "../../../../../../../../utilities/response.js" +import { deleteMany } from "../../../../../../../../utilities/sql/deleteMany.js" +import { insertMany } from "../../../../../../../../utilities/sql/insertMany.js" +import { selectOne } from "../../../../../../../../utilities/sql/selectOne.js" function generateAccounts(parameters: { accounts: Array @@ -68,7 +69,7 @@ function generateAccounts(parameters: { return newAccounts } -export const generateAccountsRoute = apiFactory.createApp().post(generateAccountsRouteDefinition.path, async (c) => { +export const generateAccountsRoute = registerRoute(generateAccountsRouteDefinition, async (c) => { const { idOrganization } = await checkUserSessionMiddleware({ context: c }) const body = await validateBodyMiddleware({ context: c, diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/readAllAccounts.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/readAllAccounts.ts new file mode 100644 index 00000000..d46fe9bd --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/readAllAccounts.ts @@ -0,0 +1,34 @@ +import { models, readAllAccountsRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../utilities/response.js" +import { selectMany } from "../../../../../../../../utilities/sql/selectMany.js" + +export const readAllAccountsRoute = registerRoute(readAllAccountsRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: readAllAccountsRouteDefinition.schemas.body, + }) + + const readAllAccounts = await selectMany({ + database: c.var.clients.sql, + table: models.account, + where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear)), + }) + + return response({ + context: c, + statusCode: 200, + schema: readAllAccountsRouteDefinition.schemas.return, + data: readAllAccounts, + }) +}) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/balanceSheets/$idBalanceSheet/$idBalanceSheetRoutes.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/$idBalanceSheetRoutes.ts similarity index 100% rename from packages/api/src/routes/auth/$idYear/yearSettings/balanceSheets/$idBalanceSheet/$idBalanceSheetRoutes.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/$idBalanceSheetRoutes.ts diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/deleteOneBalanceSheet.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/deleteOneBalanceSheet.ts new file mode 100644 index 00000000..7bd301ba --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/deleteOneBalanceSheet.ts @@ -0,0 +1,41 @@ +import { deleteOneBalanceSheetRouteDefinition, models } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../../../../utilities/response.js" +import { deleteOne } from "../../../../../../../../../utilities/sql/deleteOne.js" + +export const deleteOneBalanceSheetRoute = apiFactory + .createApp() + .post(deleteOneBalanceSheetRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: deleteOneBalanceSheetRouteDefinition.schemas.body, + }) + + const deleteOneBalanceSheet = await deleteOne({ + database: c.var.clients.sql, + table: models.balanceSheet, + where: (table) => + and( + eq(table.idOrganization, idOrganization), + eq(table.idYear, body.idYear), + eq(table.id, body.idBalanceSheet), + ), + }) + + return response({ + context: c, + statusCode: 200, + schema: deleteOneBalanceSheetRouteDefinition.schemas.return, + data: deleteOneBalanceSheet, + }) + }) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/readOneBalanceSheet.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/readOneBalanceSheet.ts new file mode 100644 index 00000000..99213725 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/readOneBalanceSheet.ts @@ -0,0 +1,41 @@ +import { models, readOneBalanceSheetRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../../../../utilities/response.js" +import { selectOne } from "../../../../../../../../../utilities/sql/selectOne.js" + +export const readOneBalanceSheetRoute = apiFactory + .createApp() + .post(readOneBalanceSheetRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: readOneBalanceSheetRouteDefinition.schemas.body, + }) + + const readOneBalanceSheet = await selectOne({ + database: c.var.clients.sql, + table: models.balanceSheet, + where: (table) => + and( + eq(table.idOrganization, idOrganization), + eq(table.idYear, body.idYear), + eq(table.id, body.idBalanceSheet), + ), + }) + + return response({ + context: c, + statusCode: 200, + schema: readOneBalanceSheetRouteDefinition.schemas.return, + data: readOneBalanceSheet, + }) + }) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/updateOneBalanceSheet.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/updateOneBalanceSheet.ts new file mode 100644 index 00000000..14b2d8d6 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/updateOneBalanceSheet.ts @@ -0,0 +1,49 @@ +import { models, updateOneBalanceSheetRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../../../../utilities/response.js" +import { updateOne } from "../../../../../../../../../utilities/sql/updateOne.js" + +export const updateOneBalanceSheetRoute = apiFactory + .createApp() + .post(updateOneBalanceSheetRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: updateOneBalanceSheetRouteDefinition.schemas.body, + }) + + const updatedBalanceSheet = await updateOne({ + database: c.var.clients.sql, + table: models.balanceSheet, + data: { + idBalanceSheetParent: body.idBalanceSheetParent, + isComputed: body.isComputed, + number: body.number, + label: body.label, + lastUpdatedAt: new Date().toISOString(), + lastUpdatedBy: auth.user.id, + }, + where: (table) => + and( + eq(table.idOrganization, idOrganization), + eq(table.idYear, body.idYear), + eq(table.id, body.idBalanceSheet), + ), + }) + + return response({ + context: c, + statusCode: 200, + schema: updateOneBalanceSheetRouteDefinition.schemas.return, + data: updatedBalanceSheet, + }) + }) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/balanceSheets/balanceSheetsRoutes.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/balanceSheetsRoutes.ts similarity index 100% rename from packages/api/src/routes/auth/$idYear/yearSettings/balanceSheets/balanceSheetsRoutes.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/balanceSheetsRoutes.ts diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/balanceSheets/connectAccountsToBalanceSheets.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/connectAccountsToBalanceSheets.ts similarity index 89% rename from packages/api/src/routes/auth/$idYear/yearSettings/balanceSheets/connectAccountsToBalanceSheets.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/connectAccountsToBalanceSheets.ts index 92a217dd..65cfd621 100644 --- a/packages/api/src/routes/auth/$idYear/yearSettings/balanceSheets/connectAccountsToBalanceSheets.ts +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/connectAccountsToBalanceSheets.ts @@ -6,13 +6,13 @@ import { models, } from "@arrhes/application-metadata" import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { response } from "../../../../../utilities/response.js" -import { selectMany } from "../../../../../utilities/sql/selectMany.js" -import { selectOne } from "../../../../../utilities/sql/selectOne.js" -import { updateOne } from "../../../../../utilities/sql/updateOne.js" +import { checkUserSessionMiddleware } from "../../../../../../../../../../middlewares/checkUserSessionMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../../../utilities/response.js" +import { selectMany } from "../../../../../../../../utilities/sql/selectMany.js" +import { selectOne } from "../../../../../../../../utilities/sql/selectOne.js" +import { updateOne } from "../../../../../../../../utilities/sql/updateOne.js" export const connectAccountsToBalanceSheetsRoute = apiFactory .createApp() diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/createOneBalanceSheet.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/createOneBalanceSheet.ts new file mode 100644 index 00000000..0aa1818c --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/createOneBalanceSheet.ts @@ -0,0 +1,49 @@ +import { createOneBalanceSheetRouteDefinition, generateId, models } from "@arrhes/application-metadata" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../../../utilities/response.js" +import { insertOne } from "../../../../../../../../utilities/sql/insertOne.js" + +export const createOneBalanceSheetRoute = apiFactory + .createApp() + .post(createOneBalanceSheetRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: createOneBalanceSheetRouteDefinition.schemas.body, + }) + + const createOneBalanceSheet = await insertOne({ + database: c.var.clients.sql, + table: models.balanceSheet, + data: { + id: generateId(), + idOrganization: idOrganization, + idYear: body.idYear, + idBalanceSheetParent: body.idBalanceSheetParent, + isDefault: false, + isComputed: body.isComputed, + side: body.side, + number: body.number, + label: body.label, + createdAt: new Date().toISOString(), + lastUpdatedAt: null, + createdBy: auth.user.id, + lastUpdatedBy: null, + }, + }) + + return response({ + context: c, + statusCode: 200, + schema: createOneBalanceSheetRouteDefinition.schemas.return, + data: createOneBalanceSheet, + }) + }) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/balanceSheets/generateBalanceSheets.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/generateBalanceSheets.ts similarity index 83% rename from packages/api/src/routes/auth/$idYear/yearSettings/balanceSheets/generateBalanceSheets.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/generateBalanceSheets.ts index 12fe1f76..121975ac 100644 --- a/packages/api/src/routes/auth/$idYear/yearSettings/balanceSheets/generateBalanceSheets.ts +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/generateBalanceSheets.ts @@ -9,14 +9,14 @@ import { } from "@arrhes/application-metadata" import { and, eq } from "drizzle-orm" import type * as v from "valibot" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { Exception } from "../../../../../utilities/exception.js" -import { response } from "../../../../../utilities/response.js" -import { deleteMany } from "../../../../../utilities/sql/deleteMany.js" -import { insertMany } from "../../../../../utilities/sql/insertMany.js" -import { selectOne } from "../../../../../utilities/sql/selectOne.js" +import { checkUserSessionMiddleware } from "../../../../../../../../../../middlewares/checkUserSessionMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../utilities/apiFactory.js" +import { Exception } from "../../../../../../../../utilities/exception.js" +import { response } from "../../../../../../../../utilities/response.js" +import { deleteMany } from "../../../../../../../../utilities/sql/deleteMany.js" +import { insertMany } from "../../../../../../../../utilities/sql/insertMany.js" +import { selectOne } from "../../../../../../../../utilities/sql/selectOne.js" export const generateBalanceSheetsRoute = apiFactory .createApp() diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/readAllBalanceSheets.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/readAllBalanceSheets.ts new file mode 100644 index 00000000..3297fa96 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/readAllBalanceSheets.ts @@ -0,0 +1,36 @@ +import { models, readAllBalanceSheetsRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../../../utilities/response.js" +import { selectMany } from "../../../../../../../../utilities/sql/selectMany.js" + +export const readAllBalanceSheetsRoute = apiFactory + .createApp() + .post(readAllBalanceSheetsRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: readAllBalanceSheetsRouteDefinition.schemas.body, + }) + + const readAllBalanceSheets = await selectMany({ + database: c.var.clients.sql, + table: models.balanceSheet, + where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear)), + }) + + return response({ + context: c, + statusCode: 200, + schema: readAllBalanceSheetsRouteDefinition.schemas.return, + data: readAllBalanceSheets, + }) + }) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/computations/$idComputation/$idComputationRoutes.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/$idComputationRoutes.ts similarity index 100% rename from packages/api/src/routes/auth/$idYear/yearSettings/computations/$idComputation/$idComputationRoutes.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/$idComputationRoutes.ts diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/$idComputationIncomeStatementRoutes.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/$idComputationIncomeStatementRoutes.ts similarity index 100% rename from packages/api/src/routes/auth/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/$idComputationIncomeStatementRoutes.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/$idComputationIncomeStatementRoutes.ts diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/deleteOneComputationIncomeStatement.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/deleteOneComputationIncomeStatement.ts new file mode 100644 index 00000000..4dd5f01e --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/deleteOneComputationIncomeStatement.ts @@ -0,0 +1,41 @@ +import { deleteOneComputationIncomeStatementRouteDefinition, models } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../../../../../../utilities/response.js" +import { deleteOne } from "../../../../../../../../../../../utilities/sql/deleteOne.js" + +export const deleteOneComputationIncomeStatementRoute = apiFactory + .createApp() + .post(deleteOneComputationIncomeStatementRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: deleteOneComputationIncomeStatementRouteDefinition.schemas.body, + }) + + const deleteOneComputationIncomeStatement = await deleteOne({ + database: c.var.clients.sql, + table: models.computationIncomeStatement, + where: (table) => + and( + eq(table.idOrganization, idOrganization), + eq(table.idYear, body.idYear), + eq(table.id, body.idComputationIncomeStatement), + ), + }) + + return response({ + context: c, + statusCode: 200, + schema: deleteOneComputationIncomeStatementRouteDefinition.schemas.return, + data: deleteOneComputationIncomeStatement, + }) + }) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/readOneComputationIncomeStatement.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/readOneComputationIncomeStatement.ts new file mode 100644 index 00000000..e03c6d5c --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/readOneComputationIncomeStatement.ts @@ -0,0 +1,41 @@ +import { models, readOneComputationIncomeStatementRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../../../../../../utilities/response.js" +import { selectOne } from "../../../../../../../../../../../utilities/sql/selectOne.js" + +export const readOneComputationIncomeStatementRoute = apiFactory + .createApp() + .post(readOneComputationIncomeStatementRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: readOneComputationIncomeStatementRouteDefinition.schemas.body, + }) + + const readOneComputationIncomeStatement = await selectOne({ + database: c.var.clients.sql, + table: models.computationIncomeStatement, + where: (table) => + and( + eq(table.idOrganization, idOrganization), + eq(table.idYear, body.idYear), + eq(table.id, body.idComputationIncomeStatement), + ), + }) + + return response({ + context: c, + statusCode: 200, + schema: readOneComputationIncomeStatementRouteDefinition.schemas.return, + data: readOneComputationIncomeStatement, + }) + }) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/updateOneComputationIncomeStatement.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/updateOneComputationIncomeStatement.ts new file mode 100644 index 00000000..5cfee2b0 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/updateOneComputationIncomeStatement.ts @@ -0,0 +1,48 @@ +import { models, updateOneComputationIncomeStatementRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../../../../../../utilities/response.js" +import { updateOne } from "../../../../../../../../../../../utilities/sql/updateOne.js" + +export const updateOneComputationIncomeStatementRoute = apiFactory + .createApp() + .post(updateOneComputationIncomeStatementRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: updateOneComputationIncomeStatementRouteDefinition.schemas.body, + }) + + const updateOneComputationIncomeStatement = await updateOne({ + database: c.var.clients.sql, + table: models.computationIncomeStatement, + data: { + idComputation: body.idComputation, + idIncomeStatement: body.idIncomeStatement, + operation: body.operation, + lastUpdatedAt: new Date().toISOString(), + lastUpdatedBy: auth.user.id, + }, + where: (table) => + and( + eq(table.idOrganization, idOrganization), + eq(table.idYear, body.idYear), + eq(table.id, body.idComputationIncomeStatement), + ), + }) + + return response({ + context: c, + statusCode: 200, + schema: updateOneComputationIncomeStatementRouteDefinition.schemas.return, + data: updateOneComputationIncomeStatement, + }) + }) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/computationIncomeStatementsRoutes.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/computationIncomeStatementsRoutes.ts similarity index 100% rename from packages/api/src/routes/auth/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/computationIncomeStatementsRoutes.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/computationIncomeStatementsRoutes.ts diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/createOneComputationIncomeStatement.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/createOneComputationIncomeStatement.ts new file mode 100644 index 00000000..7d6317fb --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/createOneComputationIncomeStatement.ts @@ -0,0 +1,47 @@ +import { createOneComputationIncomeStatementRouteDefinition, generateId, models } from "@arrhes/application-metadata" +import { checkAuthMiddleware } from "../../../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../../../../../utilities/response.js" +import { insertOne } from "../../../../../../../../../../utilities/sql/insertOne.js" + +export const createOneComputationIncomeStatementRoute = apiFactory + .createApp() + .post(createOneComputationIncomeStatementRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: createOneComputationIncomeStatementRouteDefinition.schemas.body, + }) + + const createOneComputationIncomeStatement = await insertOne({ + database: c.var.clients.sql, + table: models.computationIncomeStatement, + data: { + id: generateId(), + idOrganization: idOrganization, + idYear: body.idYear, + idComputation: body.idComputation, + idIncomeStatement: body.idIncomeStatement, + operation: body.operation, + index: body.index, + createdAt: new Date().toISOString(), + lastUpdatedAt: null, + createdBy: auth.user.id, + lastUpdatedBy: null, + }, + }) + + return response({ + context: c, + statusCode: 200, + schema: createOneComputationIncomeStatementRouteDefinition.schemas.return, + data: createOneComputationIncomeStatement, + }) + }) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/readAllComputationIncomeStatements.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/readAllComputationIncomeStatements.ts new file mode 100644 index 00000000..803c4b5b --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/readAllComputationIncomeStatements.ts @@ -0,0 +1,52 @@ +import { models, readAllComputationIncomeStatementsRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../../../../../utilities/response.js" +import { selectMany } from "../../../../../../../../../../utilities/sql/selectMany.js" + +export const readAllComputationIncomeStatementsRoute = apiFactory + .createApp() + .post(readAllComputationIncomeStatementsRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: readAllComputationIncomeStatementsRouteDefinition.schemas.body, + }) + + const readAllComputationIncomeStatements = await selectMany({ + database: c.var.clients.sql, + table: models.computationIncomeStatement, + where: (table) => { + if (body.idComputation !== undefined) { + return and( + eq(table.idOrganization, idOrganization), + eq(table.idYear, body.idYear), + eq(table.idComputation, body.idComputation), + ) + } + if (body.idIncomeStatement !== undefined) { + return and( + eq(table.idOrganization, idOrganization), + eq(table.idYear, body.idYear), + eq(table.idIncomeStatement, body.idIncomeStatement), + ) + } + return and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear)) + }, + }) + + return response({ + context: c, + statusCode: 200, + schema: readAllComputationIncomeStatementsRouteDefinition.schemas.return, + data: readAllComputationIncomeStatements, + }) + }) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/deleteOneComputation.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/deleteOneComputation.ts new file mode 100644 index 00000000..aed4f642 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/deleteOneComputation.ts @@ -0,0 +1,41 @@ +import { deleteOneComputationRouteDefinition, models } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../../../../utilities/response.js" +import { deleteOne } from "../../../../../../../../../utilities/sql/deleteOne.js" + +export const deleteOneComputationRoute = apiFactory + .createApp() + .post(deleteOneComputationRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: deleteOneComputationRouteDefinition.schemas.body, + }) + + const deleteOneComputation = await deleteOne({ + database: c.var.clients.sql, + table: models.computation, + where: (table) => + and( + eq(table.idOrganization, idOrganization), + eq(table.idYear, body.idYear), + eq(table.id, body.idComputation), + ), + }) + + return response({ + context: c, + statusCode: 200, + schema: deleteOneComputationRouteDefinition.schemas.return, + data: deleteOneComputation, + }) + }) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/readOneComputation.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/readOneComputation.ts new file mode 100644 index 00000000..64a0713a --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/readOneComputation.ts @@ -0,0 +1,41 @@ +import { models, readOneComputationRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../../../../utilities/response.js" +import { selectOne } from "../../../../../../../../../utilities/sql/selectOne.js" + +export const readOneComputationRoute = apiFactory + .createApp() + .post(readOneComputationRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: readOneComputationRouteDefinition.schemas.body, + }) + + const readOneComputation = await selectOne({ + database: c.var.clients.sql, + table: models.computation, + where: (table) => + and( + eq(table.idOrganization, idOrganization), + eq(table.idYear, body.idYear), + eq(table.id, body.idComputation), + ), + }) + + return response({ + context: c, + statusCode: 200, + schema: readOneComputationRouteDefinition.schemas.return, + data: readOneComputation, + }) + }) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/updateOneComputation.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/updateOneComputation.ts new file mode 100644 index 00000000..c0b7a6d4 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/updateOneComputation.ts @@ -0,0 +1,47 @@ +import { models, updateOneComputationRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../../../../utilities/response.js" +import { updateOne } from "../../../../../../../../../utilities/sql/updateOne.js" + +export const updateOneComputationRoute = apiFactory + .createApp() + .post(updateOneComputationRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: updateOneComputationRouteDefinition.schemas.body, + }) + + const updateOneComputation = await updateOne({ + database: c.var.clients.sql, + table: models.computation, + data: { + number: body.number, + label: body.label, + lastUpdatedAt: new Date().toISOString(), + lastUpdatedBy: auth.user.id, + }, + where: (table) => + and( + eq(table.idOrganization, idOrganization), + eq(table.idYear, body.idYear), + eq(table.id, body.idComputation), + ), + }) + + return response({ + context: c, + statusCode: 200, + schema: updateOneComputationRouteDefinition.schemas.return, + data: updateOneComputation, + }) + }) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/computations/computationsRoutes.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/computationsRoutes.ts similarity index 100% rename from packages/api/src/routes/auth/$idYear/yearSettings/computations/computationsRoutes.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/computationsRoutes.ts diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/createOneComputation.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/createOneComputation.ts new file mode 100644 index 00000000..77f799d8 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/createOneComputation.ts @@ -0,0 +1,46 @@ +import { createOneComputationRouteDefinition, generateId, models } from "@arrhes/application-metadata" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../../../utilities/response.js" +import { insertOne } from "../../../../../../../../utilities/sql/insertOne.js" + +export const createOneComputationRoute = apiFactory + .createApp() + .post(createOneComputationRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: createOneComputationRouteDefinition.schemas.body, + }) + + const createOneComputation = await insertOne({ + database: c.var.clients.sql, + table: models.computation, + data: { + id: generateId(), + idOrganization: idOrganization, + idYear: body.idYear, + index: body.index, + number: body.number, + label: body.label, + createdAt: new Date().toISOString(), + lastUpdatedAt: null, + createdBy: auth.user.id, + lastUpdatedBy: null, + }, + }) + + return response({ + context: c, + statusCode: 200, + schema: createOneComputationRouteDefinition.schemas.return, + data: createOneComputation, + }) + }) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/computations/generateComputations.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/generateComputations.ts similarity index 84% rename from packages/api/src/routes/auth/$idYear/yearSettings/computations/generateComputations.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/generateComputations.ts index a1b29ac2..742088e5 100644 --- a/packages/api/src/routes/auth/$idYear/yearSettings/computations/generateComputations.ts +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/generateComputations.ts @@ -6,14 +6,14 @@ import { models, } from "@arrhes/application-metadata" import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { Exception } from "../../../../../utilities/exception.js" -import { response } from "../../../../../utilities/response.js" -import { deleteMany } from "../../../../../utilities/sql/deleteMany.js" -import { insertMany } from "../../../../../utilities/sql/insertMany.js" -import { selectMany } from "../../../../../utilities/sql/selectMany.js" +import { checkUserSessionMiddleware } from "../../../../../../../../../../middlewares/checkUserSessionMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../utilities/apiFactory.js" +import { Exception } from "../../../../../../../../utilities/exception.js" +import { response } from "../../../../../../../../utilities/response.js" +import { deleteMany } from "../../../../../../../../utilities/sql/deleteMany.js" +import { insertMany } from "../../../../../../../../utilities/sql/insertMany.js" +import { selectMany } from "../../../../../../../../utilities/sql/selectMany.js" export const generateComputationsRoute = apiFactory .createApp() diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/readAllComputations.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/readAllComputations.ts new file mode 100644 index 00000000..0a91c06d --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/readAllComputations.ts @@ -0,0 +1,36 @@ +import { models, readAllComputationsRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../../../utilities/response.js" +import { selectMany } from "../../../../../../../../utilities/sql/selectMany.js" + +export const readAllComputationsRoute = apiFactory + .createApp() + .post(readAllComputationsRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: readAllComputationsRouteDefinition.schemas.body, + }) + + const readAllComputations = await selectMany({ + database: c.var.clients.sql, + table: models.computation, + where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear)), + }) + + return response({ + context: c, + statusCode: 200, + schema: readAllComputationsRouteDefinition.schemas.return, + data: readAllComputations, + }) + }) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/closeYear.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/closeYear.ts new file mode 100644 index 00000000..0f74ac8e --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/closeYear.ts @@ -0,0 +1,39 @@ +import { closeYearRouteDefinition, models } from "@arrhes/application-metadata" +import { and, eq, not } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../utilities/response.js" +import { updateOne } from "../../../../../../../../utilities/sql/updateOne.js" + +export const closeYearRoute = registerRoute(closeYearRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: closeYearRouteDefinition.schemas.body, + }) + + const closeYear = await updateOne({ + database: c.var.clients.sql, + table: models.year, + data: { + isClosed: not(models.year.isClosed), + lastUpdatedAt: new Date().toISOString(), + lastUpdatedBy: auth.user.id, + }, + where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.id, body.idYear)), + }) + + return response({ + context: c, + statusCode: 200, + schema: closeYearRouteDefinition.schemas.return, + data: closeYear, + }) +}) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/deleteOneYear.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/deleteOneYear.ts new file mode 100644 index 00000000..55aabd03 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/deleteOneYear.ts @@ -0,0 +1,34 @@ +import { deleteOneYearRouteDefinition, models } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../utilities/response.js" +import { deleteOne } from "../../../../../../../../utilities/sql/deleteOne.js" + +export const deleteOneYearRoute = registerRoute(deleteOneYearRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: deleteOneYearRouteDefinition.schemas.body, + }) + + const deleteOneYear = await deleteOne({ + database: c.var.clients.sql, + table: models.year, + where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.id, body.idYear)), + }) + + return response({ + context: c, + statusCode: 200, + schema: deleteOneYearRouteDefinition.schemas.return, + data: deleteOneYear, + }) +}) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/general/generalRoutes.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/generalRoutes.ts similarity index 100% rename from packages/api/src/routes/auth/$idYear/yearSettings/general/generalRoutes.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/generalRoutes.ts diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/general/openYear.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/openYear.ts similarity index 88% rename from packages/api/src/routes/auth/$idYear/yearSettings/general/openYear.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/openYear.ts index 2fd4febb..da94ae85 100644 --- a/packages/api/src/routes/auth/$idYear/yearSettings/general/openYear.ts +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/openYear.ts @@ -1,11 +1,11 @@ import { openYearRouteDefinition } from "@arrhes/application-metadata" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { response } from "../../../../../utilities/response.js" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../utilities/response.js" -export const openYearRoute = apiFactory.createApp().post(openYearRouteDefinition.path, async (c) => { - await checkUserSessionMiddleware({ +export const openYearRoute = registerRoute(openYearRouteDefinition, async (c) => { + await checkAuthMiddleware({ context: c, }) const _body = await validateBodyMiddleware({ diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/general/settleBalanceSheet.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/settleBalanceSheet.ts similarity index 81% rename from packages/api/src/routes/auth/$idYear/yearSettings/general/settleBalanceSheet.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/settleBalanceSheet.ts index 64fcd15e..3b4ecfb1 100644 --- a/packages/api/src/routes/auth/$idYear/yearSettings/general/settleBalanceSheet.ts +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/settleBalanceSheet.ts @@ -1,22 +1,26 @@ import { generateId, models, settleBalanceSheetRouteDefinition } from "@arrhes/application-metadata" import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { Exception } from "../../../../../utilities/exception.js" -import { response } from "../../../../../utilities/response.js" -import { deleteMany } from "../../../../../utilities/sql/deleteMany.js" -import { insertMany } from "../../../../../utilities/sql/insertMany.js" -import { insertOne } from "../../../../../utilities/sql/insertOne.js" -import { selectMany } from "../../../../../utilities/sql/selectMany.js" -import { selectOne } from "../../../../../utilities/sql/selectOne.js" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../utilities/apiFactory.js" +import { Exception } from "../../../../../../../../utilities/exception.js" +import { response } from "../../../../../../../../utilities/response.js" +import { deleteMany } from "../../../../../../../../utilities/sql/deleteMany.js" +import { insertMany } from "../../../../../../../../utilities/sql/insertMany.js" +import { insertOne } from "../../../../../../../../utilities/sql/insertOne.js" +import { selectMany } from "../../../../../../../../utilities/sql/selectMany.js" +import { selectOne } from "../../../../../../../../utilities/sql/selectOne.js" export const settleBalanceSheetRoute = apiFactory .createApp() .post(settleBalanceSheetRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ + const auth = await checkAuthMiddleware({ context: c, }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) const body = await validateBodyMiddleware({ context: c, schema: settleBalanceSheetRouteDefinition.schemas.body, @@ -98,7 +102,7 @@ export const settleBalanceSheetRoute = apiFactory credit: algebraicBalance > 0 ? String(algebraicBalance.toFixed(2)) : "0.00", createdAt: new Date().toISOString(), lastUpdatedAt: null, - createdBy: user.id, + createdBy: auth.user.id, lastUpdatedBy: null, }) } @@ -125,7 +129,7 @@ export const settleBalanceSheetRoute = apiFactory date: year.endingAt, createdAt: new Date().toISOString(), lastUpdatedAt: null, - createdBy: user.id, + createdBy: auth.user.id, lastUpdatedBy: null, }, }) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/general/settleIncomeStatement.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/settleIncomeStatement.ts similarity index 84% rename from packages/api/src/routes/auth/$idYear/yearSettings/general/settleIncomeStatement.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/settleIncomeStatement.ts index c5d7e6de..f9b9bb97 100644 --- a/packages/api/src/routes/auth/$idYear/yearSettings/general/settleIncomeStatement.ts +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/settleIncomeStatement.ts @@ -1,22 +1,26 @@ import { generateId, models, settleIncomeStatementRouteDefinition } from "@arrhes/application-metadata" import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { Exception } from "../../../../../utilities/exception.js" -import { response } from "../../../../../utilities/response.js" -import { deleteMany } from "../../../../../utilities/sql/deleteMany.js" -import { insertMany } from "../../../../../utilities/sql/insertMany.js" -import { insertOne } from "../../../../../utilities/sql/insertOne.js" -import { selectMany } from "../../../../../utilities/sql/selectMany.js" -import { selectOne } from "../../../../../utilities/sql/selectOne.js" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../utilities/apiFactory.js" +import { Exception } from "../../../../../../../../utilities/exception.js" +import { response } from "../../../../../../../../utilities/response.js" +import { deleteMany } from "../../../../../../../../utilities/sql/deleteMany.js" +import { insertMany } from "../../../../../../../../utilities/sql/insertMany.js" +import { insertOne } from "../../../../../../../../utilities/sql/insertOne.js" +import { selectMany } from "../../../../../../../../utilities/sql/selectMany.js" +import { selectOne } from "../../../../../../../../utilities/sql/selectOne.js" export const settleIncomeStatementRoute = apiFactory .createApp() .post(settleIncomeStatementRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ + const auth = await checkAuthMiddleware({ context: c, }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) const body = await validateBodyMiddleware({ context: c, schema: settleIncomeStatementRouteDefinition.schemas.body, @@ -98,7 +102,7 @@ export const settleIncomeStatementRoute = apiFactory credit: algebraicBalance > 0 ? String(algebraicBalance.toFixed(2)) : "0.00", createdAt: new Date().toISOString(), lastUpdatedAt: null, - createdBy: user.id, + createdBy: auth.user.id, lastUpdatedBy: null, }) } @@ -130,7 +134,7 @@ export const settleIncomeStatementRoute = apiFactory date: year.endingAt, createdAt: new Date().toISOString(), lastUpdatedAt: null, - createdBy: user.id, + createdBy: auth.user.id, lastUpdatedBy: null, }, }) @@ -157,7 +161,7 @@ export const settleIncomeStatementRoute = apiFactory credit: algebraicResult < 0 ? String((-algebraicResult).toFixed(2)) : "0.00", createdAt: new Date().toISOString(), lastUpdatedAt: null, - createdBy: user.id, + createdBy: auth.user.id, lastUpdatedBy: null, }) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/updateOneYear.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/updateOneYear.ts new file mode 100644 index 00000000..99321e8f --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/updateOneYear.ts @@ -0,0 +1,42 @@ +import { models, updateOneYearRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../utilities/response.js" +import { updateOne } from "../../../../../../../../utilities/sql/updateOne.js" + +export const updateOneYearRoute = registerRoute(updateOneYearRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: updateOneYearRouteDefinition.schemas.body, + }) + + const updateOneYear = await updateOne({ + database: c.var.clients.sql, + table: models.year, + data: { + idYearPrevious: body.idYearPrevious, + label: body.label, + startingAt: body.startingAt, + endingAt: body.endingAt, + lastUpdatedAt: new Date().toISOString(), + lastUpdatedBy: auth.user.id, + }, + where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.id, body.idYear)), + }) + + return response({ + context: c, + statusCode: 200, + schema: updateOneYearRouteDefinition.schemas.return, + data: updateOneYear, + }) +}) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/incomeStatements/$idIncomeStatement/$idIncomeStatementRoutes.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/$idIncomeStatementRoutes.ts similarity index 100% rename from packages/api/src/routes/auth/$idYear/yearSettings/incomeStatements/$idIncomeStatement/$idIncomeStatementRoutes.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/$idIncomeStatementRoutes.ts diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/deleteOneIncomeStatement.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/deleteOneIncomeStatement.ts new file mode 100644 index 00000000..6f069362 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/deleteOneIncomeStatement.ts @@ -0,0 +1,41 @@ +import { deleteOneIncomeStatementRouteDefinition, models } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../../../../utilities/response.js" +import { deleteOne } from "../../../../../../../../../utilities/sql/deleteOne.js" + +export const deleteOneIncomeStatementRoute = apiFactory + .createApp() + .post(deleteOneIncomeStatementRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: deleteOneIncomeStatementRouteDefinition.schemas.body, + }) + + const deleteOneIncomeStatement = await deleteOne({ + database: c.var.clients.sql, + table: models.incomeStatement, + where: (table) => + and( + eq(table.idOrganization, idOrganization), + eq(table.idYear, body.idYear), + eq(table.id, body.idIncomeStatement), + ), + }) + + return response({ + context: c, + statusCode: 200, + schema: deleteOneIncomeStatementRouteDefinition.schemas.return, + data: deleteOneIncomeStatement, + }) + }) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/readOneIncomeStatement.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/readOneIncomeStatement.ts new file mode 100644 index 00000000..42a5aa79 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/readOneIncomeStatement.ts @@ -0,0 +1,41 @@ +import { models, readOneIncomeStatementRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../../../../utilities/response.js" +import { selectOne } from "../../../../../../../../../utilities/sql/selectOne.js" + +export const readOneIncomeStatementRoute = apiFactory + .createApp() + .post(readOneIncomeStatementRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: readOneIncomeStatementRouteDefinition.schemas.body, + }) + + const readOneIncomeStatement = await selectOne({ + database: c.var.clients.sql, + table: models.incomeStatement, + where: (table) => + and( + eq(table.idOrganization, idOrganization), + eq(table.idYear, body.idYear), + eq(table.id, body.idIncomeStatement), + ), + }) + + return response({ + context: c, + statusCode: 200, + schema: readOneIncomeStatementRouteDefinition.schemas.return, + data: readOneIncomeStatement, + }) + }) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/updateOneIncomeStatement.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/updateOneIncomeStatement.ts new file mode 100644 index 00000000..05cda039 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/updateOneIncomeStatement.ts @@ -0,0 +1,49 @@ +import { models, updateOneIncomeStatementRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../../../../utilities/response.js" +import { updateOne } from "../../../../../../../../../utilities/sql/updateOne.js" + +export const updateOneIncomeStatementRoute = apiFactory + .createApp() + .post(updateOneIncomeStatementRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: updateOneIncomeStatementRouteDefinition.schemas.body, + }) + + const updatedIncomeStatement = await updateOne({ + database: c.var.clients.sql, + table: models.incomeStatement, + data: { + idIncomeStatementParent: body.idIncomeStatementParent, + isComputed: body.isComputed, + number: body.number, + label: body.label, + lastUpdatedAt: new Date().toISOString(), + lastUpdatedBy: auth.user.id, + }, + where: (table) => + and( + eq(table.idOrganization, idOrganization), + eq(table.idYear, body.idYear), + eq(table.id, body.idIncomeStatement), + ), + }) + + return response({ + context: c, + statusCode: 200, + schema: updateOneIncomeStatementRouteDefinition.schemas.return, + data: updatedIncomeStatement, + }) + }) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/incomeStatements/connectAccountsToIncomeStatements.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/connectAccountsToIncomeStatements.ts similarity index 85% rename from packages/api/src/routes/auth/$idYear/yearSettings/incomeStatements/connectAccountsToIncomeStatements.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/connectAccountsToIncomeStatements.ts index 21c0dfce..60ab1607 100644 --- a/packages/api/src/routes/auth/$idYear/yearSettings/incomeStatements/connectAccountsToIncomeStatements.ts +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/connectAccountsToIncomeStatements.ts @@ -6,13 +6,13 @@ import { models, } from "@arrhes/application-metadata" import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { response } from "../../../../../utilities/response.js" -import { selectMany } from "../../../../../utilities/sql/selectMany.js" -import { selectOne } from "../../../../../utilities/sql/selectOne.js" -import { updateOne } from "../../../../../utilities/sql/updateOne.js" +import { checkUserSessionMiddleware } from "../../../../../../../../../../middlewares/checkUserSessionMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../../../utilities/response.js" +import { selectMany } from "../../../../../../../../utilities/sql/selectMany.js" +import { selectOne } from "../../../../../../../../utilities/sql/selectOne.js" +import { updateOne } from "../../../../../../../../utilities/sql/updateOne.js" export const connectAccountsToIncomeStatementsRoute = apiFactory .createApp() diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/createOneIncomeStatement.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/createOneIncomeStatement.ts new file mode 100644 index 00000000..b1a09c3e --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/createOneIncomeStatement.ts @@ -0,0 +1,48 @@ +import { createOneIncomeStatementRouteDefinition, generateId, models } from "@arrhes/application-metadata" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../../../utilities/response.js" +import { insertOne } from "../../../../../../../../utilities/sql/insertOne.js" + +export const createOneIncomeStatementRoute = apiFactory + .createApp() + .post(createOneIncomeStatementRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: createOneIncomeStatementRouteDefinition.schemas.body, + }) + + const createOneIncomeStatement = await insertOne({ + database: c.var.clients.sql, + table: models.incomeStatement, + data: { + id: generateId(), + idOrganization: idOrganization, + idYear: body.idYear, + idIncomeStatementParent: body.idIncomeStatementParent, + isDefault: false, + isComputed: body.isComputed, + number: body.number, + label: body.label, + createdAt: new Date().toISOString(), + lastUpdatedAt: null, + createdBy: auth.user.id, + lastUpdatedBy: null, + }, + }) + + return response({ + context: c, + statusCode: 200, + schema: createOneIncomeStatementRouteDefinition.schemas.return, + data: createOneIncomeStatement, + }) + }) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/incomeStatements/generateIncomeStatements.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/generateIncomeStatements.ts similarity index 83% rename from packages/api/src/routes/auth/$idYear/yearSettings/incomeStatements/generateIncomeStatements.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/generateIncomeStatements.ts index 3700288f..cdcfbcfc 100644 --- a/packages/api/src/routes/auth/$idYear/yearSettings/incomeStatements/generateIncomeStatements.ts +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/generateIncomeStatements.ts @@ -9,14 +9,14 @@ import { } from "@arrhes/application-metadata" import { and, eq } from "drizzle-orm" import type * as v from "valibot" -import { checkUserSessionMiddleware } from "../../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../../utilities/apiFactory.js" -import { Exception } from "../../../../../utilities/exception.js" -import { response } from "../../../../../utilities/response.js" -import { deleteMany } from "../../../../../utilities/sql/deleteMany.js" -import { insertMany } from "../../../../../utilities/sql/insertMany.js" -import { selectOne } from "../../../../../utilities/sql/selectOne.js" +import { checkUserSessionMiddleware } from "../../../../../../../../../../middlewares/checkUserSessionMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../utilities/apiFactory.js" +import { Exception } from "../../../../../../../../utilities/exception.js" +import { response } from "../../../../../../../../utilities/response.js" +import { deleteMany } from "../../../../../../../../utilities/sql/deleteMany.js" +import { insertMany } from "../../../../../../../../utilities/sql/insertMany.js" +import { selectOne } from "../../../../../../../../utilities/sql/selectOne.js" export const generateIncomeStatementsRoute = apiFactory .createApp() diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/incomeStatements/incomeStatementsRoutes.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/incomeStatementsRoutes.ts similarity index 100% rename from packages/api/src/routes/auth/$idYear/yearSettings/incomeStatements/incomeStatementsRoutes.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/incomeStatementsRoutes.ts diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/readAllIncomeStatements.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/readAllIncomeStatements.ts new file mode 100644 index 00000000..34dea83d --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/readAllIncomeStatements.ts @@ -0,0 +1,36 @@ +import { models, readAllIncomeStatementsRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../utilities/apiFactory.js" +import { response } from "../../../../../../../../utilities/response.js" +import { selectMany } from "../../../../../../../../utilities/sql/selectMany.js" + +export const readAllIncomeStatementsRoute = apiFactory + .createApp() + .post(readAllIncomeStatementsRouteDefinition.path, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: readAllIncomeStatementsRouteDefinition.schemas.body, + }) + + const readAllIncomeStatements = await selectMany({ + database: c.var.clients.sql, + table: models.incomeStatement, + where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear)), + }) + + return response({ + context: c, + statusCode: 200, + schema: readAllIncomeStatementsRouteDefinition.schemas.return, + data: readAllIncomeStatements, + }) + }) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/journals/$idJournal/$idJournalRoute.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/$idJournalRoute.ts similarity index 100% rename from packages/api/src/routes/auth/$idYear/yearSettings/journals/$idJournal/$idJournalRoute.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/$idJournalRoute.ts diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/deleteOneJournal.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/deleteOneJournal.ts new file mode 100644 index 00000000..9b6ad365 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/deleteOneJournal.ts @@ -0,0 +1,35 @@ +import { deleteOneJournalRouteDefinition, models } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../../utilities/response.js" +import { deleteOne } from "../../../../../../../../../utilities/sql/deleteOne.js" + +export const deleteOneJournalRoute = registerRoute(deleteOneJournalRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: deleteOneJournalRouteDefinition.schemas.body, + }) + + const deleteOneJournal = await deleteOne({ + database: c.var.clients.sql, + table: models.journal, + where: (table) => + and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear), eq(table.id, body.idJournal)), + }) + + return response({ + context: c, + statusCode: 200, + schema: deleteOneJournalRouteDefinition.schemas.return, + data: deleteOneJournal, + }) +}) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/readOneJournal.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/readOneJournal.ts new file mode 100644 index 00000000..9dd90b7e --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/readOneJournal.ts @@ -0,0 +1,35 @@ +import { models, readOneJournalRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../../utilities/response.js" +import { selectOne } from "../../../../../../../../../utilities/sql/selectOne.js" + +export const readOneJournalRoute = registerRoute(readOneJournalRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: readOneJournalRouteDefinition.schemas.body, + }) + + const readOneJournal = await selectOne({ + database: c.var.clients.sql, + table: models.journal, + where: (table) => + and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear), eq(table.id, body.idJournal)), + }) + + return response({ + context: c, + statusCode: 200, + schema: readOneJournalRouteDefinition.schemas.return, + data: readOneJournal, + }) +}) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/updateOneJournal.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/updateOneJournal.ts new file mode 100644 index 00000000..f5448ecd --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/updateOneJournal.ts @@ -0,0 +1,41 @@ +import { models, updateOneJournalRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../../utilities/response.js" +import { updateOne } from "../../../../../../../../../utilities/sql/updateOne.js" + +export const updateOneJournalRoute = registerRoute(updateOneJournalRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: updateOneJournalRouteDefinition.schemas.body, + }) + + const updateOneJournal = await updateOne({ + database: c.var.clients.sql, + table: models.journal, + data: { + code: body.code, + label: body.label, + lastUpdatedAt: new Date().toISOString(), + lastUpdatedBy: auth.user.id, + }, + where: (table) => + and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear), eq(table.id, body.idJournal)), + }) + + return response({ + context: c, + statusCode: 200, + schema: updateOneJournalRouteDefinition.schemas.return, + data: updateOneJournal, + }) +}) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/createOneJournal.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/createOneJournal.ts new file mode 100644 index 00000000..89c78efa --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/createOneJournal.ts @@ -0,0 +1,43 @@ +import { createOneJournalRouteDefinition, generateId, models } from "@arrhes/application-metadata" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../utilities/response.js" +import { insertOne } from "../../../../../../../../utilities/sql/insertOne.js" + +export const createOneJournalRoute = registerRoute(createOneJournalRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: createOneJournalRouteDefinition.schemas.body, + }) + + const createOneJournal = await insertOne({ + database: c.var.clients.sql, + table: models.journal, + data: { + id: generateId(), + idOrganization: idOrganization, + idYear: body.idYear, + code: body.code, + label: body.label, + createdAt: new Date().toISOString(), + lastUpdatedAt: null, + createdBy: auth.user.id, + lastUpdatedBy: null, + }, + }) + + return response({ + context: c, + statusCode: 200, + schema: createOneJournalRouteDefinition.schemas.return, + data: createOneJournal, + }) +}) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/generateJournals.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/generateJournals.ts new file mode 100644 index 00000000..f87e902b --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/generateJournals.ts @@ -0,0 +1,64 @@ +/* +import { defaultJournals, generateId, generateJournalsRouteDefinition, models } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkUserSessionMiddleware } from "../../../../../../../../../../middlewares/checkUserSessionMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { apiFactory } from "../../../../../../../../utilities/apiFactory.js" +import { registerRoute } from "../../../../../../../../utilities/registerRoute.js" +import { Exception } from "../../../../../../../../utilities/exception.js" +import { response } from "../../../../../../../../utilities/response.js" +import { deleteMany } from "../../../../../../../../utilities/sql/deleteMany.js" +import { insertMany } from "../../../../../../../../utilities/sql/insertMany.js" + +export const generateJournalsRoute = registerRoute(generateJournalsRouteDefinition, async (c) => { + const { idOrganization } = await checkUserSessionMiddleware({ context: c }) + const body = await validateBodyMiddleware({ + context: c, + schema: generateJournalsRouteDefinition.schemas.body, + }) + + const generatedJournals = await c.var.clients.sql.transaction(async (tx) => { + try { + const _deletedJournals = await deleteMany({ + database: tx, + table: models.journal, + where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear)), + }) + } catch (_error: unknown) { + throw new Exception({ + internalMessage: "Failed to delete journals", + externalMessage: "Échec de la suppression des journaux", + }) + } + + const newJournals = defaultJournals.map((defaultJournal) => { + return { + id: generateId(), + idOrganization: idOrganization, + idYear: body.idYear, + code: defaultJournal.code, + label: defaultJournal.label, + createdAt: new Date().toISOString(), + lastUpdatedAt: null, + createdBy: null, + lastUpdatedBy: null, + } + }) + + const generatedJournals = await insertMany({ + database: tx, + table: models.journal, + data: newJournals, + }) + + return generatedJournals + }) + + return response({ + context: c, + statusCode: 200, + schema: generateJournalsRouteDefinition.schemas.return, + data: generatedJournals, + }) +}) +*/ diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/journals/journalsRoutes.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/journalsRoutes.ts similarity index 100% rename from packages/api/src/routes/auth/$idYear/yearSettings/journals/journalsRoutes.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/journalsRoutes.ts diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/readAllJournals.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/readAllJournals.ts new file mode 100644 index 00000000..d7d6359e --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/readAllJournals.ts @@ -0,0 +1,34 @@ +import { models, readAllJournalsRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../utilities/response.js" +import { selectMany } from "../../../../../../../../utilities/sql/selectMany.js" + +export const readAllJournalsRoute = registerRoute(readAllJournalsRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: readAllJournalsRouteDefinition.schemas.body, + }) + + const readAllJournals = await selectMany({ + database: c.var.clients.sql, + table: models.journal, + where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear)), + }) + + return response({ + context: c, + statusCode: 200, + schema: readAllJournalsRouteDefinition.schemas.return, + data: readAllJournals, + }) +}) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/tags/$idTag/$idTagRoutes.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/$idTagRoutes.ts similarity index 100% rename from packages/api/src/routes/auth/$idYear/yearSettings/tags/$idTag/$idTagRoutes.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/$idTagRoutes.ts diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/deleteOneTag.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/deleteOneTag.ts new file mode 100644 index 00000000..d28eef7d --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/deleteOneTag.ts @@ -0,0 +1,35 @@ +import { deleteOneTagRouteDefinition, models } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../../utilities/response.js" +import { deleteOne } from "../../../../../../../../../utilities/sql/deleteOne.js" + +export const deleteOneTagRoute = registerRoute(deleteOneTagRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: deleteOneTagRouteDefinition.schemas.body, + }) + + const deleteOneTag = await deleteOne({ + database: c.var.clients.sql, + table: models.tag, + where: (table) => + and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear), eq(table.id, body.idTag)), + }) + + return response({ + context: c, + statusCode: 200, + schema: deleteOneTagRouteDefinition.schemas.return, + data: deleteOneTag, + }) +}) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/readOneTag.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/readOneTag.ts new file mode 100644 index 00000000..87787765 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/readOneTag.ts @@ -0,0 +1,35 @@ +import { models, readOneTagRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../../utilities/response.js" +import { selectOne } from "../../../../../../../../../utilities/sql/selectOne.js" + +export const readOneTagRoute = registerRoute(readOneTagRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: readOneTagRouteDefinition.schemas.body, + }) + + const readOneTag = await selectOne({ + database: c.var.clients.sql, + table: models.tag, + where: (table) => + and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear), eq(table.id, body.idTag)), + }) + + return response({ + context: c, + statusCode: 200, + schema: readOneTagRouteDefinition.schemas.return, + data: readOneTag, + }) +}) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/updateOneTag.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/updateOneTag.ts new file mode 100644 index 00000000..0bd149a0 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/updateOneTag.ts @@ -0,0 +1,41 @@ +import { models, updateOneTagRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../../utilities/response.js" +import { updateOne } from "../../../../../../../../../utilities/sql/updateOne.js" + +export const updateOneTagRoute = registerRoute(updateOneTagRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: updateOneTagRouteDefinition.schemas.body, + }) + + const updateOneTag = await updateOne({ + database: c.var.clients.sql, + table: models.tag, + data: { + label: body.label, + + lastUpdatedAt: new Date().toISOString(), + lastUpdatedBy: auth.user.id, + }, + where: (table) => + and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear), eq(table.id, body.idTag)), + }) + + return response({ + context: c, + statusCode: 200, + schema: updateOneTagRouteDefinition.schemas.return, + data: updateOneTag, + }) +}) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/createOneTag.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/createOneTag.ts new file mode 100644 index 00000000..07f3811c --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/createOneTag.ts @@ -0,0 +1,44 @@ +import { createOneTagRouteDefinition, generateId, models } from "@arrhes/application-metadata" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../utilities/response.js" +import { insertOne } from "../../../../../../../../utilities/sql/insertOne.js" + +export const createOneTagRoute = registerRoute(createOneTagRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: createOneTagRouteDefinition.schemas.body, + }) + + const createOneTag = await insertOne({ + database: c.var.clients.sql, + table: models.tag, + data: { + id: generateId(), + idOrganization: idOrganization, + idYear: body.idYear, + + label: body.label, + + createdAt: new Date().toISOString(), + lastUpdatedAt: null, + createdBy: auth.user.id, + lastUpdatedBy: null, + }, + }) + + return response({ + context: c, + statusCode: 200, + schema: createOneTagRouteDefinition.schemas.return, + data: createOneTag, + }) +}) diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/readAllTags.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/readAllTags.ts new file mode 100644 index 00000000..18a998c3 --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/readAllTags.ts @@ -0,0 +1,34 @@ +import { models, readAllTagsRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../../../../utilities/registerRoute.js" +import { response } from "../../../../../../../../utilities/response.js" +import { selectMany } from "../../../../../../../../utilities/sql/selectMany.js" + +export const readAllTagsRoute = registerRoute(readAllTagsRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + const body = await validateBodyMiddleware({ + context: c, + schema: readAllTagsRouteDefinition.schemas.body, + }) + + const readAllTags = await selectMany({ + database: c.var.clients.sql, + table: models.tag, + where: (table) => and(eq(table.idOrganization, idOrganization), eq(table.idYear, body.idYear)), + }) + + return response({ + context: c, + statusCode: 200, + schema: readAllTagsRouteDefinition.schemas.return, + data: readAllTags, + }) +}) diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/tags/tagsRoutes.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/tagsRoutes.ts similarity index 100% rename from packages/api/src/routes/auth/$idYear/yearSettings/tags/tagsRoutes.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/tagsRoutes.ts diff --git a/packages/api/src/routes/auth/$idYear/yearSettings/yearSettingsRoute.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/yearSettingsRoute.ts similarity index 100% rename from packages/api/src/routes/auth/$idYear/yearSettings/yearSettingsRoute.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/$idYear/yearSettings/yearSettingsRoute.ts diff --git a/packages/api/src/routes/auth/$idOrganization/years/createOneYear.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/createOneYear.ts similarity index 92% rename from packages/api/src/routes/auth/$idOrganization/years/createOneYear.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/createOneYear.ts index fe03189d..8e7758f5 100644 --- a/packages/api/src/routes/auth/$idOrganization/years/createOneYear.ts +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/createOneYear.ts @@ -15,14 +15,15 @@ import { } from "@arrhes/application-metadata" import { and, eq } from "drizzle-orm" import type * as v from "valibot" -import { checkUserSessionMiddleware } from "../../../../middlewares/checkUserSessionMiddleware.js" -import { validateBodyMiddleware } from "../../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../../utilities/apiFactory.js" -import { response } from "../../../../utilities/response.js" -import { insertMany } from "../../../../utilities/sql/insertMany.js" -import { insertOne } from "../../../../utilities/sql/insertOne.js" -import { selectOne } from "../../../../utilities/sql/selectOne.js" -import { updateOne } from "../../../../utilities/sql/updateOne.js" +import { checkAuthMiddleware } from "../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../../../utilities/registerRoute.js" +import { response } from "../../../../../utilities/response.js" +import { insertMany } from "../../../../../utilities/sql/insertMany.js" +import { insertOne } from "../../../../../utilities/sql/insertOne.js" +import { selectOne } from "../../../../../utilities/sql/selectOne.js" +import { updateOne } from "../../../../../utilities/sql/updateOne.js" function buildAccountRows(parameters: { accounts: Array @@ -73,10 +74,13 @@ function buildAccountRows(parameters: { return newAccounts } -export const createOneYearRoute = apiFactory.createApp().post(createOneYearRouteDefinition.path, async (c) => { - const { user, idOrganization } = await checkUserSessionMiddleware({ +export const createOneYearRoute = registerRoute(createOneYearRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ context: c, }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) const body = await validateBodyMiddleware({ context: c, schema: createOneYearRouteDefinition.schemas.body, @@ -98,7 +102,7 @@ export const createOneYearRoute = apiFactory.createApp().post(createOneYearRoute endingAt: body.endingAt, createdAt: new Date().toISOString(), lastUpdatedAt: null, - createdBy: user.id, + createdBy: auth.user.id, lastUpdatedBy: null, }, }) @@ -303,7 +307,7 @@ export const createOneYearRoute = apiFactory.createApp().post(createOneYearRoute defaultBalanceSheet.side === "liability" ? defaultAccount.flow : undefined, lastUpdatedAt: new Date().toISOString(), - lastUpdatedBy: user.id, + lastUpdatedBy: auth.user.id, }, where: (table) => and( @@ -339,7 +343,7 @@ export const createOneYearRoute = apiFactory.createApp().post(createOneYearRoute data: { idIncomeStatement: incomeStatement.id, lastUpdatedAt: new Date().toISOString(), - lastUpdatedBy: user.id, + lastUpdatedBy: auth.user.id, }, where: (table) => and( diff --git a/packages/api/src/routes/auth/organizations/$idOrganization/years/readAllYears.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/readAllYears.ts new file mode 100644 index 00000000..ec0825ee --- /dev/null +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/readAllYears.ts @@ -0,0 +1,42 @@ +import { models, readAllYearsRouteDefinition } from "@arrhes/application-metadata" +import { and, eq } from "drizzle-orm" +import { checkAuthMiddleware } from "../../../../../middlewares/checkAuthMiddleware.js" +import { requireOrganizationMiddleware } from "../../../../../middlewares/requireOrganizationMiddleware.js" +import { validateBodyMiddleware } from "../../../../../middlewares/validateBody.middleware.js" +import { Exception } from "../../../../../utilities/exception.js" +import { registerRoute } from "../../../../../utilities/registerRoute.js" +import { response } from "../../../../../utilities/response.js" +import { selectMany } from "../../../../../utilities/sql/selectMany.js" + +export const readAllYearsRoute = registerRoute(readAllYearsRouteDefinition, async (c) => { + const auth = await checkAuthMiddleware({ + context: c, + }) + const idOrganization = await requireOrganizationMiddleware({ + idOrganization: auth.idOrganization, + }) + if (!idOrganization) { + throw new Exception({ + statusCode: 400, + internalMessage: "idOrganization is required for readAllYears", + externalMessage: "Organization identifier is required", + }) + } + const _body = await validateBodyMiddleware({ + context: c, + schema: readAllYearsRouteDefinition.schemas.body, + }) + + const readAllYears = await selectMany({ + database: c.var.clients.sql, + table: models.year, + where: (table) => and(eq(table.idOrganization, idOrganization)), + }) + + return response({ + context: c, + statusCode: 200, + schema: readAllYearsRouteDefinition.schemas.return, + data: readAllYears, + }) +}) diff --git a/packages/api/src/routes/auth/$idOrganization/years/yearsRoutes.ts b/packages/api/src/routes/auth/organizations/$idOrganization/years/yearsRoutes.ts similarity index 77% rename from packages/api/src/routes/auth/$idOrganization/years/yearsRoutes.ts rename to packages/api/src/routes/auth/organizations/$idOrganization/years/yearsRoutes.ts index 63be947b..f300ee72 100644 --- a/packages/api/src/routes/auth/$idOrganization/years/yearsRoutes.ts +++ b/packages/api/src/routes/auth/organizations/$idOrganization/years/yearsRoutes.ts @@ -1,4 +1,4 @@ -import { $idYearRoutes } from "../../$idYear/$idYearRoutes.js" +import { $idYearRoutes } from "./$idYear/$idYearRoutes.js" import { createOneYearRoute } from "./createOneYear.js" import { readAllYearsRoute } from "./readAllYears.js" diff --git a/packages/api/src/routes/auth/organizations/activateOrganizationMembership.ts b/packages/api/src/routes/auth/organizations/activateOrganizationMembership.ts index c72ad7ce..a7a9673f 100644 --- a/packages/api/src/routes/auth/organizations/activateOrganizationMembership.ts +++ b/packages/api/src/routes/auth/organizations/activateOrganizationMembership.ts @@ -1,6 +1,6 @@ import { activateOrganizationMembershipRouteDefinition, models } from "@arrhes/application-metadata" import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../middlewares/checkUserSessionMiddleware.js" +import { checkAuthMiddleware } from "../../../middlewares/checkAuthMiddleware.js" import { validateBodyMiddleware } from "../../../middlewares/validateBody.middleware.js" import { apiFactory } from "../../../utilities/apiFactory.js" import { response } from "../../../utilities/response.js" @@ -9,7 +9,7 @@ import { updateOne } from "../../../utilities/sql/updateOne.js" export const activateOrganizationMembershipRoute = apiFactory .createApp() .post(activateOrganizationMembershipRouteDefinition.path, async (c) => { - const { user } = await checkUserSessionMiddleware({ + const { user } = await checkAuthMiddleware({ context: c, }) const body = await validateBodyMiddleware({ diff --git a/packages/api/src/routes/auth/organizations/addNewOrganization.ts b/packages/api/src/routes/auth/organizations/addNewOrganization.ts index d31f851c..68779247 100644 --- a/packages/api/src/routes/auth/organizations/addNewOrganization.ts +++ b/packages/api/src/routes/auth/organizations/addNewOrganization.ts @@ -1,5 +1,5 @@ import { addNewOrganizationRouteDefinition, generateId, models } from "@arrhes/application-metadata" -import { checkUserSessionMiddleware } from "../../../middlewares/checkUserSessionMiddleware.js" +import { checkAuthMiddleware } from "../../../middlewares/checkAuthMiddleware.js" import { validateBodyMiddleware } from "../../../middlewares/validateBody.middleware.js" import { apiFactory } from "../../../utilities/apiFactory.js" import { response } from "../../../utilities/response.js" @@ -8,7 +8,7 @@ import { insertOne } from "../../../utilities/sql/insertOne.js" export const addNewOrganizationRoute = apiFactory .createApp() .post(addNewOrganizationRouteDefinition.path, async (c) => { - const { user } = await checkUserSessionMiddleware({ + const { user } = await checkAuthMiddleware({ context: c, }) const body = await validateBodyMiddleware({ diff --git a/packages/api/src/routes/auth/organizations/getAllMyOrganizations.ts b/packages/api/src/routes/auth/organizations/getAllMyOrganizations.ts index d50ad9d5..040c302e 100644 --- a/packages/api/src/routes/auth/organizations/getAllMyOrganizations.ts +++ b/packages/api/src/routes/auth/organizations/getAllMyOrganizations.ts @@ -1,6 +1,6 @@ import { getAllMyOrganizationsRouteDefinition } from "@arrhes/application-metadata" import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../middlewares/checkUserSessionMiddleware.js" +import { checkAuthMiddleware } from "../../../middlewares/checkAuthMiddleware.js" import { validateBodyMiddleware } from "../../../middlewares/validateBody.middleware.js" import { apiFactory } from "../../../utilities/apiFactory.js" import { response } from "../../../utilities/response.js" @@ -8,7 +8,7 @@ import { response } from "../../../utilities/response.js" export const getAllMyOrganizationsRoute = apiFactory .createApp() .post(getAllMyOrganizationsRouteDefinition.path, async (c) => { - const { user } = await checkUserSessionMiddleware({ + const { user } = await checkAuthMiddleware({ context: c, }) const _body = await validateBodyMiddleware({ diff --git a/packages/api/src/routes/auth/organizations/organizationsRoutes.ts b/packages/api/src/routes/auth/organizations/organizationsRoutes.ts index 52d8f3b8..1d1784e9 100644 --- a/packages/api/src/routes/auth/organizations/organizationsRoutes.ts +++ b/packages/api/src/routes/auth/organizations/organizationsRoutes.ts @@ -1,7 +1,7 @@ -import { $idOrganizationRoutes } from "../../../routes/auth/$idOrganization/$idOrganizationRoutes.js" -import { activateOrganizationMembershipRoute } from "../../../routes/auth/organizations/activateOrganizationMembership.js" -import { addNewOrganizationRoute } from "../../../routes/auth/organizations/addNewOrganization.js" -import { getAllMyOrganizationsRoute } from "../../../routes/auth/organizations/getAllMyOrganizations.js" +import { $idOrganizationRoutes } from "./$idOrganization/$idOrganizationRoutes.js" +import { activateOrganizationMembershipRoute } from "./activateOrganizationMembership.js" +import { addNewOrganizationRoute } from "./addNewOrganization.js" +import { getAllMyOrganizationsRoute } from "./getAllMyOrganizations.js" export const organizationsRoutes = [ activateOrganizationMembershipRoute, diff --git a/packages/api/src/routes/auth/settings/activateUser.ts b/packages/api/src/routes/auth/settings/activateUser.ts index 7ff47716..8e260577 100644 --- a/packages/api/src/routes/auth/settings/activateUser.ts +++ b/packages/api/src/routes/auth/settings/activateUser.ts @@ -1,14 +1,14 @@ import { activateUserRouteDefinition, models } from "@arrhes/application-metadata" import { eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../middlewares/checkUserSessionMiddleware.js" +import { requireCookieSessionMiddleware } from "../../../middlewares/requireCookieSessionMiddleware.js" import { validateBodyMiddleware } from "../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../utilities/apiFactory.js" import { Exception } from "../../../utilities/exception.js" +import { registerRoute } from "../../../utilities/registerRoute.js" import { response } from "../../../utilities/response.js" import { updateOne } from "../../../utilities/sql/updateOne.js" -export const activateUserRoute = apiFactory.createApp().post(activateUserRouteDefinition.path, async (c) => { - const { user } = await checkUserSessionMiddleware({ +export const activateUserRoute = registerRoute(activateUserRouteDefinition, async (c) => { + const { user } = await requireCookieSessionMiddleware({ context: c, }) const body = await validateBodyMiddleware({ diff --git a/packages/api/src/routes/auth/settings/deleteUser.ts b/packages/api/src/routes/auth/settings/deleteUser.ts index 04499443..734b92a3 100644 --- a/packages/api/src/routes/auth/settings/deleteUser.ts +++ b/packages/api/src/routes/auth/settings/deleteUser.ts @@ -1,16 +1,16 @@ import { pbkdf2Sync } from "node:crypto" import { deleteUserRouteDefinition, models } from "@arrhes/application-metadata" import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../middlewares/checkUserSessionMiddleware.js" +import { requireCookieSessionMiddleware } from "../../../middlewares/requireCookieSessionMiddleware.js" import { validateBodyMiddleware } from "../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../utilities/apiFactory.js" import { Exception } from "../../../utilities/exception.js" +import { registerRoute } from "../../../utilities/registerRoute.js" import { response } from "../../../utilities/response.js" import { deleteOne } from "../../../utilities/sql/deleteOne.js" import { selectMany } from "../../../utilities/sql/selectMany.js" -export const deleteUserRoute = apiFactory.createApp().post(deleteUserRouteDefinition.path, async (c) => { - const { user } = await checkUserSessionMiddleware({ +export const deleteUserRoute = registerRoute(deleteUserRouteDefinition, async (c) => { + const { user } = await requireCookieSessionMiddleware({ context: c, }) const body = await validateBodyMiddleware({ diff --git a/packages/api/src/routes/auth/settings/readUserSession.ts b/packages/api/src/routes/auth/settings/readUserSession.ts index 02b38a24..307781fc 100644 --- a/packages/api/src/routes/auth/settings/readUserSession.ts +++ b/packages/api/src/routes/auth/settings/readUserSession.ts @@ -1,10 +1,10 @@ import { readUserSessionRouteDefinition } from "@arrhes/application-metadata" -import { checkUserSessionMiddleware } from "../../../middlewares/checkUserSessionMiddleware.js" -import { apiFactory } from "../../../utilities/apiFactory.js" +import { requireCookieSessionMiddleware } from "../../../middlewares/requireCookieSessionMiddleware.js" +import { registerRoute } from "../../../utilities/registerRoute.js" import { response } from "../../../utilities/response.js" -export const readUserSessionRoute = apiFactory.createApp().post(readUserSessionRouteDefinition.path, async (c) => { - const { user, userSession } = await checkUserSessionMiddleware({ +export const readUserSessionRoute = registerRoute(readUserSessionRouteDefinition, async (c) => { + const { user, userSession } = await requireCookieSessionMiddleware({ context: c, }) diff --git a/packages/api/src/routes/auth/settings/resendEmailValidation.ts b/packages/api/src/routes/auth/settings/resendEmailValidation.ts index 3bd3112a..e4fe4d53 100644 --- a/packages/api/src/routes/auth/settings/resendEmailValidation.ts +++ b/packages/api/src/routes/auth/settings/resendEmailValidation.ts @@ -1,6 +1,6 @@ import { models, resendEmailValidationRouteDefinition } from "@arrhes/application-metadata" import { eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../middlewares/checkUserSessionMiddleware.js" +import { requireCookieSessionMiddleware } from "../../../middlewares/requireCookieSessionMiddleware.js" import { apiFactory } from "../../../utilities/apiFactory.js" import { sendEmail } from "../../../utilities/email/sendEmail.js" import { emailValidationTemplate } from "../../../utilities/email/templates/emailValidation.js" @@ -12,7 +12,7 @@ import { updateOne } from "../../../utilities/sql/updateOne.js" export const resendEmailValidationRoute = apiFactory .createApp() .post(resendEmailValidationRouteDefinition.path, async (c) => { - const { user } = await checkUserSessionMiddleware({ + const { user } = await requireCookieSessionMiddleware({ context: c, }) diff --git a/packages/api/src/routes/auth/settings/settingsRoutes.ts b/packages/api/src/routes/auth/settings/settingsRoutes.ts index 56b3335c..53a22b74 100644 --- a/packages/api/src/routes/auth/settings/settingsRoutes.ts +++ b/packages/api/src/routes/auth/settings/settingsRoutes.ts @@ -1,11 +1,11 @@ -import { activateUserRoute } from "../../../routes/auth/settings/activateUser.js" -import { deleteUserRoute } from "../../../routes/auth/settings/deleteUser.js" -import { readUserSessionRoute } from "../../../routes/auth/settings/readUserSession.js" -import { resendEmailValidationRoute } from "../../../routes/auth/settings/resendEmailValidation.js" -import { updateUserRoute } from "../../../routes/auth/settings/updateUser.js" -import { updateUserEmailRoute } from "../../../routes/auth/settings/updateUserEmail.js" -import { updateUserPasswordRoute } from "../../../routes/auth/settings/updateUserPassword.js" -import { validateUserEmailRoute } from "../../../routes/auth/settings/validateUserEmail.js" +import { activateUserRoute } from "./activateUser.js" +import { deleteUserRoute } from "./deleteUser.js" +import { readUserSessionRoute } from "./readUserSession.js" +import { resendEmailValidationRoute } from "./resendEmailValidation.js" +import { updateUserRoute } from "./updateUser.js" +import { updateUserEmailRoute } from "./updateUserEmail.js" +import { updateUserPasswordRoute } from "./updateUserPassword.js" +import { validateUserEmailRoute } from "./validateUserEmail.js" export const settingsRoutes = [ activateUserRoute, diff --git a/packages/api/src/routes/auth/settings/updateUser.ts b/packages/api/src/routes/auth/settings/updateUser.ts index 415add53..0329124d 100644 --- a/packages/api/src/routes/auth/settings/updateUser.ts +++ b/packages/api/src/routes/auth/settings/updateUser.ts @@ -1,13 +1,13 @@ import { models, updateUserRouteDefinition } from "@arrhes/application-metadata" import { eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../middlewares/checkUserSessionMiddleware.js" +import { requireCookieSessionMiddleware } from "../../../middlewares/requireCookieSessionMiddleware.js" import { validateBodyMiddleware } from "../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../utilities/apiFactory.js" +import { registerRoute } from "../../../utilities/registerRoute.js" import { response } from "../../../utilities/response.js" import { updateOne } from "../../../utilities/sql/updateOne.js" -export const updateUserRoute = apiFactory.createApp().post(updateUserRouteDefinition.path, async (c) => { - const { user } = await checkUserSessionMiddleware({ +export const updateUserRoute = registerRoute(updateUserRouteDefinition, async (c) => { + const { user } = await requireCookieSessionMiddleware({ context: c, }) const body = await validateBodyMiddleware({ @@ -20,6 +20,7 @@ export const updateUserRoute = apiFactory.createApp().post(updateUserRouteDefini table: models.user, data: { alias: body.alias, + dashboardMode: body.dashboardMode, lastUpdatedAt: new Date().toISOString(), }, where: (table) => eq(table.id, user.id), diff --git a/packages/api/src/routes/auth/settings/updateUserEmail.ts b/packages/api/src/routes/auth/settings/updateUserEmail.ts index b0fa9f48..316e2283 100644 --- a/packages/api/src/routes/auth/settings/updateUserEmail.ts +++ b/packages/api/src/routes/auth/settings/updateUserEmail.ts @@ -1,18 +1,18 @@ import { pbkdf2Sync } from "node:crypto" import { models, updateUserEmailRouteDefinition } from "@arrhes/application-metadata" import { eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../middlewares/checkUserSessionMiddleware.js" +import { requireCookieSessionMiddleware } from "../../../middlewares/requireCookieSessionMiddleware.js" import { validateBodyMiddleware } from "../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../utilities/apiFactory.js" import { sendEmail } from "../../../utilities/email/sendEmail.js" import { emailValidationTemplate } from "../../../utilities/email/templates/emailValidation.js" import { Exception } from "../../../utilities/exception.js" import { generateVerificationToken } from "../../../utilities/generateVerificationToken.js" +import { registerRoute } from "../../../utilities/registerRoute.js" import { response } from "../../../utilities/response.js" import { updateOne } from "../../../utilities/sql/updateOne.js" -export const updateUserEmailRoute = apiFactory.createApp().post(updateUserEmailRouteDefinition.path, async (c) => { - const { user } = await checkUserSessionMiddleware({ +export const updateUserEmailRoute = registerRoute(updateUserEmailRouteDefinition, async (c) => { + const { user } = await requireCookieSessionMiddleware({ context: c, }) const body = await validateBodyMiddleware({ diff --git a/packages/api/src/routes/auth/settings/updateUserPassword.ts b/packages/api/src/routes/auth/settings/updateUserPassword.ts index 55867b28..ba261755 100644 --- a/packages/api/src/routes/auth/settings/updateUserPassword.ts +++ b/packages/api/src/routes/auth/settings/updateUserPassword.ts @@ -1,7 +1,7 @@ import { pbkdf2Sync } from "node:crypto" import { models, updateUserPasswordRouteDefinition } from "@arrhes/application-metadata" import { eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../middlewares/checkUserSessionMiddleware.js" +import { requireCookieSessionMiddleware } from "../../../middlewares/requireCookieSessionMiddleware.js" import { validateBodyMiddleware } from "../../../middlewares/validateBody.middleware.js" import { apiFactory } from "../../../utilities/apiFactory.js" import { Exception } from "../../../utilities/exception.js" @@ -11,7 +11,7 @@ import { updateOne } from "../../../utilities/sql/updateOne.js" export const updateUserPasswordRoute = apiFactory .createApp() .post(updateUserPasswordRouteDefinition.path, async (c) => { - const { user } = await checkUserSessionMiddleware({ + const { user } = await requireCookieSessionMiddleware({ context: c, }) const body = await validateBodyMiddleware({ diff --git a/packages/api/src/routes/auth/settings/validateUserEmail.ts b/packages/api/src/routes/auth/settings/validateUserEmail.ts index 4f962891..05df5732 100644 --- a/packages/api/src/routes/auth/settings/validateUserEmail.ts +++ b/packages/api/src/routes/auth/settings/validateUserEmail.ts @@ -1,14 +1,14 @@ import { models, validateUserEmailRouteDefinition } from "@arrhes/application-metadata" import { eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../middlewares/checkUserSessionMiddleware.js" +import { requireCookieSessionMiddleware } from "../../../middlewares/requireCookieSessionMiddleware.js" import { validateBodyMiddleware } from "../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../utilities/apiFactory.js" import { Exception } from "../../../utilities/exception.js" +import { registerRoute } from "../../../utilities/registerRoute.js" import { response } from "../../../utilities/response.js" import { updateOne } from "../../../utilities/sql/updateOne.js" -export const validateUserEmailRoute = apiFactory.createApp().post(validateUserEmailRouteDefinition.path, async (c) => { - const { user } = await checkUserSessionMiddleware({ +export const validateUserEmailRoute = registerRoute(validateUserEmailRouteDefinition, async (c) => { + const { user } = await requireCookieSessionMiddleware({ context: c, }) const body = await validateBodyMiddleware({ diff --git a/packages/api/src/routes/auth/support/createOneTicket.ts b/packages/api/src/routes/auth/support/createOneTicket.ts index e6c15314..1e31ab67 100644 --- a/packages/api/src/routes/auth/support/createOneTicket.ts +++ b/packages/api/src/routes/auth/support/createOneTicket.ts @@ -1,14 +1,14 @@ import { createOneTicketRouteDefinition, generateId, models } from "@arrhes/application-metadata" -import { checkUserSessionMiddleware } from "../../../middlewares/checkUserSessionMiddleware.js" +import { checkAuthMiddleware } from "../../../middlewares/checkAuthMiddleware.js" import { validateBodyMiddleware } from "../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../utilities/apiFactory.js" import { sendEmail } from "../../../utilities/email/sendEmail.js" import { supportTemplate } from "../../../utilities/email/templates/support.js" +import { registerRoute } from "../../../utilities/registerRoute.js" import { response } from "../../../utilities/response.js" import { insertOne } from "../../../utilities/sql/insertOne.js" -export const createOneTicketRoute = apiFactory.createApp().post(createOneTicketRouteDefinition.path, async (c) => { - const { user } = await checkUserSessionMiddleware({ +export const createOneTicketRoute = registerRoute(createOneTicketRouteDefinition, async (c) => { + const { user } = await checkAuthMiddleware({ context: c, }) const body = await validateBodyMiddleware({ diff --git a/packages/api/src/routes/auth/support/createOneTicketMessage.ts b/packages/api/src/routes/auth/support/createOneTicketMessage.ts index b270490b..f9889a7d 100644 --- a/packages/api/src/routes/auth/support/createOneTicketMessage.ts +++ b/packages/api/src/routes/auth/support/createOneTicketMessage.ts @@ -1,6 +1,6 @@ import { createOneTicketMessageRouteDefinition, generateId, models } from "@arrhes/application-metadata" import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../middlewares/checkUserSessionMiddleware.js" +import { checkAuthMiddleware } from "../../../middlewares/checkAuthMiddleware.js" import { validateBodyMiddleware } from "../../../middlewares/validateBody.middleware.js" import { apiFactory } from "../../../utilities/apiFactory.js" import { response } from "../../../utilities/response.js" @@ -11,7 +11,7 @@ import { updateOne } from "../../../utilities/sql/updateOne.js" export const createOneTicketMessageRoute = apiFactory .createApp() .post(createOneTicketMessageRouteDefinition.path, async (c) => { - const { user } = await checkUserSessionMiddleware({ + const { user } = await checkAuthMiddleware({ context: c, }) const body = await validateBodyMiddleware({ diff --git a/packages/api/src/routes/auth/support/readAllTicketMessages.ts b/packages/api/src/routes/auth/support/readAllTicketMessages.ts index e5c12b7f..9c25b3af 100644 --- a/packages/api/src/routes/auth/support/readAllTicketMessages.ts +++ b/packages/api/src/routes/auth/support/readAllTicketMessages.ts @@ -1,6 +1,6 @@ import { models, readAllTicketMessagesRouteDefinition } from "@arrhes/application-metadata" import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../middlewares/checkUserSessionMiddleware.js" +import { checkAuthMiddleware } from "../../../middlewares/checkAuthMiddleware.js" import { validateBodyMiddleware } from "../../../middlewares/validateBody.middleware.js" import { apiFactory } from "../../../utilities/apiFactory.js" import { response } from "../../../utilities/response.js" @@ -10,7 +10,7 @@ import { selectOne } from "../../../utilities/sql/selectOne.js" export const readAllTicketMessagesRoute = apiFactory .createApp() .post(readAllTicketMessagesRouteDefinition.path, async (c) => { - const { user } = await checkUserSessionMiddleware({ + const { user } = await checkAuthMiddleware({ context: c, }) const body = await validateBodyMiddleware({ diff --git a/packages/api/src/routes/auth/support/readAllTickets.ts b/packages/api/src/routes/auth/support/readAllTickets.ts index bb4ccd77..028dc0ce 100644 --- a/packages/api/src/routes/auth/support/readAllTickets.ts +++ b/packages/api/src/routes/auth/support/readAllTickets.ts @@ -1,13 +1,13 @@ import { models, readAllTicketsRouteDefinition } from "@arrhes/application-metadata" import { eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../middlewares/checkUserSessionMiddleware.js" +import { checkAuthMiddleware } from "../../../middlewares/checkAuthMiddleware.js" import { validateBodyMiddleware } from "../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../utilities/apiFactory.js" +import { registerRoute } from "../../../utilities/registerRoute.js" import { response } from "../../../utilities/response.js" import { selectMany } from "../../../utilities/sql/selectMany.js" -export const readAllTicketsRoute = apiFactory.createApp().post(readAllTicketsRouteDefinition.path, async (c) => { - const { user } = await checkUserSessionMiddleware({ +export const readAllTicketsRoute = registerRoute(readAllTicketsRouteDefinition, async (c) => { + const { user } = await checkAuthMiddleware({ context: c, }) await validateBodyMiddleware({ diff --git a/packages/api/src/routes/auth/support/readOneTicket.ts b/packages/api/src/routes/auth/support/readOneTicket.ts index ea77515b..6589ae86 100644 --- a/packages/api/src/routes/auth/support/readOneTicket.ts +++ b/packages/api/src/routes/auth/support/readOneTicket.ts @@ -1,13 +1,13 @@ import { models, readOneTicketRouteDefinition } from "@arrhes/application-metadata" import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../middlewares/checkUserSessionMiddleware.js" +import { checkAuthMiddleware } from "../../../middlewares/checkAuthMiddleware.js" import { validateBodyMiddleware } from "../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../utilities/apiFactory.js" +import { registerRoute } from "../../../utilities/registerRoute.js" import { response } from "../../../utilities/response.js" import { selectOne } from "../../../utilities/sql/selectOne.js" -export const readOneTicketRoute = apiFactory.createApp().post(readOneTicketRouteDefinition.path, async (c) => { - const { user } = await checkUserSessionMiddleware({ +export const readOneTicketRoute = registerRoute(readOneTicketRouteDefinition, async (c) => { + const { user } = await checkAuthMiddleware({ context: c, }) const body = await validateBodyMiddleware({ diff --git a/packages/api/src/routes/auth/support/updateOneTicket.ts b/packages/api/src/routes/auth/support/updateOneTicket.ts index 7df31e32..b11a6c2b 100644 --- a/packages/api/src/routes/auth/support/updateOneTicket.ts +++ b/packages/api/src/routes/auth/support/updateOneTicket.ts @@ -1,14 +1,14 @@ import { models, updateOneTicketRouteDefinition } from "@arrhes/application-metadata" import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../middlewares/checkUserSessionMiddleware.js" +import { checkAuthMiddleware } from "../../../middlewares/checkAuthMiddleware.js" import { validateBodyMiddleware } from "../../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../../utilities/apiFactory.js" +import { registerRoute } from "../../../utilities/registerRoute.js" import { response } from "../../../utilities/response.js" import { selectOne } from "../../../utilities/sql/selectOne.js" import { updateOne } from "../../../utilities/sql/updateOne.js" -export const updateOneTicketRoute = apiFactory.createApp().post(updateOneTicketRouteDefinition.path, async (c) => { - const { user } = await checkUserSessionMiddleware({ +export const updateOneTicketRoute = registerRoute(updateOneTicketRouteDefinition, async (c) => { + const { user } = await checkAuthMiddleware({ context: c, }) const body = await validateBodyMiddleware({ diff --git a/packages/api/src/routes/auth/support/updateOneTicketStatus.ts b/packages/api/src/routes/auth/support/updateOneTicketStatus.ts index d604152b..b4e40a72 100644 --- a/packages/api/src/routes/auth/support/updateOneTicketStatus.ts +++ b/packages/api/src/routes/auth/support/updateOneTicketStatus.ts @@ -1,6 +1,6 @@ import { models, updateOneTicketStatusRouteDefinition } from "@arrhes/application-metadata" import { and, eq } from "drizzle-orm" -import { checkUserSessionMiddleware } from "../../../middlewares/checkUserSessionMiddleware.js" +import { checkAuthMiddleware } from "../../../middlewares/checkAuthMiddleware.js" import { validateBodyMiddleware } from "../../../middlewares/validateBody.middleware.js" import { apiFactory } from "../../../utilities/apiFactory.js" import { response } from "../../../utilities/response.js" @@ -10,7 +10,7 @@ import { updateOne } from "../../../utilities/sql/updateOne.js" export const updateOneTicketStatusRoute = apiFactory .createApp() .post(updateOneTicketStatusRouteDefinition.path, async (c) => { - const { user } = await checkUserSessionMiddleware({ + const { user } = await checkAuthMiddleware({ context: c, }) const body = await validateBodyMiddleware({ diff --git a/packages/api/src/routes/public/mollieWebhook.ts b/packages/api/src/routes/public/mollieWebhook.ts index 7d0f0d95..b950c958 100644 --- a/packages/api/src/routes/public/mollieWebhook.ts +++ b/packages/api/src/routes/public/mollieWebhook.ts @@ -1,12 +1,12 @@ import { models, mollieWebhookRouteDefinition } from "@arrhes/application-metadata" import { eq } from "drizzle-orm" -import { apiFactory } from "../../utilities/apiFactory.js" import { apiLog } from "../../utilities/apiLog.js" +import { registerRoute } from "../../utilities/registerRoute.js" import { response } from "../../utilities/response.js" import { updateOne } from "../../utilities/sql/updateOne.js" import { validate } from "../../utilities/validate.js" -export const mollieWebhookRoute = apiFactory.createApp().post(mollieWebhookRouteDefinition.path, async (c) => { +export const mollieWebhookRoute = registerRoute(mollieWebhookRouteDefinition, async (c) => { try { // Mollie sends webhooks as application/x-www-form-urlencoded (body: id=tr_xxx) const rawBody = await c.req.parseBody() diff --git a/packages/api/src/routes/public/publicRoute.ts b/packages/api/src/routes/public/publicRoute.ts index f0c3d9e3..90a4f1fd 100644 --- a/packages/api/src/routes/public/publicRoute.ts +++ b/packages/api/src/routes/public/publicRoute.ts @@ -1,10 +1,10 @@ -import { mollieWebhookRoute } from "../../routes/public/mollieWebhook.js" -import { resetPasswordRoute } from "../../routes/public/resetPassword.js" -import { sendMagicLinkRoute } from "../../routes/public/sendMagicLink.js" -import { signInRoute } from "../../routes/public/signIn.js" -import { signOutRoute } from "../../routes/public/signOut.js" -import { signUpRoute } from "../../routes/public/signUp.js" import { apiFactory } from "../../utilities/apiFactory.js" +import { mollieWebhookRoute } from "./mollieWebhook.js" +import { resetPasswordRoute } from "./user/resetPassword.js" +import { sendMagicLinkRoute } from "./user/sendMagicLink.js" +import { signInRoute } from "./user/signIn.js" +import { signOutRoute } from "./user/signOut.js" +import { signUpRoute } from "./user/signUp.js" export const publicRoute = apiFactory .createApp() diff --git a/packages/api/src/routes/public/resetPassword.ts b/packages/api/src/routes/public/resetPassword.ts deleted file mode 100644 index 4a44af5d..00000000 --- a/packages/api/src/routes/public/resetPassword.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { pbkdf2Sync, randomBytes } from "node:crypto" -import { models, resetPasswordRouteDefinition } from "@arrhes/application-metadata" -import { eq } from "drizzle-orm" -import { validateBodyMiddleware } from "../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../utilities/apiFactory.js" -import { sendEmail } from "../../utilities/email/sendEmail.js" -import { resetPasswordTemplate } from "../../utilities/email/templates/resetPassword.js" -import { response } from "../../utilities/response.js" -import { selectOne } from "../../utilities/sql/selectOne.js" -import { updateOne } from "../../utilities/sql/updateOne.js" - -function generateTemporaryPassword() { - return randomBytes(12).toString("base64url") -} - -export const resetPasswordRoute = apiFactory.createApp().post(resetPasswordRouteDefinition.path, async (c) => { - const body = await validateBodyMiddleware({ - context: c, - schema: resetPasswordRouteDefinition.schemas.body, - }) - - const user = await selectOne({ - database: c.var.clients.sql, - table: models.user, - where: (table) => eq(table.email, body.email.trim().toLowerCase()), - }) - - const temporaryPassword = generateTemporaryPassword() - const passwordSalt = randomBytes(32).toString("hex") - const passwordHash = pbkdf2Sync(temporaryPassword, passwordSalt, 128000, 64, "sha512").toString("hex") - - await updateOne({ - database: c.var.clients.sql, - table: models.user, - data: { - passwordHash, - passwordSalt, - lastUpdatedAt: new Date().toISOString(), - }, - where: (table) => eq(table.id, user.id), - }) - - await sendEmail({ - var: c.var, - to: user.email, - subject: "Réinitialisation de votre mot de passe", - html: resetPasswordTemplate({ - newPassword: temporaryPassword, - }), - }) - - return response({ - context: c, - statusCode: 200, - schema: resetPasswordRouteDefinition.schemas.return, - data: {}, - }) -}) diff --git a/packages/api/src/routes/public/sendMagicLink.ts b/packages/api/src/routes/public/sendMagicLink.ts deleted file mode 100644 index 0e09f00d..00000000 --- a/packages/api/src/routes/public/sendMagicLink.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { models, sendMagicLinkRouteDefinition } from "@arrhes/application-metadata" -import { eq } from "drizzle-orm" -import { validateBodyMiddleware } from "../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../utilities/apiFactory.js" -import { response } from "../../utilities/response.js" -import { selectOne } from "../../utilities/sql/selectOne.js" - -export const sendMagicLinkRoute = apiFactory.createApp().post(sendMagicLinkRouteDefinition.path, async (c) => { - const body = await validateBodyMiddleware({ - context: c, - schema: sendMagicLinkRouteDefinition.schemas.body, - }) - - const _user = await selectOne({ - database: c.var.clients.sql, - table: models.user, - where: (table) => eq(table.email, body.email.trim().toLowerCase()), - }) - - return response({ - context: c, - statusCode: 200, - schema: sendMagicLinkRouteDefinition.schemas.return, - data: {}, - }) -}) diff --git a/packages/api/src/routes/public/signOut.ts b/packages/api/src/routes/public/signOut.ts deleted file mode 100644 index 43aeb898..00000000 --- a/packages/api/src/routes/public/signOut.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { models, signOutRouteDefinition } from "@arrhes/application-metadata" -import { eq } from "drizzle-orm" -import { validateBodyMiddleware } from "../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../utilities/apiFactory.js" -import { getCookieDomainFromHost } from "../../utilities/cookies/getCookieDomainFromHost.js" -import { parseCookies } from "../../utilities/cookies/parseCookies.js" -import { serializeCookie } from "../../utilities/cookies/serializeCookie.js" -import { unsignString } from "../../utilities/cookies/unsignString.js" -import { Exception } from "../../utilities/exception.js" -import { response } from "../../utilities/response.js" -import { updateOne } from "../../utilities/sql/updateOne.js" -import { getCookieSecurityOptions, productName, userSessionCookieMaxAge } from "../../utilities/variables.js" - -export const signOutRoute = apiFactory.createApp().post(signOutRouteDefinition.path, async (c) => { - const _body = await validateBodyMiddleware({ - context: c, - schema: signOutRouteDefinition.schemas.body, - }) - - try { - const idUserSession = unsignString({ - signedValue: parseCookies({ - value: c.req.header("cookie"), - })[`${productName}_${"id_user_session"}`], - secret: c.var.env.COOKIES_KEY, - }) - - if (idUserSession === undefined) { - throw new Exception({ - statusCode: 401, - internalMessage: "Invalid session", - cause: "idUserSession not found in signed cookie", - }) - } - - await updateOne({ - database: c.var.clients.sql, - table: models.userSession, - data: { - lastUpdatedAt: new Date().toISOString(), - isActive: false, - }, - where: (table) => eq(table.id, idUserSession), - }) - } catch (_erro: unknown) { - // do nothing - } - - const cookieSecurity = getCookieSecurityOptions(c.var.env.ENV) - const cookieDomain = getCookieDomainFromHost({ - hostHeader: c.req.header("host"), - fallbackDomain: c.var.env.COOKIES_DOMAIN, - }) - c.res.headers.append( - "Set-Cookie", - serializeCookie({ - name: `${productName}_${"id_user_session"}`, - value: "", - options: { - maxAge: userSessionCookieMaxAge, - httpOnly: true, - ...cookieSecurity, - domain: cookieDomain, - path: "/", - }, - }), - ) - c.res.headers.append( - "Set-Cookie", - serializeCookie({ - name: `${productName}_${"is_auth"}`, - value: String(false), - options: { - maxAge: userSessionCookieMaxAge, - httpOnly: false, - ...cookieSecurity, - domain: cookieDomain, - path: "/", - }, - }), - ) - - return response({ - context: c, - statusCode: 200, - schema: signOutRouteDefinition.schemas.return, - data: {}, - }) -}) diff --git a/packages/api/src/routes/public/user/resetPassword.ts b/packages/api/src/routes/public/user/resetPassword.ts new file mode 100644 index 00000000..7ca64913 --- /dev/null +++ b/packages/api/src/routes/public/user/resetPassword.ts @@ -0,0 +1,58 @@ +import { pbkdf2Sync, randomBytes } from "node:crypto" +import { models, resetPasswordRouteDefinition } from "@arrhes/application-metadata" +import { eq } from "drizzle-orm" +import { validateBodyMiddleware } from "../../../middlewares/validateBody.middleware.js" +import { sendEmail } from "../../../utilities/email/sendEmail.js" +import { resetPasswordTemplate } from "../../../utilities/email/templates/resetPassword.js" +import { registerRoute } from "../../../utilities/registerRoute.js" +import { response } from "../../../utilities/response.js" +import { selectOne } from "../../../utilities/sql/selectOne.js" +import { updateOne } from "../../../utilities/sql/updateOne.js" + +function generateTemporaryPassword() { + return randomBytes(12).toString("base64url") +} + +export const resetPasswordRoute = registerRoute(resetPasswordRouteDefinition, async (c) => { + const body = await validateBodyMiddleware({ + context: c, + schema: resetPasswordRouteDefinition.schemas.body, + }) + + const user = await selectOne({ + database: c.var.clients.sql, + table: models.user, + where: (table) => eq(table.email, body.email.trim().toLowerCase()), + }) + + const temporaryPassword = generateTemporaryPassword() + const passwordSalt = randomBytes(32).toString("hex") + const passwordHash = pbkdf2Sync(temporaryPassword, passwordSalt, 128000, 64, "sha512").toString("hex") + + await updateOne({ + database: c.var.clients.sql, + table: models.user, + data: { + passwordHash, + passwordSalt, + lastUpdatedAt: new Date().toISOString(), + }, + where: (table) => eq(table.id, user.id), + }) + + await sendEmail({ + var: c.var, + to: user.email, + subject: "Réinitialisation de votre mot de passe", + html: resetPasswordTemplate({ + newPassword: temporaryPassword, + }), + }) + + return response({ + context: c, + statusCode: 200, + schema: resetPasswordRouteDefinition.schemas.return, + data: {}, + }) +}) diff --git a/packages/api/src/routes/public/user/sendMagicLink.ts b/packages/api/src/routes/public/user/sendMagicLink.ts new file mode 100644 index 00000000..6fb74084 --- /dev/null +++ b/packages/api/src/routes/public/user/sendMagicLink.ts @@ -0,0 +1,26 @@ +import { models, sendMagicLinkRouteDefinition } from "@arrhes/application-metadata" +import { eq } from "drizzle-orm" +import { validateBodyMiddleware } from "../../../middlewares/validateBody.middleware.js" +import { registerRoute } from "../../../utilities/registerRoute.js" +import { response } from "../../../utilities/response.js" +import { selectOne } from "../../../utilities/sql/selectOne.js" + +export const sendMagicLinkRoute = registerRoute(sendMagicLinkRouteDefinition, async (c) => { + const body = await validateBodyMiddleware({ + context: c, + schema: sendMagicLinkRouteDefinition.schemas.body, + }) + + const _user = await selectOne({ + database: c.var.clients.sql, + table: models.user, + where: (table) => eq(table.email, body.email.trim().toLowerCase()), + }) + + return response({ + context: c, + statusCode: 200, + schema: sendMagicLinkRouteDefinition.schemas.return, + data: {}, + }) +}) diff --git a/packages/api/src/routes/public/signIn.ts b/packages/api/src/routes/public/user/signIn.ts similarity index 76% rename from packages/api/src/routes/public/signIn.ts rename to packages/api/src/routes/public/user/signIn.ts index 0e403f03..a403191f 100644 --- a/packages/api/src/routes/public/signIn.ts +++ b/packages/api/src/routes/public/user/signIn.ts @@ -1,19 +1,19 @@ import { pbkdf2Sync } from "node:crypto" import { generateId, models, signInRouteDefinition } from "@arrhes/application-metadata" import { eq } from "drizzle-orm" -import { validateBodyMiddleware } from "../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../utilities/apiFactory.js" -import { getCookieDomainFromHost } from "../../utilities/cookies/getCookieDomainFromHost.js" -import { serializeCookie } from "../../utilities/cookies/serializeCookie.js" -import { signString } from "../../utilities/cookies/signString.js" -import { Exception } from "../../utilities/exception.js" -import { getRemoteAddress } from "../../utilities/getRemoteAddress.js" -import { response } from "../../utilities/response.js" -import { insertOne } from "../../utilities/sql/insertOne.js" -import { selectOne } from "../../utilities/sql/selectOne.js" -import { getCookieSecurityOptions, productName, userSessionCookieMaxAge } from "../../utilities/variables.js" +import { validateBodyMiddleware } from "../../../middlewares/validateBody.middleware.js" +import { getCookieDomainFromHost } from "../../../utilities/cookies/getCookieDomainFromHost.js" +import { serializeCookie } from "../../../utilities/cookies/serializeCookie.js" +import { signString } from "../../../utilities/cookies/signString.js" +import { Exception } from "../../../utilities/exception.js" +import { getRemoteAddress } from "../../../utilities/getRemoteAddress.js" +import { registerRoute } from "../../../utilities/registerRoute.js" +import { response } from "../../../utilities/response.js" +import { insertOne } from "../../../utilities/sql/insertOne.js" +import { selectOne } from "../../../utilities/sql/selectOne.js" +import { getCookieSecurityOptions, productName, userSessionCookieMaxAge } from "../../../utilities/variables.js" -export const signInRoute = apiFactory.createApp().post(signInRouteDefinition.path, async (c) => { +export const signInRoute = registerRoute(signInRouteDefinition, async (c) => { const body = await validateBodyMiddleware({ context: c, schema: signInRouteDefinition.schemas.body, diff --git a/packages/api/src/routes/public/user/signOut.ts b/packages/api/src/routes/public/user/signOut.ts new file mode 100644 index 00000000..405b76c3 --- /dev/null +++ b/packages/api/src/routes/public/user/signOut.ts @@ -0,0 +1,89 @@ +import { models, signOutRouteDefinition } from "@arrhes/application-metadata" +import { eq } from "drizzle-orm" +import { validateBodyMiddleware } from "../../../middlewares/validateBody.middleware.js" +import { getCookieDomainFromHost } from "../../../utilities/cookies/getCookieDomainFromHost.js" +import { parseCookies } from "../../../utilities/cookies/parseCookies.js" +import { serializeCookie } from "../../../utilities/cookies/serializeCookie.js" +import { unsignString } from "../../../utilities/cookies/unsignString.js" +import { Exception } from "../../../utilities/exception.js" +import { registerRoute } from "../../../utilities/registerRoute.js" +import { response } from "../../../utilities/response.js" +import { updateOne } from "../../../utilities/sql/updateOne.js" +import { getCookieSecurityOptions, productName } from "../../../utilities/variables.js" + +export const signOutRoute = registerRoute(signOutRouteDefinition, async (c) => { + const _body = await validateBodyMiddleware({ + context: c, + schema: signOutRouteDefinition.schemas.body, + }) + + try { + const idUserSession = unsignString({ + signedValue: parseCookies({ + value: c.req.header("cookie"), + })[`${productName}_${"id_user_session"}`], + secret: c.var.env.COOKIES_KEY, + }) + + if (idUserSession === undefined) { + throw new Exception({ + statusCode: 401, + internalMessage: "Invalid session", + cause: "idUserSession not found in signed cookie", + }) + } + + await updateOne({ + database: c.var.clients.sql, + table: models.userSession, + data: { + lastUpdatedAt: new Date().toISOString(), + isActive: false, + }, + where: (table) => eq(table.id, idUserSession), + }) + } catch (_erro: unknown) { + // do nothing + } + + const cookieSecurity = getCookieSecurityOptions(c.var.env.ENV) + const cookieDomain = getCookieDomainFromHost({ + hostHeader: c.req.header("host"), + fallbackDomain: c.var.env.COOKIES_DOMAIN, + }) + c.res.headers.append( + "Set-Cookie", + serializeCookie({ + name: `${productName}_${"id_user_session"}`, + value: "", + options: { + maxAge: 0, + httpOnly: true, + ...cookieSecurity, + domain: cookieDomain, + path: "/", + }, + }), + ) + c.res.headers.append( + "Set-Cookie", + serializeCookie({ + name: `${productName}_${"is_auth"}`, + value: "", + options: { + maxAge: 0, + httpOnly: false, + ...cookieSecurity, + domain: cookieDomain, + path: "/", + }, + }), + ) + + return response({ + context: c, + statusCode: 200, + schema: signOutRouteDefinition.schemas.return, + data: {}, + }) +}) diff --git a/packages/api/src/routes/public/signUp.ts b/packages/api/src/routes/public/user/signUp.ts similarity index 80% rename from packages/api/src/routes/public/signUp.ts rename to packages/api/src/routes/public/user/signUp.ts index 225f351a..77d0008e 100644 --- a/packages/api/src/routes/public/signUp.ts +++ b/packages/api/src/routes/public/user/signUp.ts @@ -1,18 +1,18 @@ import { pbkdf2Sync } from "node:crypto" import { generateId, models, signUpRouteDefinition } from "@arrhes/application-metadata" -import { validateBodyMiddleware } from "../../middlewares/validateBody.middleware.js" -import { apiFactory } from "../../utilities/apiFactory.js" -import { getCookieDomainFromHost } from "../../utilities/cookies/getCookieDomainFromHost.js" -import { serializeCookie } from "../../utilities/cookies/serializeCookie.js" -import { signString } from "../../utilities/cookies/signString.js" -import { Exception } from "../../utilities/exception.js" -import { generateVerificationToken } from "../../utilities/generateVerificationToken.js" -import { getRemoteAddress } from "../../utilities/getRemoteAddress.js" -import { response } from "../../utilities/response.js" -import { insertOne } from "../../utilities/sql/insertOne.js" -import { getCookieSecurityOptions, productName, userSessionCookieMaxAge } from "../../utilities/variables.js" +import { validateBodyMiddleware } from "../../../middlewares/validateBody.middleware.js" +import { getCookieDomainFromHost } from "../../../utilities/cookies/getCookieDomainFromHost.js" +import { serializeCookie } from "../../../utilities/cookies/serializeCookie.js" +import { signString } from "../../../utilities/cookies/signString.js" +import { Exception } from "../../../utilities/exception.js" +import { generateVerificationToken } from "../../../utilities/generateVerificationToken.js" +import { getRemoteAddress } from "../../../utilities/getRemoteAddress.js" +import { registerRoute } from "../../../utilities/registerRoute.js" +import { response } from "../../../utilities/response.js" +import { insertOne } from "../../../utilities/sql/insertOne.js" +import { getCookieSecurityOptions, productName, userSessionCookieMaxAge } from "../../../utilities/variables.js" -export const signUpRoute = apiFactory.createApp().post(signUpRouteDefinition.path, async (c) => { +export const signUpRoute = registerRoute(signUpRouteDefinition, async (c) => { const body = await validateBodyMiddleware({ context: c, schema: signUpRouteDefinition.schemas.body, diff --git a/packages/api/src/routes/routes.ts b/packages/api/src/routes/routes.ts index befa44dc..891042d3 100644 --- a/packages/api/src/routes/routes.ts +++ b/packages/api/src/routes/routes.ts @@ -1,6 +1,6 @@ -import { authRoute } from "../routes/auth/authRoute.js" -import { internalRoute } from "../routes/internal/internalRoute.js" -import { publicRoute } from "../routes/public/publicRoute.js" import { apiFactory } from "../utilities/apiFactory.js" +import { authRoute } from "./auth/authRoute.js" +import { internalRoute } from "./internal/internalRoute.js" +import { publicRoute } from "./public/publicRoute.js" export const routes = apiFactory.createApp().route("/", authRoute).route("/", publicRoute).route("/", internalRoute) diff --git a/packages/api/src/server.ts b/packages/api/src/server.ts index efb1deed..97febe55 100644 --- a/packages/api/src/server.ts +++ b/packages/api/src/server.ts @@ -1,5 +1,6 @@ import { createServer } from "node:http" import { api } from "./api.js" +import { checkDatabaseSchema } from "./utilities/checkDatabaseSchema.js" import { getClients } from "./utilities/getClients.js" import { getEnv } from "./utilities/getEnv.js" import { ensureStorageBucket } from "./utilities/storage/ensureStorageBucket.js" @@ -15,6 +16,25 @@ async function startServer() { console.error("Unhandled Rejection:", reason) // Similar to uncaughtException handling }) + + // Schema check — runs on every startup. + // When SCHEMA_CHECK_ONLY=1, exit immediately after the check so that + // start.sh can use plain `tsx` (not watch) to get a real exit code + // before handing off to `tsx watch`. + try { + const env = getEnv() + const clients = await getClients(env) + await checkDatabaseSchema(clients.sql) + } catch (error: unknown) { + console.error("Database schema check failed — run migrations and restart:") + console.error(error instanceof Error ? error.message : error) + process.exit(1) + } + + if (process.env.SCHEMA_CHECK_ONLY === "1") { + process.exit(0) + } + while (true) { try { // Get variables and clients diff --git a/packages/api/src/utilities/checkDatabaseSchema.ts b/packages/api/src/utilities/checkDatabaseSchema.ts new file mode 100644 index 00000000..5ef07276 --- /dev/null +++ b/packages/api/src/utilities/checkDatabaseSchema.ts @@ -0,0 +1,72 @@ +import { models } from "@arrhes/application-metadata" +import { getTableColumns, getTableName, sql } from "drizzle-orm" +import type { sqlClient } from "../clients/sqlClient.js" + +type DbClient = ReturnType + +export async function checkDatabaseSchema(db: DbClient) { + // Collect expected tables and their columns from the Drizzle model definitions. + // `models` is a plain { key: pgTable } object — table instances only, no relations. + const expected = new Map>() // table_name -> Set + + for (const table of Object.values(models)) { + const tableName = getTableName(table) + const columns = getTableColumns(table) + const colNames = new Set(Object.values(columns).map((col) => col.name)) + expected.set(tableName, colNames) + } + + // Query all columns present in the public schema + const rows = await db.execute<{ + table_name: string + column_name: string + }>( + sql`SELECT table_name, column_name + FROM information_schema.columns + WHERE table_schema = 'public'`, + ) + + const actual = new Map>() + for (const row of rows) { + if (!actual.has(row.table_name)) { + actual.set(row.table_name, new Set()) + } + actual.get(row.table_name)!.add(row.column_name) + } + + const drift: string[] = [] + + // Model → DB: tables/columns expected by the code but absent from the DB + for (const [tableName, expectedCols] of expected) { + if (!actual.has(tableName)) { + drift.push(` Table '${tableName}' is missing from the database`) + continue + } + const actualCols = actual.get(tableName)! + for (const col of expectedCols) { + if (!actualCols.has(col)) { + drift.push(` Column '${tableName}.${col}' is missing from the database`) + } + } + } + + // DB → Model: tables/columns present in the DB but no longer in the code + for (const [tableName, actualCols] of actual) { + if (!expected.has(tableName)) { + drift.push(` Table '${tableName}' exists in the database but has no model`) + continue + } + const expectedCols = expected.get(tableName)! + for (const col of actualCols) { + if (!expectedCols.has(col)) { + drift.push(` Column '${tableName}.${col}' exists in the database but is not in the model`) + } + } + } + + if (drift.length > 0) { + throw new Error(`Database schema is out of date — run migrations before starting the API:\n${drift.join("\n")}`) + } + + console.info(`Database schema check passed (${expected.size} tables verified)`) +} diff --git a/packages/api/src/utilities/cookies/parseCookies.ts b/packages/api/src/utilities/cookies/parseCookies.ts index 396440af..f1133e6e 100644 --- a/packages/api/src/utilities/cookies/parseCookies.ts +++ b/packages/api/src/utilities/cookies/parseCookies.ts @@ -1,4 +1,4 @@ -import { Exception } from "../../utilities/exception.js" +import { Exception } from "../exception.js" export function parseCookies(parameters: { value: string | undefined }) { try { diff --git a/packages/api/src/utilities/cookies/signString.ts b/packages/api/src/utilities/cookies/signString.ts index 7e2d36de..b55df26d 100644 --- a/packages/api/src/utilities/cookies/signString.ts +++ b/packages/api/src/utilities/cookies/signString.ts @@ -1,5 +1,5 @@ import { createHmac } from "node:crypto" -import { Exception } from "../../utilities/exception.js" +import { Exception } from "../exception.js" export function signString(parameters: { value: string; secret: string }) { try { diff --git a/packages/api/src/utilities/cookies/unsignString.ts b/packages/api/src/utilities/cookies/unsignString.ts index bef499f7..253bb54c 100644 --- a/packages/api/src/utilities/cookies/unsignString.ts +++ b/packages/api/src/utilities/cookies/unsignString.ts @@ -1,6 +1,6 @@ import { timingSafeEqual } from "node:crypto" -import { signString } from "../../utilities/cookies/signString.js" -import { Exception } from "../../utilities/exception.js" +import { Exception } from "../exception.js" +import { signString } from "./signString.js" export function unsignString(parameters: { signedValue: string | undefined; secret: string }) { try { diff --git a/packages/api/src/utilities/getEnv.ts b/packages/api/src/utilities/getEnv.ts index 0ce0b92a..88582131 100644 --- a/packages/api/src/utilities/getEnv.ts +++ b/packages/api/src/utilities/getEnv.ts @@ -1,5 +1,5 @@ import * as v from "valibot" -import { validate } from "../utilities/validate.js" +import { validate } from "./validate.js" enum Env { development = "development", diff --git a/packages/api/src/utilities/ocr/processOcr.ts b/packages/api/src/utilities/ocr/processOcr.ts index 4ab74d56..0625bf46 100644 --- a/packages/api/src/utilities/ocr/processOcr.ts +++ b/packages/api/src/utilities/ocr/processOcr.ts @@ -42,7 +42,7 @@ interface ProcessOcrResult { } export async function processOcr(params: ProcessOcrParams): Promise { - const { idOrganization, idYear, idUser, sourceFile } = params + const { idOrganization, idUser, sourceFile } = params console.log( `[processOcr] Starting OCR for file "${sourceFile.name}" (id=${sourceFile.id}, type=${sourceFile.type}, storageKey=${sourceFile.storageKey})`, @@ -175,13 +175,7 @@ export async function processOcr(params: ProcessOcrParams): Promise 0 && existingOcrFiles[0]) { @@ -208,7 +202,7 @@ export async function processOcr(params: ProcessOcrParams): Promise, +): ReturnType { + const app = apiFactory.createApp() + const method = routeDef.method.toLowerCase() as "get" | "post" | "patch" | "delete" + app[method](routeDef.path, handler) + return app +} diff --git a/packages/api/src/utilities/response.ts b/packages/api/src/utilities/response.ts index 2a00d46e..e826eb1e 100644 --- a/packages/api/src/utilities/response.ts +++ b/packages/api/src/utilities/response.ts @@ -2,8 +2,8 @@ import type { routeDefinition } from "@arrhes/application-metadata" import type { Context } from "hono" import type { ContentfulStatusCode } from "hono/utils/http-status" import type * as v from "valibot" -import type { ApiEnv } from "../utilities/apiFactory.js" -import { validate } from "../utilities/validate.js" +import type { ApiEnv } from "./apiFactory.js" +import { validate } from "./validate.js" type GContext = Context diff --git a/packages/api/src/utilities/safeParseJSON.ts b/packages/api/src/utilities/safeParseJSON.ts index c2a1d9c9..0f373c75 100644 --- a/packages/api/src/utilities/safeParseJSON.ts +++ b/packages/api/src/utilities/safeParseJSON.ts @@ -1,5 +1,5 @@ -import { completeStringifiedJSON } from "../utilities/completeStringifiedJSON.js" -import { Exception } from "../utilities/exception.js" +import { completeStringifiedJSON } from "./completeStringifiedJSON.js" +import { Exception } from "./exception.js" export function safeParseJSON(parameters: { rawString: string }) { try { diff --git a/packages/api/src/utilities/sql/deleteMany.ts b/packages/api/src/utilities/sql/deleteMany.ts index afd6b855..043c1daf 100644 --- a/packages/api/src/utilities/sql/deleteMany.ts +++ b/packages/api/src/utilities/sql/deleteMany.ts @@ -1,7 +1,7 @@ import type { SQL, TableConfig } from "drizzle-orm" import type { PgTable } from "drizzle-orm/pg-core" import type { sqlClient } from "../../clients/sqlClient.js" -import { Exception } from "../../utilities/exception.js" +import { Exception } from "../exception.js" export async function deleteMany>(parameters: { database: ReturnType | Parameters["transaction"]>[0]>[0] diff --git a/packages/api/src/utilities/sql/deleteOne.ts b/packages/api/src/utilities/sql/deleteOne.ts index d08ad461..6f345a39 100644 --- a/packages/api/src/utilities/sql/deleteOne.ts +++ b/packages/api/src/utilities/sql/deleteOne.ts @@ -1,7 +1,7 @@ import type { SQL, TableConfig } from "drizzle-orm" import type { PgTable } from "drizzle-orm/pg-core" import type { sqlClient } from "../../clients/sqlClient.js" -import { Exception } from "../../utilities/exception.js" +import { Exception } from "../exception.js" export async function deleteOne>(parameters: { database: ReturnType | Parameters["transaction"]>[0]>[0] diff --git a/packages/api/src/utilities/sql/insertMany.ts b/packages/api/src/utilities/sql/insertMany.ts index db5dae4c..7d779200 100644 --- a/packages/api/src/utilities/sql/insertMany.ts +++ b/packages/api/src/utilities/sql/insertMany.ts @@ -1,6 +1,6 @@ import type { PgInsertValue, PgTable, TableConfig } from "drizzle-orm/pg-core" import type { sqlClient } from "../../clients/sqlClient.js" -import { Exception } from "../../utilities/exception.js" +import { Exception } from "../exception.js" // : Promise> diff --git a/packages/api/src/utilities/sql/insertOne.ts b/packages/api/src/utilities/sql/insertOne.ts index 08e50372..fec6d46c 100644 --- a/packages/api/src/utilities/sql/insertOne.ts +++ b/packages/api/src/utilities/sql/insertOne.ts @@ -1,7 +1,7 @@ import type { TableConfig } from "drizzle-orm" import type { PgInsertValue, PgTable } from "drizzle-orm/pg-core" import type { sqlClient } from "../../clients/sqlClient.js" -import { Exception } from "../../utilities/exception.js" +import { Exception } from "../exception.js" export async function insertOne>(parameters: { database: ReturnType | Parameters["transaction"]>[0]>[0] diff --git a/packages/api/src/utilities/sql/selectMany.ts b/packages/api/src/utilities/sql/selectMany.ts index 497448fb..62caf660 100644 --- a/packages/api/src/utilities/sql/selectMany.ts +++ b/packages/api/src/utilities/sql/selectMany.ts @@ -1,7 +1,7 @@ import type { SQL, TableConfig } from "drizzle-orm" import type { PgTable } from "drizzle-orm/pg-core" import type { sqlClient } from "../../clients/sqlClient.js" -import { Exception } from "../../utilities/exception.js" +import { Exception } from "../exception.js" export async function selectMany>(parameters: { database: ReturnType | Parameters["transaction"]>[0]>[0] diff --git a/packages/api/src/utilities/sql/selectOne.ts b/packages/api/src/utilities/sql/selectOne.ts index fa9dae5d..37ffc8a3 100644 --- a/packages/api/src/utilities/sql/selectOne.ts +++ b/packages/api/src/utilities/sql/selectOne.ts @@ -1,7 +1,7 @@ import type { SQL, TableConfig } from "drizzle-orm" import type { PgTable } from "drizzle-orm/pg-core" import type { sqlClient } from "../../clients/sqlClient.js" -import { Exception } from "../../utilities/exception.js" +import { Exception } from "../exception.js" export async function selectOne>(parameters: { database: ReturnType | Parameters["transaction"]>[0]>[0] diff --git a/packages/api/src/utilities/sql/updateOne.ts b/packages/api/src/utilities/sql/updateOne.ts index b89ccd97..2238e095 100644 --- a/packages/api/src/utilities/sql/updateOne.ts +++ b/packages/api/src/utilities/sql/updateOne.ts @@ -1,7 +1,7 @@ import type { SQL, TableConfig } from "drizzle-orm" import type { PgTable, PgUpdateSetSource } from "drizzle-orm/pg-core" import type { sqlClient } from "../../clients/sqlClient.js" -import { Exception } from "../../utilities/exception.js" +import { Exception } from "../exception.js" export async function updateOne>(parameters: { database: ReturnType | Parameters["transaction"]>[0]>[0] diff --git a/packages/api/src/utilities/storage/deleteObject.ts b/packages/api/src/utilities/storage/deleteObject.ts index c60184e9..1c4c0da3 100644 --- a/packages/api/src/utilities/storage/deleteObject.ts +++ b/packages/api/src/utilities/storage/deleteObject.ts @@ -1,7 +1,7 @@ import { DeleteObjectCommand } from "@aws-sdk/client-s3" -import { Exception } from "../../utilities/exception.js" -import type { getClients } from "../../utilities/getClients.js" -import type { getEnv } from "../../utilities/getEnv.js" +import { Exception } from "../exception.js" +import type { getClients } from "../getClients.js" +import type { getEnv } from "../getEnv.js" export async function deleteObject(parameters: { var: { diff --git a/packages/api/src/utilities/storage/generateDeleteSignedUrl.ts b/packages/api/src/utilities/storage/generateDeleteSignedUrl.ts index 843de839..52bc6e9d 100644 --- a/packages/api/src/utilities/storage/generateDeleteSignedUrl.ts +++ b/packages/api/src/utilities/storage/generateDeleteSignedUrl.ts @@ -1,8 +1,8 @@ import { DeleteObjectCommand } from "@aws-sdk/client-s3" import { getSignedUrl } from "@aws-sdk/s3-request-presigner" -import { Exception } from "../../utilities/exception.js" -import type { getClients } from "../../utilities/getClients.js" -import type { getEnv } from "../../utilities/getEnv.js" +import { Exception } from "../exception.js" +import type { getClients } from "../getClients.js" +import type { getEnv } from "../getEnv.js" export async function generateDeleteSignedUrl(parameters: { var: { diff --git a/packages/api/src/utilities/storage/generateGetSignedUrl.ts b/packages/api/src/utilities/storage/generateGetSignedUrl.ts index ff9f80c8..df4de615 100644 --- a/packages/api/src/utilities/storage/generateGetSignedUrl.ts +++ b/packages/api/src/utilities/storage/generateGetSignedUrl.ts @@ -1,9 +1,9 @@ import { GetObjectCommand } from "@aws-sdk/client-s3" import { getSignedUrl } from "@aws-sdk/s3-request-presigner" -import { Exception } from "../../utilities/exception.js" -import type { getClients } from "../../utilities/getClients.js" -import type { getEnv } from "../../utilities/getEnv.js" -import { storageFileExpiresIn } from "../../utilities/variables.js" +import { Exception } from "../exception.js" +import type { getClients } from "../getClients.js" +import type { getEnv } from "../getEnv.js" +import { storageFileExpiresIn } from "../variables.js" export async function generateGetSignedUrl(parameters: { var: { diff --git a/packages/api/src/utilities/storage/generatePutSignedUrl.ts b/packages/api/src/utilities/storage/generatePutSignedUrl.ts index b344ceb6..763a572c 100644 --- a/packages/api/src/utilities/storage/generatePutSignedUrl.ts +++ b/packages/api/src/utilities/storage/generatePutSignedUrl.ts @@ -1,8 +1,8 @@ import { PutObjectCommand } from "@aws-sdk/client-s3" import { getSignedUrl } from "@aws-sdk/s3-request-presigner" -import { Exception } from "../../utilities/exception.js" -import type { getClients } from "../../utilities/getClients.js" -import type { getEnv } from "../../utilities/getEnv.js" +import { Exception } from "../exception.js" +import type { getClients } from "../getClients.js" +import type { getEnv } from "../getEnv.js" export async function generatePutSignedUrl(parameters: { var: { diff --git a/packages/api/src/utilities/storage/getObject.ts b/packages/api/src/utilities/storage/getObject.ts index 23150194..d4f2667b 100644 --- a/packages/api/src/utilities/storage/getObject.ts +++ b/packages/api/src/utilities/storage/getObject.ts @@ -1,7 +1,7 @@ import { GetObjectCommand } from "@aws-sdk/client-s3" -import { Exception } from "../../utilities/exception.js" -import type { getClients } from "../../utilities/getClients.js" -import type { getEnv } from "../../utilities/getEnv.js" +import { Exception } from "../exception.js" +import type { getClients } from "../getClients.js" +import type { getEnv } from "../getEnv.js" export async function getObject(parameters: { var: { diff --git a/packages/api/src/utilities/storage/putObject.ts b/packages/api/src/utilities/storage/putObject.ts index 55f8e20c..6dd12f64 100644 --- a/packages/api/src/utilities/storage/putObject.ts +++ b/packages/api/src/utilities/storage/putObject.ts @@ -1,7 +1,7 @@ import { PutObjectCommand } from "@aws-sdk/client-s3" -import { Exception } from "../../utilities/exception.js" -import type { getClients } from "../../utilities/getClients.js" -import type { getEnv } from "../../utilities/getEnv.js" +import { Exception } from "../exception.js" +import type { getClients } from "../getClients.js" +import type { getEnv } from "../getEnv.js" export async function putObject(parameters: { var: { diff --git a/packages/api/src/utilities/validate.ts b/packages/api/src/utilities/validate.ts index 20edd85e..cb3333f8 100644 --- a/packages/api/src/utilities/validate.ts +++ b/packages/api/src/utilities/validate.ts @@ -1,5 +1,5 @@ import * as v from "valibot" -import { Exception } from "../utilities/exception.js" +import { Exception } from "./exception.js" export function validate>(parameters: { schema: T diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json index db11ed04..ecff0485 100644 --- a/packages/api/tsconfig.json +++ b/packages/api/tsconfig.json @@ -20,9 +20,15 @@ "outDir": "./build", "paths": { "#/*": ["./src/*"] - } + }, + "incremental": true, + "tsBuildInfoFile": "./.tsbuildinfo" }, "exclude": ["./node_modules", "./build"], "include": ["./src"], - "references": [{ "path": "../metadata" }] + "references": [ + { + "path": "../metadata" + } + ] } diff --git a/packages/cli/arrhes.sh b/packages/cli/arrhes.sh new file mode 100644 index 00000000..6ea62c04 --- /dev/null +++ b/packages/cli/arrhes.sh @@ -0,0 +1,1060 @@ +#!/bin/sh +# arrhes — Arrhes API CLI +# Requires: curl +# Config: ~/.arrhes/config (ARRHES_URL, ARRHES_API_KEY) +set -e + +VERSION="0.1.0" +DEFAULT_URL="https://api.arrhes.com" +CONFIG_FILE="${ARRHES_CONFIG:-${HOME}/.arrhes/config}" + +# ── Utilities ───────────────────────────────────────────────────────────────── + +_die() { printf 'arrhes: %s\n' "$*" >&2; exit 1; } +_need_cmd() { command -v "$1" >/dev/null 2>&1 || _die "'$1' is required but not found"; } +_jesc() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'; } + +# JSON body accumulator +_JBODY='' +_jbody_reset() { _JBODY=''; } +_jbody_raw() { + [ -n "$_JBODY" ] && _JBODY="${_JBODY}," + _JBODY="${_JBODY}\"$1\":$2" +} +# _jstr: add "key":"value" — skip if value is empty +_jstr() { [ -n "$2" ] && _jbody_raw "$1" "\"$(_jesc "$2")\"" || true; } +# _jstr_null: add "key":"value" or "key":null +_jstr_null() { + if [ -n "$2" ]; then _jbody_raw "$1" "\"$(_jesc "$2")\""; else _jbody_raw "$1" "null"; fi +} +# _jnum: add "key":number — skip if value is empty +_jnum() { [ -n "$2" ] && _jbody_raw "$1" "$2" || true; } +# _jbool: add "key":true|false +_jbool() { _jbody_raw "$1" "$2"; } +# _jbody: emit the accumulated JSON object +_jbody() { printf '{%s}' "$_JBODY"; } + +# ── Config ──────────────────────────────────────────────────────────────────── + +_cfg_read() { + ARRHES_URL="$DEFAULT_URL" + ARRHES_API_KEY='' + [ -f "$CONFIG_FILE" ] && . "$CONFIG_FILE" +} + +_cfg_write() { + # $1=url $2=api_key + mkdir -p "$(dirname "$CONFIG_FILE")" + printf 'ARRHES_URL=%s\nARRHES_API_KEY=%s\n' "$1" "$2" > "$CONFIG_FILE" + chmod 600 "$CONFIG_FILE" +} + +_require_cfg() { + _cfg_read + [ -n "$ARRHES_API_KEY" ] || _die "Not logged in. Run: arrhes login --api-key " +} + +# ── HTTP ────────────────────────────────────────────────────────────────────── + +_RESP=$(mktemp) +trap 'rm -f "$_RESP"' EXIT INT TERM + +_api() { + # _api METHOD /path [json_body] + method="$1"; path="$2"; body="${3:-}" + url="${ARRHES_URL}${path}" + if [ -n "$body" ]; then + HTTP_CODE=$(curl -sS -o "$_RESP" -w "%{http_code}" \ + -X "$method" \ + -H "Authorization: Bearer ${ARRHES_API_KEY}" \ + -H "Content-Type: application/json" \ + -d "$body" "$url") + else + HTTP_CODE=$(curl -sS -o "$_RESP" -w "%{http_code}" \ + -X "$method" \ + -H "Authorization: Bearer ${ARRHES_API_KEY}" \ + "$url") + fi + case "$HTTP_CODE" in + 2??) cat "$_RESP" ;; + *) cat "$_RESP" >&2; exit 1 ;; + esac +} + +# The API key is scoped to an org; the server uses its own org ID from the key. +# 'me' is just a valid URL placeholder for :idOrganization. +_org_path() { printf '/v1/organizations/me'; } +_year_path() { printf '/v1/organizations/me/years/%s' "$1"; } + +# ── login / whoami / logout ─────────────────────────────────────────────────── + +_cmd_login() { + api_key=''; base_url='' + while [ $# -gt 0 ]; do + case "$1" in + --api-key) api_key="$2"; shift ;; + --url) base_url="$2"; shift ;; + *) _die "Unknown option: $1" ;; + esac; shift + done + [ -n "$api_key" ] || _die "--api-key is required" + base_url="${base_url:-$DEFAULT_URL}"; base_url="${base_url%/}" + ARRHES_URL="$base_url" ARRHES_API_KEY="$api_key" _api GET "/v1/users/me" > /dev/null + _cfg_write "$base_url" "$api_key" + printf 'Logged in. Config saved to %s\n' "$CONFIG_FILE" +} + +_cmd_whoami() { + _require_cfg + _api GET "/v1/users/me" +} + +_cmd_logout() { + _cfg_read + _cfg_write "${ARRHES_URL:-$DEFAULT_URL}" "" + printf 'Logged out.\n' +} + +# ── org ─────────────────────────────────────────────────────────────────────── + +_cmd_org() { + subcmd="${1:-}"; [ $# -gt 0 ] && shift + case "$subcmd" in + get) + _require_cfg; _api GET "$(_org_path)" + ;; + update) + _org_update "$@" + ;; + delete) + _require_cfg + _api DELETE "$(_org_path)" > /dev/null + printf 'Organization deleted.\n' + ;; + *) _die "arrhes org: unknown subcommand '$subcmd'. Use: get, update, delete" ;; + esac +} + +_org_update() { + name=''; email=''; siren='' + while [ $# -gt 0 ]; do + case "$1" in + --name) name="$2"; shift ;; + --email) email="$2"; shift ;; + --siren) siren="$2"; shift ;; + *) _die "Unknown option: $1" ;; + esac; shift + done + _require_cfg + _jbody_reset; _jstr name "$name"; _jstr email "$email"; _jstr siren "$siren" + _api PATCH "$(_org_path)" "$(_jbody)" +} + +# ── years ───────────────────────────────────────────────────────────────────── + +_cmd_years() { + subcmd="${1:-}"; [ $# -gt 0 ] && shift + case "$subcmd" in + list) _require_cfg; _api GET "$(_org_path)/years" ;; + get) _years_get "$@" ;; + create) _years_create "$@" ;; + update) _years_update "$@" ;; + delete) _years_delete "$@" ;; + close) _years_close "$@" ;; + open) _years_open "$@" ;; + settle-balance-sheet) _years_settle_bs "$@" ;; + settle-income-statement) _years_settle_is "$@" ;; + *) _die "arrhes years: unknown subcommand '$subcmd'" ;; + esac +} + +_years_get() { + id="${1:?Usage: arrhes years get }" + _require_cfg; _api GET "$(_year_path "$id")" +} + +_years_create() { + start=''; end=''; label='' + while [ $# -gt 0 ]; do + case "$1" in --start) start="$2"; shift ;; --end) end="$2"; shift ;; --label) label="$2"; shift ;; *) _die "Unknown: $1" ;; esac; shift + done + [ -n "$start" ] || _die "--start is required" + [ -n "$end" ] || _die "--end is required" + _require_cfg; _jbody_reset; _jstr startingAt "$start"; _jstr endingAt "$end"; _jstr label "$label" + _api POST "$(_org_path)/years" "$(_jbody)" +} + +_years_update() { + id="${1:?Usage: arrhes years update }"; shift + start=''; end=''; label='' + while [ $# -gt 0 ]; do + case "$1" in --start) start="$2"; shift ;; --end) end="$2"; shift ;; --label) label="$2"; shift ;; *) _die "Unknown: $1" ;; esac; shift + done + _require_cfg; _jbody_reset; _jstr startingAt "$start"; _jstr endingAt "$end"; _jstr label "$label" + _api PATCH "$(_year_path "$id")" "$(_jbody)" +} + +_years_delete() { + id="${1:?Usage: arrhes years delete }" + _require_cfg; _api DELETE "$(_year_path "$id")" > /dev/null + printf 'Year %s deleted.\n' "$id" +} + +_years_close() { + id="${1:?Usage: arrhes years close }" + _require_cfg; _api POST "$(_year_path "$id")/close" +} + +_years_open() { + id="${1:?Usage: arrhes years open }"; shift + journal_opening='' + while [ $# -gt 0 ]; do + case "$1" in --journal-opening) journal_opening="$2"; shift ;; *) _die "Unknown: $1" ;; esac; shift + done + [ -n "$journal_opening" ] || _die "--journal-opening is required" + _require_cfg; _jbody_reset; _jstr idJournalOpening "$journal_opening" + _api POST "$(_year_path "$id")/open" "$(_jbody)" +} + +_years_settle_bs() { + id="${1:?Usage: arrhes years settle-balance-sheet }"; shift + journal_closing='' + while [ $# -gt 0 ]; do + case "$1" in --journal-closing) journal_closing="$2"; shift ;; *) _die "Unknown: $1" ;; esac; shift + done + [ -n "$journal_closing" ] || _die "--journal-closing is required" + _require_cfg; _jbody_reset; _jstr idJournalClosing "$journal_closing" + _api POST "$(_year_path "$id")/settle-balance-sheet" "$(_jbody)" +} + +_years_settle_is() { + id="${1:?Usage: arrhes years settle-income-statement }"; shift + journal_closing='' + while [ $# -gt 0 ]; do + case "$1" in --journal-closing) journal_closing="$2"; shift ;; *) _die "Unknown: $1" ;; esac; shift + done + [ -n "$journal_closing" ] || _die "--journal-closing is required" + _require_cfg; _jbody_reset; _jstr idJournalClosing "$journal_closing" + _api POST "$(_year_path "$id")/settle-income-statement" "$(_jbody)" +} + +# ── journals ────────────────────────────────────────────────────────────────── + +_cmd_journals() { + subcmd="${1:-}"; [ $# -gt 0 ] && shift + case "$subcmd" in + list) _journals_list "$@" ;; get) _journals_get "$@" ;; + create) _journals_create "$@" ;; update) _journals_update "$@" ;; + delete) _journals_delete "$@" ;; + *) _die "arrhes journals: unknown subcommand '$subcmd'" ;; + esac +} + +_journals_base() { printf '%s/journals' "$(_year_path "$1")"; } + +_journals_list() { + year='' + while [ $# -gt 0 ]; do case "$1" in --year) year="$2"; shift ;; *) _die "Unknown: $1" ;; esac; shift; done + [ -n "$year" ] || _die "--year is required" + _require_cfg; _api GET "$(_journals_base "$year")" +} + +_journals_get() { + id=''; year='' + while [ $# -gt 0 ]; do case "$1" in --year) year="$2"; shift ;; -*) _die "Unknown: $1" ;; *) id="$1" ;; esac; shift; done + [ -n "$id" ] && [ -n "$year" ] || _die "Usage: arrhes journals get --year " + _require_cfg; _api GET "$(_journals_base "$year")/$id" +} + +_journals_create() { + year=''; code=''; label='' + while [ $# -gt 0 ]; do + case "$1" in --year) year="$2"; shift ;; --code) code="$2"; shift ;; --label) label="$2"; shift ;; *) _die "Unknown: $1" ;; esac; shift + done + [ -n "$year" ] && [ -n "$code" ] && [ -n "$label" ] || _die "--year, --code, and --label are required" + _require_cfg; _jbody_reset; _jstr code "$code"; _jstr label "$label" + _api POST "$(_journals_base "$year")" "$(_jbody)" +} + +_journals_update() { + id=''; year=''; code=''; label='' + while [ $# -gt 0 ]; do + case "$1" in --year) year="$2"; shift ;; --code) code="$2"; shift ;; --label) label="$2"; shift ;; -*) _die "Unknown: $1" ;; *) id="$1" ;; esac; shift + done + [ -n "$id" ] && [ -n "$year" ] || _die "Usage: arrhes journals update --year " + _require_cfg; _jbody_reset; _jstr code "$code"; _jstr label "$label" + _api PATCH "$(_journals_base "$year")/$id" "$(_jbody)" +} + +_journals_delete() { + id=''; year='' + while [ $# -gt 0 ]; do case "$1" in --year) year="$2"; shift ;; -*) _die "Unknown: $1" ;; *) id="$1" ;; esac; shift; done + [ -n "$id" ] && [ -n "$year" ] || _die "Usage: arrhes journals delete --year " + _require_cfg; _api DELETE "$(_journals_base "$year")/$id" > /dev/null + printf 'Journal %s deleted.\n' "$id" +} + +# ── accounts ────────────────────────────────────────────────────────────────── + +_cmd_accounts() { + subcmd="${1:-}"; [ $# -gt 0 ] && shift + case "$subcmd" in + list) _accounts_list "$@" ;; get) _accounts_get "$@" ;; + create) _accounts_create "$@" ;; update) _accounts_update "$@" ;; + delete) _accounts_delete "$@" ;; + *) _die "arrhes accounts: unknown subcommand '$subcmd'" ;; + esac +} + +_accounts_base() { printf '%s/accounts' "$(_year_path "$1")"; } + +_accounts_list() { + year='' + while [ $# -gt 0 ]; do case "$1" in --year) year="$2"; shift ;; *) _die "Unknown: $1" ;; esac; shift; done + [ -n "$year" ] || _die "--year is required" + _require_cfg; _api GET "$(_accounts_base "$year")" +} + +_accounts_get() { + id=''; year='' + while [ $# -gt 0 ]; do case "$1" in --year) year="$2"; shift ;; -*) _die "Unknown: $1" ;; *) id="$1" ;; esac; shift; done + [ -n "$id" ] && [ -n "$year" ] || _die "Usage: arrhes accounts get --year " + _require_cfg; _api GET "$(_accounts_base "$year")/$id" +} + +_accounts_create() { + year=''; number=''; label=''; type=''; parent=''; selectable='true' + while [ $# -gt 0 ]; do + case "$1" in + --year) year="$2"; shift ;; + --number) number="$2"; shift ;; + --label) label="$2"; shift ;; + --type) type="$2"; shift ;; + --parent) parent="$2"; shift ;; + --no-selectable) selectable='false' ;; + *) _die "Unknown: $1" ;; + esac; shift + done + [ -n "$year" ] && [ -n "$number" ] && [ -n "$label" ] && [ -n "$type" ] && [ -n "$parent" ] || \ + _die "--year, --number, --label, --type, and --parent are required" + _require_cfg; _jbody_reset + _jstr number "$number"; _jstr label "$label"; _jstr type "$type" + _jstr idAccountParent "$parent"; _jbool isSelectable "$selectable" + _api POST "$(_accounts_base "$year")" "$(_jbody)" +} + +_accounts_update() { + id=''; year=''; number=''; label=''; type=''; parent='' + while [ $# -gt 0 ]; do + case "$1" in + --year) year="$2"; shift ;; --number) number="$2"; shift ;; --label) label="$2"; shift ;; + --type) type="$2"; shift ;; --parent) parent="$2"; shift ;; -*) _die "Unknown: $1" ;; *) id="$1" ;; + esac; shift + done + [ -n "$id" ] && [ -n "$year" ] || _die "Usage: arrhes accounts update --year " + _require_cfg; _jbody_reset; _jstr number "$number"; _jstr label "$label"; _jstr type "$type"; _jstr idAccountParent "$parent" + _api PATCH "$(_accounts_base "$year")/$id" "$(_jbody)" +} + +_accounts_delete() { + id=''; year='' + while [ $# -gt 0 ]; do case "$1" in --year) year="$2"; shift ;; -*) _die "Unknown: $1" ;; *) id="$1" ;; esac; shift; done + [ -n "$id" ] && [ -n "$year" ] || _die "Usage: arrhes accounts delete --year " + _require_cfg; _api DELETE "$(_accounts_base "$year")/$id" > /dev/null + printf 'Account %s deleted.\n' "$id" +} + +# ── tags ────────────────────────────────────────────────────────────────────── + +_cmd_tags() { + subcmd="${1:-}"; [ $# -gt 0 ] && shift + case "$subcmd" in + list) _tags_list "$@" ;; get) _tags_get "$@" ;; + create) _tags_create "$@" ;; update) _tags_update "$@" ;; + delete) _tags_delete "$@" ;; + *) _die "arrhes tags: unknown subcommand '$subcmd'" ;; + esac +} + +_tags_base() { printf '%s/tags' "$(_year_path "$1")"; } + +_tags_list() { + year='' + while [ $# -gt 0 ]; do case "$1" in --year) year="$2"; shift ;; *) _die "Unknown: $1" ;; esac; shift; done + [ -n "$year" ] || _die "--year is required" + _require_cfg; _api GET "$(_tags_base "$year")" +} + +_tags_get() { + id=''; year='' + while [ $# -gt 0 ]; do case "$1" in --year) year="$2"; shift ;; -*) _die "Unknown: $1" ;; *) id="$1" ;; esac; shift; done + [ -n "$id" ] && [ -n "$year" ] || _die "Usage: arrhes tags get --year " + _require_cfg; _api GET "$(_tags_base "$year")/$id" +} + +_tags_create() { + year=''; label='' + while [ $# -gt 0 ]; do case "$1" in --year) year="$2"; shift ;; --label) label="$2"; shift ;; *) _die "Unknown: $1" ;; esac; shift; done + [ -n "$year" ] && [ -n "$label" ] || _die "--year and --label are required" + _require_cfg; _jbody_reset; _jstr label "$label" + _api POST "$(_tags_base "$year")" "$(_jbody)" +} + +_tags_update() { + id=''; year=''; label='' + while [ $# -gt 0 ]; do case "$1" in --year) year="$2"; shift ;; --label) label="$2"; shift ;; -*) _die "Unknown: $1" ;; *) id="$1" ;; esac; shift; done + [ -n "$id" ] && [ -n "$year" ] || _die "Usage: arrhes tags update --year " + _require_cfg; _jbody_reset; _jstr label "$label" + _api PATCH "$(_tags_base "$year")/$id" "$(_jbody)" +} + +_tags_delete() { + id=''; year='' + while [ $# -gt 0 ]; do case "$1" in --year) year="$2"; shift ;; -*) _die "Unknown: $1" ;; *) id="$1" ;; esac; shift; done + [ -n "$id" ] && [ -n "$year" ] || _die "Usage: arrhes tags delete --year " + _require_cfg; _api DELETE "$(_tags_base "$year")/$id" > /dev/null + printf 'Tag %s deleted.\n' "$id" +} + +# ── entries ─────────────────────────────────────────────────────────────────── + +_cmd_entries() { + subcmd="${1:-}"; [ $# -gt 0 ] && shift + case "$subcmd" in + list) _entries_list "$@" ;; get) _entries_get "$@" ;; + create) _entries_create "$@" ;; update) _entries_update "$@" ;; + duplicate) _entries_duplicate "$@" ;; reverse) _entries_reverse "$@" ;; + delete) _entries_delete "$@" ;; compute) _entries_compute "$@" ;; + lines) _cmd_entry_lines "$@" ;; tags) _cmd_entry_tags "$@" ;; + *) _die "arrhes entries: unknown subcommand '$subcmd'" ;; + esac +} + +_entries_base() { printf '%s/entries' "$(_year_path "$1")"; } + +_entries_list() { + year='' + while [ $# -gt 0 ]; do case "$1" in --year) year="$2"; shift ;; *) _die "Unknown: $1" ;; esac; shift; done + [ -n "$year" ] || _die "--year is required" + _require_cfg; _api GET "$(_entries_base "$year")" +} + +_entries_get() { + id=''; year='' + while [ $# -gt 0 ]; do case "$1" in --year) year="$2"; shift ;; -*) _die "Unknown: $1" ;; *) id="$1" ;; esac; shift; done + [ -n "$id" ] && [ -n "$year" ] || _die "Usage: arrhes entries get --year " + _require_cfg; _api GET "$(_entries_base "$year")/$id" +} + +_entries_create() { + year=''; journal=''; label=''; date='' + while [ $# -gt 0 ]; do + case "$1" in --year) year="$2"; shift ;; --journal) journal="$2"; shift ;; --label) label="$2"; shift ;; --date) date="$2"; shift ;; *) _die "Unknown: $1" ;; esac; shift + done + [ -n "$year" ] && [ -n "$journal" ] || _die "--year and --journal are required" + date="${date:-$(date +%Y-%m-%d)}" + _require_cfg; _jbody_reset; _jstr idJournal "$journal"; _jstr label "$label"; _jstr date "$date" + _api POST "$(_entries_base "$year")" "$(_jbody)" +} + +_entries_update() { + id=''; year=''; label=''; date=''; journal=''; file='' + while [ $# -gt 0 ]; do + case "$1" in + --year) year="$2"; shift ;; --label) label="$2"; shift ;; + --date) date="$2"; shift ;; --journal) journal="$2"; shift ;; + --file) file="$2"; shift ;; -*) _die "Unknown: $1" ;; *) id="$1" ;; + esac; shift + done + [ -n "$id" ] && [ -n "$year" ] || _die "Usage: arrhes entries update --year " + _require_cfg; _jbody_reset; _jstr label "$label"; _jstr date "$date"; _jstr idJournal "$journal"; _jstr idFile "$file" + _api PATCH "$(_entries_base "$year")/$id" "$(_jbody)" +} + +_entries_duplicate() { + id=''; year='' + while [ $# -gt 0 ]; do case "$1" in --year) year="$2"; shift ;; -*) _die "Unknown: $1" ;; *) id="$1" ;; esac; shift; done + [ -n "$id" ] && [ -n "$year" ] || _die "Usage: arrhes entries duplicate --year " + _require_cfg; _api POST "$(_entries_base "$year")/$id/duplicate" +} + +_entries_reverse() { + id=''; year='' + while [ $# -gt 0 ]; do case "$1" in --year) year="$2"; shift ;; -*) _die "Unknown: $1" ;; *) id="$1" ;; esac; shift; done + [ -n "$id" ] && [ -n "$year" ] || _die "Usage: arrhes entries reverse --year " + _require_cfg; _api POST "$(_entries_base "$year")/$id/reverse" +} + +_entries_delete() { + id=''; year='' + while [ $# -gt 0 ]; do case "$1" in --year) year="$2"; shift ;; -*) _die "Unknown: $1" ;; *) id="$1" ;; esac; shift; done + [ -n "$id" ] && [ -n "$year" ] || _die "Usage: arrhes entries delete --year " + _require_cfg; _api DELETE "$(_entries_base "$year")/$id" > /dev/null + printf 'Entry %s deleted.\n' "$id" +} + +_entries_compute() { + id=''; year='' + while [ $# -gt 0 ]; do case "$1" in --year) year="$2"; shift ;; -*) _die "Unknown: $1" ;; *) id="$1" ;; esac; shift; done + [ -n "$id" ] && [ -n "$year" ] || _die "Usage: arrhes entries compute --year " + _require_cfg; _api POST "$(_entries_base "$year")/$id/compute" +} + +# ── entries lines ───────────────────────────────────────────────────────────── + +_cmd_entry_lines() { + subcmd="${1:-}"; [ $# -gt 0 ] && shift + case "$subcmd" in + list) _lines_list "$@" ;; get) _lines_get "$@" ;; + create) _lines_create "$@" ;; update) _lines_update "$@" ;; + delete) _lines_delete "$@" ;; + *) _die "arrhes entries lines: unknown subcommand '$subcmd'" ;; + esac +} + +_lines_base() { printf '%s/%s/lines' "$(_entries_base "$1")" "$2"; } + +_lines_list() { + entry=''; year='' + while [ $# -gt 0 ]; do case "$1" in --year) year="$2"; shift ;; -*) _die "Unknown: $1" ;; *) entry="$1" ;; esac; shift; done + [ -n "$entry" ] && [ -n "$year" ] || _die "Usage: arrhes entries lines list --year " + _require_cfg; _api GET "$(_lines_base "$year" "$entry")" +} + +_lines_get() { + entry=''; line=''; year='' + while [ $# -gt 0 ]; do + case "$1" in --year) year="$2"; shift ;; -*) _die "Unknown: $1" ;; + *) if [ -z "$entry" ]; then entry="$1"; else line="$1"; fi ;; + esac; shift + done + [ -n "$entry" ] && [ -n "$line" ] && [ -n "$year" ] || _die "Usage: arrhes entries lines get --year " + _require_cfg; _api GET "$(_lines_base "$year" "$entry")/$line" +} + +_lines_create() { + entry=''; year=''; account=''; label=''; debit=''; credit='' + while [ $# -gt 0 ]; do + case "$1" in + --year) year="$2"; shift ;; --account) account="$2"; shift ;; + --label) label="$2"; shift ;; --debit) debit="$2"; shift ;; + --credit) credit="$2"; shift ;; -*) _die "Unknown: $1" ;; *) entry="$1" ;; + esac; shift + done + [ -n "$entry" ] && [ -n "$year" ] && [ -n "$account" ] || \ + _die "Usage: arrhes entries lines create --year --account " + _require_cfg; _jbody_reset; _jstr idAccount "$account"; _jstr label "$label"; _jnum debit "$debit"; _jnum credit "$credit" + _api POST "$(_lines_base "$year" "$entry")" "$(_jbody)" +} + +_lines_update() { + entry=''; line=''; year=''; label=''; debit=''; credit='' + while [ $# -gt 0 ]; do + case "$1" in + --year) year="$2"; shift ;; --label) label="$2"; shift ;; + --debit) debit="$2"; shift ;; --credit) credit="$2"; shift ;; + -*) _die "Unknown: $1" ;; *) if [ -z "$entry" ]; then entry="$1"; else line="$1"; fi ;; + esac; shift + done + [ -n "$entry" ] && [ -n "$line" ] && [ -n "$year" ] || _die "Usage: arrhes entries lines update --year " + _require_cfg; _jbody_reset; _jstr label "$label"; _jnum debit "$debit"; _jnum credit "$credit" + _api PATCH "$(_lines_base "$year" "$entry")/$line" "$(_jbody)" +} + +_lines_delete() { + entry=''; line=''; year='' + while [ $# -gt 0 ]; do + case "$1" in --year) year="$2"; shift ;; -*) _die "Unknown: $1" ;; + *) if [ -z "$entry" ]; then entry="$1"; else line="$1"; fi ;; + esac; shift + done + [ -n "$entry" ] && [ -n "$line" ] && [ -n "$year" ] || _die "Usage: arrhes entries lines delete --year " + _require_cfg; _api DELETE "$(_lines_base "$year" "$entry")/$line" > /dev/null + printf 'Line %s deleted.\n' "$line" +} + +# ── entries tags ────────────────────────────────────────────────────────────── + +_cmd_entry_tags() { + subcmd="${1:-}"; [ $# -gt 0 ] && shift + case "$subcmd" in + add) _entry_tag_add "$@" ;; + remove) _entry_tag_remove "$@" ;; + *) _die "arrhes entries tags: use 'add' or 'remove'" ;; + esac +} + +_entry_tag_add() { + entry=''; year=''; tag='' + while [ $# -gt 0 ]; do + case "$1" in --year) year="$2"; shift ;; --tag) tag="$2"; shift ;; -*) _die "Unknown: $1" ;; *) entry="$1" ;; esac; shift + done + [ -n "$entry" ] && [ -n "$year" ] && [ -n "$tag" ] || \ + _die "Usage: arrhes entries tags add --year --tag " + _require_cfg; _jbody_reset; _jstr idTag "$tag" + _api POST "$(_entries_base "$year")/$entry/tags" "$(_jbody)" +} + +_entry_tag_remove() { + entry=''; tag=''; year='' + while [ $# -gt 0 ]; do + case "$1" in --year) year="$2"; shift ;; -*) _die "Unknown: $1" ;; + *) if [ -z "$entry" ]; then entry="$1"; else tag="$1"; fi ;; + esac; shift + done + [ -n "$entry" ] && [ -n "$tag" ] && [ -n "$year" ] || \ + _die "Usage: arrhes entries tags remove --year " + _require_cfg; _api DELETE "$(_entries_base "$year")/$entry/tags/$tag" > /dev/null + printf 'Tag removed.\n' +} + +# ── files ───────────────────────────────────────────────────────────────────── + +_cmd_files() { + subcmd="${1:-}"; [ $# -gt 0 ] && shift + case "$subcmd" in + list) _files_list "$@" ;; get) _files_get "$@" ;; + create) _files_create "$@" ;; update) _files_update "$@" ;; + delete) _files_delete "$@" ;; download-url) _files_download_url "$@" ;; + folders) _cmd_folders "$@" ;; + *) _die "arrhes files: unknown subcommand '$subcmd'" ;; + esac +} + +_files_base() { printf '%s/files' "$(_year_path "$1")"; } + +_files_list() { + year='' + while [ $# -gt 0 ]; do case "$1" in --year) year="$2"; shift ;; *) _die "Unknown: $1" ;; esac; shift; done + [ -n "$year" ] || _die "--year is required" + _require_cfg; _api GET "$(_files_base "$year")" +} + +_files_get() { + id=''; year='' + while [ $# -gt 0 ]; do case "$1" in --year) year="$2"; shift ;; -*) _die "Unknown: $1" ;; *) id="$1" ;; esac; shift; done + [ -n "$id" ] && [ -n "$year" ] || _die "Usage: arrhes files get --year " + _require_cfg; _api GET "$(_files_base "$year")/$id" +} + +_files_create() { + year=''; name=''; reference=''; hash=''; folder='' + while [ $# -gt 0 ]; do + case "$1" in + --year) year="$2"; shift ;; --name) name="$2"; shift ;; + --reference) reference="$2"; shift ;; --hash) hash="$2"; shift ;; + --folder) folder="$2"; shift ;; *) _die "Unknown: $1" ;; + esac; shift + done + [ -n "$year" ] && [ -n "$name" ] && [ -n "$reference" ] && [ -n "$hash" ] || \ + _die "--year, --name, --reference, and --hash are required" + _require_cfg; _jbody_reset; _jstr name "$name"; _jstr reference "$reference"; _jstr hash "$hash"; _jstr idFolder "$folder" + _api POST "$(_files_base "$year")" "$(_jbody)" +} + +_files_update() { + id=''; year=''; name=''; reference=''; date=''; folder='' + while [ $# -gt 0 ]; do + case "$1" in + --year) year="$2"; shift ;; --name) name="$2"; shift ;; + --reference) reference="$2"; shift ;; --date) date="$2"; shift ;; + --folder) folder="$2"; shift ;; -*) _die "Unknown: $1" ;; *) id="$1" ;; + esac; shift + done + [ -n "$id" ] && [ -n "$year" ] || _die "Usage: arrhes files update --year " + _require_cfg; _jbody_reset; _jstr name "$name"; _jstr reference "$reference"; _jstr date "$date"; _jstr idFolder "$folder" + _api PATCH "$(_files_base "$year")/$id" "$(_jbody)" +} + +_files_delete() { + id=''; year='' + while [ $# -gt 0 ]; do case "$1" in --year) year="$2"; shift ;; -*) _die "Unknown: $1" ;; *) id="$1" ;; esac; shift; done + [ -n "$id" ] && [ -n "$year" ] || _die "Usage: arrhes files delete --year " + _require_cfg; _api DELETE "$(_files_base "$year")/$id" > /dev/null + printf 'File %s deleted.\n' "$id" +} + +_files_download_url() { + id=''; year='' + while [ $# -gt 0 ]; do case "$1" in --year) year="$2"; shift ;; -*) _die "Unknown: $1" ;; *) id="$1" ;; esac; shift; done + [ -n "$id" ] && [ -n "$year" ] || _die "Usage: arrhes files download-url --year " + _require_cfg + _api POST "$(_files_base "$year")/$id/download-url" | sed 's/.*"url":"\([^"]*\)".*/\1/' +} + +# ── files folders ───────────────────────────────────────────────────────────── + +_cmd_folders() { + subcmd="${1:-}"; [ $# -gt 0 ] && shift + case "$subcmd" in + list) _folders_list "$@" ;; get) _folders_get "$@" ;; + create) _folders_create "$@" ;; update) _folders_update "$@" ;; + delete) _folders_delete "$@" ;; + *) _die "arrhes files folders: unknown subcommand '$subcmd'" ;; + esac +} + +_folders_base() { printf '%s/folders' "$(_year_path "$1")"; } + +_folders_list() { + year='' + while [ $# -gt 0 ]; do case "$1" in --year) year="$2"; shift ;; *) _die "Unknown: $1" ;; esac; shift; done + [ -n "$year" ] || _die "--year is required" + _require_cfg; _api GET "$(_folders_base "$year")" +} + +_folders_get() { + id=''; year='' + while [ $# -gt 0 ]; do case "$1" in --year) year="$2"; shift ;; -*) _die "Unknown: $1" ;; *) id="$1" ;; esac; shift; done + [ -n "$id" ] && [ -n "$year" ] || _die "Usage: arrhes files folders get --year " + _require_cfg; _api GET "$(_folders_base "$year")/$id" +} + +_folders_create() { + year=''; name=''; parent='' + while [ $# -gt 0 ]; do + case "$1" in --year) year="$2"; shift ;; --name) name="$2"; shift ;; --parent) parent="$2"; shift ;; *) _die "Unknown: $1" ;; esac; shift + done + [ -n "$year" ] && [ -n "$name" ] || _die "--year and --name are required" + _require_cfg; _jbody_reset; _jstr name "$name"; _jstr idFolderParent "$parent" + _api POST "$(_folders_base "$year")" "$(_jbody)" +} + +_folders_update() { + id=''; year=''; name=''; parent='' + while [ $# -gt 0 ]; do + case "$1" in --year) year="$2"; shift ;; --name) name="$2"; shift ;; --parent) parent="$2"; shift ;; -*) _die "Unknown: $1" ;; *) id="$1" ;; esac; shift + done + [ -n "$id" ] && [ -n "$year" ] || _die "Usage: arrhes files folders update --year " + _require_cfg; _jbody_reset; _jstr name "$name"; _jstr idFolderParent "$parent" + _api PATCH "$(_folders_base "$year")/$id" "$(_jbody)" +} + +_folders_delete() { + id=''; year='' + while [ $# -gt 0 ]; do case "$1" in --year) year="$2"; shift ;; -*) _die "Unknown: $1" ;; *) id="$1" ;; esac; shift; done + [ -n "$id" ] && [ -n "$year" ] || _die "Usage: arrhes files folders delete --year " + _require_cfg; _api DELETE "$(_folders_base "$year")/$id" > /dev/null + printf 'Folder %s deleted.\n' "$id" +} + +# ── members ─────────────────────────────────────────────────────────────────── + +_cmd_members() { + subcmd="${1:-}"; [ $# -gt 0 ] && shift + case "$subcmd" in + list) _require_cfg; _api GET "$(_org_path)/users" ;; + get) _members_get "$@" ;; + invite) _members_invite "$@" ;; + update) _members_update "$@" ;; + remove) _members_remove "$@" ;; + *) _die "arrhes members: unknown subcommand '$subcmd'" ;; + esac +} + +_members_get() { + id="${1:?Usage: arrhes members get }" + _require_cfg; _api GET "$(_org_path)/users/$id" +} + +_members_invite() { + email=''; is_admin='false' + while [ $# -gt 0 ]; do + case "$1" in --email) email="$2"; shift ;; --admin) is_admin='true' ;; *) _die "Unknown: $1" ;; esac; shift + done + [ -n "$email" ] || _die "--email is required" + _require_cfg + _api POST "$(_org_path)/users" \ + "{\"isAdmin\":${is_admin},\"user\":{\"email\":\"$(_jesc "$email")\"}}" +} + +_members_update() { + id=''; is_admin='' + while [ $# -gt 0 ]; do + case "$1" in --admin) is_admin='true' ;; --no-admin) is_admin='false' ;; -*) _die "Unknown: $1" ;; *) id="$1" ;; esac; shift + done + [ -n "$id" ] || _die "Usage: arrhes members update [--admin|--no-admin]" + _require_cfg; _jbody_reset; [ -n "$is_admin" ] && _jbool isAdmin "$is_admin" || true + _api PATCH "$(_org_path)/users/$id" "$(_jbody)" +} + +_members_remove() { + id="${1:?Usage: arrhes members remove }" + _require_cfg; _api DELETE "$(_org_path)/users/$id" > /dev/null + printf 'Member %s removed.\n' "$id" +} + +# ── api-keys ────────────────────────────────────────────────────────────────── + +_cmd_api_keys() { + subcmd="${1:-}"; [ $# -gt 0 ] && shift + case "$subcmd" in + list) _require_cfg; _api GET "$(_org_path)/api-keys" ;; + create) _api_keys_create "$@" ;; + delete) _api_keys_delete "$@" ;; + *) _die "arrhes api-keys: unknown subcommand '$subcmd'" ;; + esac +} + +_api_keys_create() { + name='' + while [ $# -gt 0 ]; do case "$1" in --name) name="$2"; shift ;; *) _die "Unknown: $1" ;; esac; shift; done + _require_cfg; _jbody_reset; _jstr name "$name" + _api POST "$(_org_path)/api-keys" "$(_jbody)" +} + +_api_keys_delete() { + id="${1:?Usage: arrhes api-keys delete }" + _require_cfg; _api DELETE "$(_org_path)/api-keys/$id" > /dev/null + printf 'API key %s deleted.\n' "$id" +} + +# ── exports ─────────────────────────────────────────────────────────────────── + +_cmd_exports() { + subcmd="${1:-}"; [ $# -gt 0 ] && shift + case "$subcmd" in + fec) _exports_fec "$@" ;; + xbrl-balance-sheet) _exports_xbrl_bs "$@" ;; + xbrl-income-statement) _exports_xbrl_is "$@" ;; + *) _die "arrhes exports: unknown subcommand '$subcmd'" ;; + esac +} + +_exports_fec() { + year='' + while [ $# -gt 0 ]; do case "$1" in --year) year="$2"; shift ;; *) _die "Unknown: $1" ;; esac; shift; done + [ -n "$year" ] || _die "--year is required" + _require_cfg; _api POST "$(_year_path "$year")/exports/fec" | sed 's/.*"url":"\([^"]*\)".*/\1/' +} + +_exports_xbrl_bs() { + year='' + while [ $# -gt 0 ]; do case "$1" in --year) year="$2"; shift ;; *) _die "Unknown: $1" ;; esac; shift; done + [ -n "$year" ] || _die "--year is required" + _require_cfg; _api POST "$(_year_path "$year")/exports/balance-sheet" | sed 's/.*"url":"\([^"]*\)".*/\1/' +} + +_exports_xbrl_is() { + year='' + while [ $# -gt 0 ]; do case "$1" in --year) year="$2"; shift ;; *) _die "Unknown: $1" ;; esac; shift; done + [ -n "$year" ] || _die "--year is required" + _require_cfg; _api POST "$(_year_path "$year")/exports/income-statement" | sed 's/.*"url":"\([^"]*\)".*/\1/' +} + +# ── balance-sheets ──────────────────────────────────────────────────────────── + +_cmd_balance_sheets() { + subcmd="${1:-}"; [ $# -gt 0 ] && shift + case "$subcmd" in + list) _bs_list "$@" ;; get) _bs_get "$@" ;; + create) _bs_create "$@" ;; update) _bs_update "$@" ;; + delete) _bs_delete "$@" ;; + *) _die "arrhes balance-sheets: unknown subcommand '$subcmd'" ;; + esac +} + +_bs_base() { printf '%s/balance-sheets' "$(_year_path "$1")"; } + +_bs_list() { + year="${1:?Usage: arrhes balance-sheets list }" + _require_cfg; _api GET "$(_bs_base "$year")" +} + +_bs_get() { + year="${1:?Usage: arrhes balance-sheets get }" + bs="${2:?missing }" + _require_cfg; _api GET "$(_bs_base "$year")/$bs" +} + +_bs_create() { + year="${1:?Usage: arrhes balance-sheets create }"; shift + parent=''; label='' + while [ $# -gt 0 ]; do case "$1" in --parent) parent="$2"; shift ;; --label) label="$2"; shift ;; *) _die "Unknown: $1" ;; esac; shift; done + _require_cfg; _jbody_reset; _jstr_null idBalanceSheetParent "$parent"; _jstr label "$label" + _api POST "$(_bs_base "$year")" "$(_jbody)" +} + +_bs_update() { + year="${1:?Usage: arrhes balance-sheets update }" + bs="${2:?missing }"; shift 2 + parent=''; label='' + while [ $# -gt 0 ]; do case "$1" in --parent) parent="$2"; shift ;; --label) label="$2"; shift ;; *) _die "Unknown: $1" ;; esac; shift; done + _require_cfg; _jbody_reset; _jstr idBalanceSheetParent "$parent"; _jstr label "$label" + _api PATCH "$(_bs_base "$year")/$bs" "$(_jbody)" +} + +_bs_delete() { + year="${1:?Usage: arrhes balance-sheets delete }" + bs="${2:?missing }" + _require_cfg; _api DELETE "$(_bs_base "$year")/$bs" > /dev/null + printf 'Balance sheet node %s deleted.\n' "$bs" +} + +# ── income-statements ───────────────────────────────────────────────────────── + +_cmd_income_statements() { + subcmd="${1:-}"; [ $# -gt 0 ] && shift + case "$subcmd" in + list) _is_list "$@" ;; get) _is_get "$@" ;; + create) _is_create "$@" ;; update) _is_update "$@" ;; + delete) _is_delete "$@" ;; computations) _cmd_computations "$@" ;; + *) _die "arrhes income-statements: unknown subcommand '$subcmd'" ;; + esac +} + +_is_base() { printf '%s/income-statements' "$(_year_path "$1")"; } + +_is_list() { + year="${1:?Usage: arrhes income-statements list }" + _require_cfg; _api GET "$(_is_base "$year")" +} + +_is_get() { + year="${1:?Usage: arrhes income-statements get }" + is="${2:?missing }" + _require_cfg; _api GET "$(_is_base "$year")/$is" +} + +_is_create() { + year="${1:?Usage: arrhes income-statements create }"; shift + label=''; parent='' + while [ $# -gt 0 ]; do case "$1" in --label) label="$2"; shift ;; --parent) parent="$2"; shift ;; *) _die "Unknown: $1" ;; esac; shift; done + [ -n "$label" ] || _die "--label is required" + _require_cfg; _jbody_reset; _jstr label "$label"; _jstr_null idIncomeStatementParent "$parent" + _api POST "$(_is_base "$year")" "$(_jbody)" +} + +_is_update() { + year="${1:?Usage: arrhes income-statements update }" + is="${2:?missing }"; shift 2 + label=''; parent='' + while [ $# -gt 0 ]; do case "$1" in --label) label="$2"; shift ;; --parent) parent="$2"; shift ;; *) _die "Unknown: $1" ;; esac; shift; done + _require_cfg; _jbody_reset; _jstr label "$label"; _jstr idIncomeStatementParent "$parent" + _api PATCH "$(_is_base "$year")/$is" "$(_jbody)" +} + +_is_delete() { + year="${1:?Usage: arrhes income-statements delete }" + is="${2:?missing }" + _require_cfg; _api DELETE "$(_is_base "$year")/$is" > /dev/null + printf 'Income statement node %s deleted.\n' "$is" +} + +# ── computations ────────────────────────────────────────────────────────────── + +_cmd_computations() { + subcmd="${1:-}"; [ $# -gt 0 ] && shift + case "$subcmd" in + list) _comp_list "$@" ;; get) _comp_get "$@" ;; + create) _comp_create "$@" ;; update) _comp_update "$@" ;; + delete) _comp_delete "$@" ;; + *) _die "arrhes income-statements computations: unknown subcommand '$subcmd'" ;; + esac +} + +_comp_base() { printf '%s/computations' "$(_year_path "$1")"; } + +_comp_list() { + year="${1:?Usage: arrhes income-statements computations list }" + _require_cfg; _api GET "$(_comp_base "$year")" +} + +_comp_get() { + year="${1:?Usage: arrhes income-statements computations get }" + comp="${2:?missing }" + _require_cfg; _api GET "$(_comp_base "$year")/$comp" +} + +_comp_create() { + year="${1:?Usage: arrhes income-statements computations create }"; shift + label='' + while [ $# -gt 0 ]; do case "$1" in --label) label="$2"; shift ;; *) _die "Unknown: $1" ;; esac; shift; done + [ -n "$label" ] || _die "--label is required" + _require_cfg; _jbody_reset; _jstr label "$label" + _api POST "$(_comp_base "$year")" "$(_jbody)" +} + +_comp_update() { + year="${1:?Usage: arrhes income-statements computations update }" + comp="${2:?missing }"; shift 2 + label='' + while [ $# -gt 0 ]; do case "$1" in --label) label="$2"; shift ;; *) _die "Unknown: $1" ;; esac; shift; done + _require_cfg; _jbody_reset; _jstr label "$label" + _api PATCH "$(_comp_base "$year")/$comp" "$(_jbody)" +} + +_comp_delete() { + year="${1:?Usage: arrhes income-statements computations delete }" + comp="${2:?missing }" + _require_cfg; _api DELETE "$(_comp_base "$year")/$comp" > /dev/null + printf 'Computation %s deleted.\n' "$comp" +} + +# ── usage ───────────────────────────────────────────────────────────────────── + +_usage() { + cat << 'EOF' +Usage: arrhes [subcommand] [options] + +Authentication: + login --api-key [--url ] + whoami + logout + +Commands: + org get | update | delete + years list | get | create | update | delete | close | open + settle-balance-sheet | settle-income-statement + journals list | get | create | update | delete + accounts list | get | create | update | delete + tags list | get | create | update | delete + entries list | get | create | update | duplicate | reverse | delete | compute + entries lines list | get | create | update | delete + entries tags add | remove + files list | get | create | update | delete | download-url + files folders list | get | create | update | delete + members list | get | invite | update | remove + api-keys list | create | delete + exports fec | xbrl-balance-sheet | xbrl-income-statement + balance-sheets list | get | create | update | delete + income-statements list | get | create | update | delete + income-statements computations list | get | create | update | delete + +Options: + --help, -h Show this help + --version, -v Show version ($VERSION) + +Output is JSON. Pipe through jq for filtering: + arrhes years list --year yr_xxx | jq '.[].id' +EOF +} + +# ── main ────────────────────────────────────────────────────────────────────── + +main() { + _need_cmd curl + cmd="${1:-}"; [ $# -gt 0 ] && shift + case "$cmd" in + login) _cmd_login "$@" ;; + whoami) _cmd_whoami ;; + logout) _cmd_logout ;; + org) _cmd_org "$@" ;; + years) _cmd_years "$@" ;; + journals) _cmd_journals "$@" ;; + accounts) _cmd_accounts "$@" ;; + tags) _cmd_tags "$@" ;; + entries) _cmd_entries "$@" ;; + files) _cmd_files "$@" ;; + members) _cmd_members "$@" ;; + api-keys) _cmd_api_keys "$@" ;; + exports) _cmd_exports "$@" ;; + balance-sheets) _cmd_balance_sheets "$@" ;; + income-statements) _cmd_income_statements "$@" ;; + --version|-v) printf 'arrhes %s\n' "$VERSION" ;; + --help|-h|'') _usage ;; + *) printf 'arrhes: unknown command: %s\n' "$cmd" >&2; _usage >&2; exit 1 ;; + esac +} + +main "$@" diff --git a/packages/cli/install.sh b/packages/cli/install.sh new file mode 100644 index 00000000..22fce5ec --- /dev/null +++ b/packages/cli/install.sh @@ -0,0 +1,32 @@ +#!/bin/sh +# Arrhes CLI installer for macOS and Linux +# Usage: curl -fsSL https://arrhes.com/cli/install.sh | sh +set -e + +REPO="arrhes/application" +INSTALL_DIR="${ARRHES_INSTALL_DIR:-$HOME/.local/bin}" +DEST="${INSTALL_DIR}/arrhes" + +command -v curl >/dev/null 2>&1 || { echo "Error: curl is required."; exit 1; } + +URL="https://github.com/${REPO}/releases/latest/download/arrhes.sh" + +echo "Downloading arrhes CLI..." +mkdir -p "$INSTALL_DIR" +curl -fsSL --progress-bar "$URL" -o "$DEST" +chmod +x "$DEST" + +echo "Installed: $DEST" +echo "Version: $($DEST --version)" + +# ── PATH hint ───────────────────────────────────────────────────────────────── +case ":$PATH:" in + *":${INSTALL_DIR}:"*) + echo "Run: arrhes --help" + ;; + *) + printf '\nAdd to PATH: export PATH="$HOME/.local/bin:$PATH"\n' + printf 'Then reload your shell, or run: source ~/.bashrc\n' + ;; +esac + diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 00000000..23e8af66 --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,7 @@ +{ + "name": "@arrhes/cli", + "version": "0.1.0", + "publishConfig": { + "access": "public" + } +} diff --git a/packages/metadata/package.json b/packages/metadata/package.json index 52a3ae4d..5666b809 100644 --- a/packages/metadata/package.json +++ b/packages/metadata/package.json @@ -17,30 +17,37 @@ }, "exports": { ".": { + "source": "./src/index.ts", "types": "./build/index.d.ts", "default": "./build/index.js" }, "./models": { + "source": "./src/models/index.ts", "types": "./build/models/index.d.ts", "default": "./build/models/index.js" }, "./components": { + "source": "./src/components/index.ts", "types": "./build/components/index.d.ts", "default": "./build/components/index.js" }, "./schemas": { + "source": "./src/schemas/index.ts", "types": "./build/schemas/index.d.ts", "default": "./build/schemas/index.js" }, "./routes": { + "source": "./src/routes/index.ts", "types": "./build/routes/index.d.ts", "default": "./build/routes/index.js" }, "./utilities": { + "source": "./src/utilities/index.ts", "types": "./build/utilities/index.d.ts", "default": "./build/utilities/index.js" }, "./entryTemplates": { + "source": "./src/entryTemplates/index.ts", "types": "./build/entryTemplates/index.d.ts", "default": "./build/entryTemplates/index.js" } diff --git a/packages/metadata/src/components/values/routePath.ts b/packages/metadata/src/components/values/routePath.ts index 0bb26b04..4bd79aef 100644 --- a/packages/metadata/src/components/values/routePath.ts +++ b/packages/metadata/src/components/values/routePath.ts @@ -2,4 +2,5 @@ export const routePath = { public: "/public", auth: "/auth", admin: "/admin", + v1: "/v1", } diff --git a/packages/metadata/src/models/file.ts b/packages/metadata/src/models/file.ts index b32b6f2a..44e8330b 100644 --- a/packages/metadata/src/models/file.ts +++ b/packages/metadata/src/models/file.ts @@ -5,7 +5,6 @@ import { idColumn } from "../components/models/idColumn.js" import { folderModel } from "./folder.js" import { organizationModel } from "./organization.js" import { userModel } from "./user.js" -import { yearModel } from "./year.js" // Model export const fileModel = pgTable( @@ -18,10 +17,6 @@ export const fileModel = pgTable( onUpdate: "cascade", }) .notNull(), - idYear: idColumn("id_year").references(() => yearModel.id, { - onDelete: "cascade", - onUpdate: "cascade", - }), idFolder: idColumn("id_folder").references(() => folderModel.id, { onDelete: "set null", onUpdate: "cascade", @@ -31,11 +26,12 @@ export const fileModel = pgTable( }), name: varchar("name", { length: 256, - }), + }).notNull(), storageKey: text("storage_key"), type: text("type"), size: integer("size"), hash: text("hash"), + date: dateTimeColumn("date"), createdAt: dateTimeColumn("created_at").notNull(), lastUpdatedAt: dateTimeColumn("last_updated_at"), createdBy: idColumn("created_by").references((): AnyPgColumn => userModel.id, { @@ -48,8 +44,8 @@ export const fileModel = pgTable( }), }, (table) => [ - uniqueIndex("table_file_id_organization_id_year_hash_unique") - .on(table.idOrganization, table.idYear, table.hash) + uniqueIndex("table_file_id_organization_hash_unique") + .on(table.idOrganization, table.hash) .where(sql`${table.hash} IS NOT NULL`), ], ) diff --git a/packages/metadata/src/models/folder.ts b/packages/metadata/src/models/folder.ts index c2fff8b1..4fda51ae 100644 --- a/packages/metadata/src/models/folder.ts +++ b/packages/metadata/src/models/folder.ts @@ -3,7 +3,6 @@ import { dateTimeColumn } from "../components/models/dateTimeColumn.js" import { idColumn } from "../components/models/idColumn.js" import { organizationModel } from "./organization.js" import { userModel } from "./user.js" -import { yearModel } from "./year.js" // Model export const folderModel = pgTable("table_folder", { @@ -14,12 +13,6 @@ export const folderModel = pgTable("table_folder", { onUpdate: "cascade", }) .notNull(), - idYear: idColumn("id_year") - .references(() => yearModel.id, { - onDelete: "cascade", - onUpdate: "cascade", - }) - .notNull(), idFolderParent: idColumn("id_folder_parent").references((): AnyPgColumn => folderModel.id, { onDelete: "cascade", onUpdate: "cascade", diff --git a/packages/metadata/src/models/user.ts b/packages/metadata/src/models/user.ts index 38303394..22d692a8 100644 --- a/packages/metadata/src/models/user.ts +++ b/packages/metadata/src/models/user.ts @@ -23,6 +23,12 @@ export const userModel = pgTable( passwordSalt: text("password_salt").notNull(), createdAt: dateTimeColumn("created_at").notNull(), lastUpdatedAt: dateTimeColumn("last_updated_at"), + dashboardMode: varchar("dashboard_mode", { + length: 16, + }) + .notNull() + .default("tabs") + .$type<"tabs" | "classic">(), }, (t) => [ index().on(t.emailToken), diff --git a/packages/metadata/src/routes/dashboard/auth/agent/agentFile/createOneAgentFile.ts b/packages/metadata/src/routes/dashboard/auth/agent/agentFile/createOneAgentFile.ts index ae6c080a..bda53b07 100644 --- a/packages/metadata/src/routes/dashboard/auth/agent/agentFile/createOneAgentFile.ts +++ b/packages/metadata/src/routes/dashboard/auth/agent/agentFile/createOneAgentFile.ts @@ -6,7 +6,8 @@ import { routeDefinition } from "../../../../../utilities/routeDefinition.js" export const createOneAgentFileRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/create-one-agent-file`, + method: "POST", + path: `${routePath.v1}/agent/sessions/:idAgentSession/files`, schemas: { body: v.object({ idOrganization: v.nonNullable(idSchema, "Ce champ est requis"), diff --git a/packages/metadata/src/routes/dashboard/auth/agent/agentMessage/createOneAgentMessage.ts b/packages/metadata/src/routes/dashboard/auth/agent/agentMessage/createOneAgentMessage.ts index df20ef3e..e26dac02 100644 --- a/packages/metadata/src/routes/dashboard/auth/agent/agentMessage/createOneAgentMessage.ts +++ b/packages/metadata/src/routes/dashboard/auth/agent/agentMessage/createOneAgentMessage.ts @@ -6,7 +6,8 @@ import { routeDefinition } from "../../../../../utilities/routeDefinition.js" export const createOneAgentMessageRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/create-one-agent-message`, + method: "POST", + path: `${routePath.v1}/agent/sessions/:idAgentSession/messages`, schemas: { body: v.object({ idOrganization: v.nonNullable(idSchema, "Ce champ est requis"), diff --git a/packages/metadata/src/routes/dashboard/auth/agent/agentMessage/getStreamForAgentMessage.ts b/packages/metadata/src/routes/dashboard/auth/agent/agentMessage/getStreamForAgentMessage.ts index 542df643..f2808f67 100644 --- a/packages/metadata/src/routes/dashboard/auth/agent/agentMessage/getStreamForAgentMessage.ts +++ b/packages/metadata/src/routes/dashboard/auth/agent/agentMessage/getStreamForAgentMessage.ts @@ -6,7 +6,8 @@ import { routeDefinition } from "../../../../../utilities/routeDefinition.js" export const getStreamForAgentMessageRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/get-stream-for-agent-message`, + method: "GET", + path: `${routePath.v1}/agent/sessions/:idAgentSession/messages/:idAgentMessage/stream`, schemas: { body: v.object({ idOrganization: v.nonNullable(idSchema, "Ce champ est requis"), diff --git a/packages/metadata/src/routes/dashboard/auth/agent/agentMessage/readAllAgentMessages.ts b/packages/metadata/src/routes/dashboard/auth/agent/agentMessage/readAllAgentMessages.ts index aee9193e..f7eb1444 100644 --- a/packages/metadata/src/routes/dashboard/auth/agent/agentMessage/readAllAgentMessages.ts +++ b/packages/metadata/src/routes/dashboard/auth/agent/agentMessage/readAllAgentMessages.ts @@ -6,7 +6,8 @@ import { routeDefinition } from "../../../../../utilities/routeDefinition.js" export const readAllAgentMessagesRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-all-agent-messages`, + method: "GET", + path: `${routePath.v1}/agent/sessions/:idAgentSession/messages`, schemas: { body: v.object({ idAgentSession: v.nonNullable(idSchema, "Ce champ est requis"), diff --git a/packages/metadata/src/routes/dashboard/auth/agent/agentSession/createOneAgentSession.ts b/packages/metadata/src/routes/dashboard/auth/agent/agentSession/createOneAgentSession.ts index ef8e6687..3d2ddceb 100644 --- a/packages/metadata/src/routes/dashboard/auth/agent/agentSession/createOneAgentSession.ts +++ b/packages/metadata/src/routes/dashboard/auth/agent/agentSession/createOneAgentSession.ts @@ -6,7 +6,8 @@ import { routeDefinition } from "../../../../../utilities/routeDefinition.js" export const createOneAgentSessionRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/create-one-agent-session`, + method: "POST", + path: `${routePath.v1}/agent/sessions`, schemas: { body: v.object({ idOrganization: v.nonNullable(idSchema, "Ce champ est requis"), diff --git a/packages/metadata/src/routes/dashboard/auth/agent/agentSession/deleteOneAgentSession.ts b/packages/metadata/src/routes/dashboard/auth/agent/agentSession/deleteOneAgentSession.ts index 42f54f8f..c61ab0e2 100644 --- a/packages/metadata/src/routes/dashboard/auth/agent/agentSession/deleteOneAgentSession.ts +++ b/packages/metadata/src/routes/dashboard/auth/agent/agentSession/deleteOneAgentSession.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../utilities/routeDefinition.js" export const deleteOneAgentSessionRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/delete-one-agent-session`, + method: "DELETE", + path: `${routePath.v1}/agent/sessions/:idAgentSession`, schemas: { body: v.object({ idAgentSession: v.nonNullable(idSchema, "Ce champ est requis"), diff --git a/packages/metadata/src/routes/dashboard/auth/agent/agentSession/readAllAgentSessions.ts b/packages/metadata/src/routes/dashboard/auth/agent/agentSession/readAllAgentSessions.ts index f632493a..e6b2f636 100644 --- a/packages/metadata/src/routes/dashboard/auth/agent/agentSession/readAllAgentSessions.ts +++ b/packages/metadata/src/routes/dashboard/auth/agent/agentSession/readAllAgentSessions.ts @@ -16,7 +16,8 @@ export const agentSessionWithMatchedContentSchema = v.object({ export const readAllAgentSessionsRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-all-agent-sessions`, + method: "GET", + path: `${routePath.v1}/agent/sessions`, schemas: { body: v.object({ idOrganization: v.nonNullable(idSchema, "Ce champ est requis"), diff --git a/packages/metadata/src/routes/dashboard/auth/agent/agentSession/readOneAgentSession.ts b/packages/metadata/src/routes/dashboard/auth/agent/agentSession/readOneAgentSession.ts index 245d8799..d747f287 100644 --- a/packages/metadata/src/routes/dashboard/auth/agent/agentSession/readOneAgentSession.ts +++ b/packages/metadata/src/routes/dashboard/auth/agent/agentSession/readOneAgentSession.ts @@ -6,7 +6,8 @@ import { routeDefinition } from "../../../../../utilities/routeDefinition.js" export const readOneAgentSessionRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-one-agent-session`, + method: "GET", + path: `${routePath.v1}/agent/sessions/:idAgentSession`, schemas: { body: v.object({ idAgentSession: v.nonNullable(idSchema, "Ce champ est requis"), diff --git a/packages/metadata/src/routes/dashboard/auth/agent/agentSession/updateOneAgentSession.ts b/packages/metadata/src/routes/dashboard/auth/agent/agentSession/updateOneAgentSession.ts index 72945eba..d0f9f813 100644 --- a/packages/metadata/src/routes/dashboard/auth/agent/agentSession/updateOneAgentSession.ts +++ b/packages/metadata/src/routes/dashboard/auth/agent/agentSession/updateOneAgentSession.ts @@ -6,7 +6,8 @@ import { routeDefinition } from "../../../../../utilities/routeDefinition.js" export const updateOneAgentSessionRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/update-one-agent-session`, + method: "PATCH", + path: `${routePath.v1}/agent/sessions/:idAgentSession`, schemas: { body: v.object({ idAgentSession: v.nonNullable(idSchema, "Ce champ est requis"), diff --git a/packages/metadata/src/routes/dashboard/auth/agent/getChatStream.ts b/packages/metadata/src/routes/dashboard/auth/agent/getChatStream.ts index 48acb5f2..e4217dbf 100644 --- a/packages/metadata/src/routes/dashboard/auth/agent/getChatStream.ts +++ b/packages/metadata/src/routes/dashboard/auth/agent/getChatStream.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../utilities/routeDefinition.js" export const getChatStreamRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/get-chat-stream`, + method: "GET", + path: `${routePath.v1}/agent/sessions/:idAgentSession/stream`, schemas: { body: v.object({ idAgentMessage: v.nonNullable(idSchema, "Ce champ est requis"), diff --git a/packages/metadata/src/routes/dashboard/auth/agent/searchReferenceable.ts b/packages/metadata/src/routes/dashboard/auth/agent/searchReferenceable.ts index b6faa360..0451fbac 100644 --- a/packages/metadata/src/routes/dashboard/auth/agent/searchReferenceable.ts +++ b/packages/metadata/src/routes/dashboard/auth/agent/searchReferenceable.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../utilities/routeDefinition.js" export const searchReferenceableRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/search-referenceable`, + method: "GET", + path: `${routePath.v1}/agent/search`, schemas: { body: v.object({ idOrganization: v.nonNullable(idSchema, "Ce champ est requis"), diff --git a/packages/metadata/src/routes/dashboard/auth/agent/undoAgentAction.ts b/packages/metadata/src/routes/dashboard/auth/agent/undoAgentAction.ts index 239ba040..7bb92cb2 100644 --- a/packages/metadata/src/routes/dashboard/auth/agent/undoAgentAction.ts +++ b/packages/metadata/src/routes/dashboard/auth/agent/undoAgentAction.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../utilities/routeDefinition.js" export const undoAgentActionRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/undo-agent-action`, + method: "POST", + path: `${routePath.v1}/agent/sessions/:idAgentSession/undo`, schemas: { body: v.object({ idAgentMessage: v.nonNullable(idSchema, "Ce champ est requis"), diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/index.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/index.ts index 49ba5c83..19a0a6de 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/index.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/index.ts @@ -46,6 +46,9 @@ export { createOneEntryRouteDefinition } from "./years/$idYear/entries/createOne export { createOneEntryFromTemplateRouteDefinition } from "./years/$idYear/entries/createOneEntryFromTemplate.js" export { readAllEntriesRouteDefinition } from "./years/$idYear/entries/readAllEntries.js" export { readAllEntryTagsRouteDefinition } from "./years/$idYear/entries/readAllEntryTags.js" +export { generateBalanceSheetXmlRouteDefinition } from "./years/$idYear/exports/generateBalanceSheetXml.js" +export { generateFecRouteDefinition } from "./years/$idYear/exports/generateFec.js" +export { generateIncomeStatementXmlRouteDefinition } from "./years/$idYear/exports/generateIncomeStatementXml.js" export { deleteOneFileRouteDefinition } from "./years/$idYear/files/$idFile/deleteOneFile.js" export { finalizeFileUploadRouteDefinition } from "./years/$idYear/files/$idFile/finalizeFileUpload.js" export { generateFileDeleteSignedUrlRouteDefinition } from "./years/$idYear/files/$idFile/generateFileDeleteSignedUrl.js" @@ -64,8 +67,6 @@ export { createOneFolderRouteDefinition } from "./years/$idYear/folders/createOn export { readAllFoldersRouteDefinition } from "./years/$idYear/folders/readAllFolders.js" // year export { readOneYearRouteDefinition } from "./years/$idYear/readOneYear.js" -export { generateBalanceSheetXmlRouteDefinition } from "./years/$idYear/reports/generateBalanceSheetXml.js" -export { generateIncomeStatementXmlRouteDefinition } from "./years/$idYear/reports/generateIncomeStatementXml.js" export { deleteOneAccountRouteDefinition } from "./years/$idYear/yearSettings/accounts/$idAccount/deleteOneAccount.js" export { readOneAccountRouteDefinition } from "./years/$idYear/yearSettings/accounts/$idAccount/readOneAccount.js" export { updateOneAccountRouteDefinition } from "./years/$idYear/yearSettings/accounts/$idAccount/updateOneAccount.js" diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/apiKey/createOneApiKey.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/apiKey/createOneApiKey.ts index 60194f88..0384ce1c 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/apiKey/createOneApiKey.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/apiKey/createOneApiKey.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../utilities/routeDefinition. export const createOneApiKeyRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/create-one-api-key`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/api-keys`, schemas: { body: v.object({ name: v.optional(apiKeySchema.entries.name), diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/apiKey/deleteOneApiKey.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/apiKey/deleteOneApiKey.ts index 7e68bcd8..5e03206a 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/apiKey/deleteOneApiKey.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/apiKey/deleteOneApiKey.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../utilities/routeDefinition. export const deleteOneApiKeyRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/delete-one-api-key`, + method: "DELETE", + path: `${routePath.v1}/organizations/:idOrganization/api-keys/:idApiKey`, schemas: { body: v.object({ idApiKey: apiKeySchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/apiKey/readAllApiKeys.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/apiKey/readAllApiKeys.ts index de5f432c..53aae96e 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/apiKey/readAllApiKeys.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/apiKey/readAllApiKeys.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../utilities/routeDefinition. export const readAllApiKeysRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-all-api-keys`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/api-keys`, schemas: { body: v.object({}), return: v.array(apiKeySchemaReturn), diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/deleteOneOrganization.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/deleteOneOrganization.ts index fcbdcf29..eacd370d 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/deleteOneOrganization.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/deleteOneOrganization.ts @@ -4,7 +4,8 @@ import { routeDefinition } from "../../../../../../utilities/routeDefinition.js" export const deleteOneOrganizationRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/delete-one-organization`, + method: "DELETE", + path: `${routePath.v1}/organizations/:idOrganization`, schemas: { body: v.object({}), return: v.object({}), diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/cancelOrganizationBilling.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/cancelOrganizationBilling.ts index b89674b5..533a627c 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/cancelOrganizationBilling.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/cancelOrganizationBilling.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../utilities/routeDefinition. export const cancelOrganizationBillingRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/cancel-organization-billing`, + method: "DELETE", + path: `${routePath.v1}/organizations/:idOrganization/billing`, schemas: { body: v.object({ idBilling: v.nonNullable(idSchema, "Ce champ est requis"), diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/cancelSubscription.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/cancelSubscription.ts index b4ca3d27..d320c9e1 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/cancelSubscription.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/cancelSubscription.ts @@ -4,7 +4,8 @@ import { routeDefinition } from "../../../../../../../utilities/routeDefinition. export const cancelSubscriptionRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/cancel-subscription`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/billing/cancel-subscription`, schemas: { body: v.object({}), return: v.object({}), diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createFirstPayment.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createFirstPayment.ts index 8a1650a6..f8d7b8bd 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createFirstPayment.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createFirstPayment.ts @@ -4,7 +4,8 @@ import { routeDefinition } from "../../../../../../../utilities/routeDefinition. export const createFirstPaymentRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/create-first-payment`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/billing/first-payment`, schemas: { body: v.object({}), return: v.object({ diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createPaymentMethodCheckout.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createPaymentMethodCheckout.ts index bc9c7d86..de770f6c 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createPaymentMethodCheckout.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createPaymentMethodCheckout.ts @@ -4,7 +4,8 @@ import { routeDefinition } from "../../../../../../../utilities/routeDefinition. export const createPaymentMethodCheckoutRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/create-payment-method-checkout`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/billing/payment-method-checkout`, schemas: { body: v.object({}), return: v.object({ diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createResourceSubscription.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createResourceSubscription.ts index ec775597..8ba9fe99 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createResourceSubscription.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createResourceSubscription.ts @@ -4,7 +4,8 @@ import { routeDefinition } from "../../../../../../../utilities/routeDefinition. export const createResourceSubscriptionRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/create-resource-subscription`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/billing/resource-subscription`, schemas: { body: v.object({ type: v.picklist( diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createWalletTopUpCheckout.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createWalletTopUpCheckout.ts index 7691bfac..ae7b56f2 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createWalletTopUpCheckout.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createWalletTopUpCheckout.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../utilities/routeDefinition. export const createWalletTopUpCheckoutRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/create-wallet-top-up-checkout`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/billing/wallet-top-up`, schemas: { body: v.object({ amountInCents: v.pipe(integerSchema, v.minValue(1)), diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createWalletWithdrawal.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createWalletWithdrawal.ts index 55b9e04e..99d21e11 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createWalletWithdrawal.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/createWalletWithdrawal.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../utilities/routeDefinition. export const createWalletWithdrawalRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/create-wallet-withdrawal`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/billing/wallet-withdrawal`, schemas: { body: v.object({ amountInCents: v.pipe(integerSchema, v.minValue(1)), diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/generateInvoiceGetSignedUrl.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/generateInvoiceGetSignedUrl.ts index ccaaf616..44d26209 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/generateInvoiceGetSignedUrl.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/generateInvoiceGetSignedUrl.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../utilities/routeDefinition. export const generateInvoiceGetSignedUrlRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/generate-invoice-get-signed-url`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/invoices/:idInvoice/download-url`, schemas: { body: v.object({ idInvoice: v.nonNullable(idSchema, "Ce champ est requis"), diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readAllInvoices.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readAllInvoices.ts index cb77d553..1b828e5e 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readAllInvoices.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readAllInvoices.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../utilities/routeDefinition. export const readAllInvoicesRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-all-invoices`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/invoices`, schemas: { body: v.object({}), return: v.array(invoiceSchemaReturn), diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readAllOrganizationBillings.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readAllOrganizationBillings.ts index 88fbb764..3ea833b4 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readAllOrganizationBillings.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readAllOrganizationBillings.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../utilities/routeDefinition. export const readAllOrganizationBillingsRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-all-organization-billings`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/billings`, schemas: { body: v.object({}), return: v.array(organizationBillingSchemaReturn), diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readAllOrganizationPayments.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readAllOrganizationPayments.ts index 44ecbf04..46be6216 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readAllOrganizationPayments.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readAllOrganizationPayments.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../utilities/routeDefinition. export const readAllOrganizationPaymentsRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-all-organization-payments`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/payments`, schemas: { body: v.object({}), return: v.array(organizationPaymentSchemaReturn), diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readOneInvoice.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readOneInvoice.ts index 290c7c8d..75a630bc 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readOneInvoice.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readOneInvoice.ts @@ -6,7 +6,8 @@ import { routeDefinition } from "../../../../../../../utilities/routeDefinition. export const readOneInvoiceRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-one-invoice`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/invoices/:idInvoice`, schemas: { body: v.object({ idInvoice: v.nonNullable(idSchema, "Ce champ est requis"), diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readOrganizationBilling.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readOrganizationBilling.ts index 5f166416..f4e4b31d 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readOrganizationBilling.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/readOrganizationBilling.ts @@ -4,7 +4,8 @@ import { routeDefinition } from "../../../../../../../utilities/routeDefinition. export const readOrganizationBillingRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-organization-billing`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/billing`, schemas: { body: v.object({}), return: v.object({ diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/updateLicenceSubscription.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/updateLicenceSubscription.ts index d94676f6..69a16ec7 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/updateLicenceSubscription.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/updateLicenceSubscription.ts @@ -4,7 +4,8 @@ import { routeDefinition } from "../../../../../../../utilities/routeDefinition. export const updateLicenceSubscriptionRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/update-licence-subscription`, + method: "PATCH", + path: `${routePath.v1}/organizations/:idOrganization/billing/licence-subscription`, schemas: { body: v.object({ newAmountInCents: v.pipe(integerSchema, v.minValue(0)), diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/updateOcrSubscription.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/updateOcrSubscription.ts index db430c91..ce9b85b0 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/updateOcrSubscription.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/updateOcrSubscription.ts @@ -4,7 +4,8 @@ import { routeDefinition } from "../../../../../../../utilities/routeDefinition. export const updateOcrSubscriptionRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/update-ocr-subscription`, + method: "PATCH", + path: `${routePath.v1}/organizations/:idOrganization/billing/ocr-subscription`, schemas: { body: v.object({ newQuantity: v.pipe(integerSchema, v.minValue(0)), diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/updateStorageSubscription.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/updateStorageSubscription.ts index 8e806d2a..446ed3bc 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/updateStorageSubscription.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/updateStorageSubscription.ts @@ -4,7 +4,8 @@ import { routeDefinition } from "../../../../../../../utilities/routeDefinition. export const updateStorageSubscriptionRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/update-storage-subscription`, + method: "PATCH", + path: `${routePath.v1}/organizations/:idOrganization/billing/storage-subscription`, schemas: { body: v.object({ newQuantity: v.pipe(integerSchema, v.minValue(0)), diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/updateTokensSubscription.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/updateTokensSubscription.ts index 976528d8..3fc54827 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/updateTokensSubscription.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationPayment/updateTokensSubscription.ts @@ -4,7 +4,8 @@ import { routeDefinition } from "../../../../../../../utilities/routeDefinition. export const updateTokensSubscriptionRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/update-tokens-subscription`, + method: "PATCH", + path: `${routePath.v1}/organizations/:idOrganization/billing/tokens-subscription`, schemas: { body: v.object({ newQuantity: v.pipe(integerSchema, v.minValue(0)), diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationUser/createOneOrganizationUser.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationUser/createOneOrganizationUser.ts index da7eb9c4..7670d0a4 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationUser/createOneOrganizationUser.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationUser/createOneOrganizationUser.ts @@ -6,7 +6,8 @@ import { routeDefinition } from "../../../../../../../utilities/routeDefinition. export const createOneOrganizationUserRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/create-one-organization-user`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/users`, schemas: { body: v.object({ isAdmin: organizationUserSchema.entries.isAdmin, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationUser/deleteOneOrganizationUser.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationUser/deleteOneOrganizationUser.ts index 32cbb49e..283f9913 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationUser/deleteOneOrganizationUser.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationUser/deleteOneOrganizationUser.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../utilities/routeDefinition. export const deleteOneOrganizationUserRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/delete-one-organization-user`, + method: "DELETE", + path: `${routePath.v1}/organizations/:idOrganization/users/:idOrganizationUser`, schemas: { body: v.object({ idOrganizationUser: organizationUserSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationUser/readAllOrganizationUsers.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationUser/readAllOrganizationUsers.ts index 6e58d478..6f8c0126 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationUser/readAllOrganizationUsers.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationUser/readAllOrganizationUsers.ts @@ -6,7 +6,8 @@ import { routeDefinition } from "../../../../../../../utilities/routeDefinition. export const readAllOrganizationUsersRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-all-organization-users`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/users`, schemas: { body: v.object({}), return: v.array( diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationUser/readOneOrganizationUser.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationUser/readOneOrganizationUser.ts index 4541194b..ae0c8af0 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationUser/readOneOrganizationUser.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationUser/readOneOrganizationUser.ts @@ -6,7 +6,8 @@ import { routeDefinition } from "../../../../../../../utilities/routeDefinition. export const readOneOrganizationUserRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-one-organization-user`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/users/:idOrganizationUser`, schemas: { body: v.object({ idOrganizationUser: organizationUserSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationUser/updateOneOrganizationUser.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationUser/updateOneOrganizationUser.ts index 6d929510..0ea14453 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationUser/updateOneOrganizationUser.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/organizationUser/updateOneOrganizationUser.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../utilities/routeDefinition. export const updateOneOrganizationUserRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/update-one-organization-user`, + method: "PATCH", + path: `${routePath.v1}/organizations/:idOrganization/users/:idOrganizationUser`, schemas: { body: v.object({ idOrganizationUser: organizationUserSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/updateOneOrganization.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/updateOneOrganization.ts index 8a2fe6b3..33f5c37c 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/updateOneOrganization.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/organizationSettings/updateOneOrganization.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../utilities/routeDefinition.js" export const updateOneOrganizationRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/update-one-organization`, + method: "PATCH", + path: `${routePath.v1}/organizations/:idOrganization`, schemas: { body: v.object({ siren: v.optional(organizationSchema.entries.siren), diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/readOneOrganization.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/readOneOrganization.ts index 4dafab17..1c5eaa3b 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/readOneOrganization.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/readOneOrganization.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../utilities/routeDefinition.js" export const readOneOrganizationRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-one-organization`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization`, schemas: { body: v.object({ idOrganization: organizationSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/computeOneEntry.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/computeOneEntry.ts index 1ebbb4f0..e8110d19 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/computeOneEntry.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/computeOneEntry.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const computeOneEntryRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/compute-one-entry`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/entries/:idEntry/compute`, schemas: { body: v.object({ idEntry: entrySchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/deleteOneEntry.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/deleteOneEntry.ts index 7426faa7..fc718368 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/deleteOneEntry.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/deleteOneEntry.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const deleteOneEntryRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/delete-one-entry`, + method: "DELETE", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/entries/:idEntry`, schemas: { body: v.object({ idEntry: entrySchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/duplicateOneEntry.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/duplicateOneEntry.ts index 6b3863d9..c4861ecb 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/duplicateOneEntry.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/duplicateOneEntry.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const duplicateOneEntryRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/duplicate-one-entry`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/entries/:idEntry/duplicate`, schemas: { body: v.object({ idEntry: entrySchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/$idEntryLine/deleteOneEntryLine.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/$idEntryLine/deleteOneEntryLine.ts index 17d779b3..defb24fb 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/$idEntryLine/deleteOneEntryLine.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/$idEntryLine/deleteOneEntryLine.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../../../utilities/rout export const deleteOneEntryLineRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/delete-one-entry-line`, + method: "DELETE", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/entries/:idEntry/lines/:idEntryLine`, schemas: { body: v.object({ idEntryLine: entryLineSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/$idEntryLine/readOneEntryLine.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/$idEntryLine/readOneEntryLine.ts index ed070c1f..7d3fff1b 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/$idEntryLine/readOneEntryLine.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/$idEntryLine/readOneEntryLine.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../../../utilities/rout export const readOneEntryLineRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-one-entry-line`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/entries/:idEntry/lines/:idEntryLine`, schemas: { body: v.object({ idEntryLine: entryLineSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/$idEntryLine/updateOneEntryLine.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/$idEntryLine/updateOneEntryLine.ts index fc406201..3fcf4052 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/$idEntryLine/updateOneEntryLine.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/$idEntryLine/updateOneEntryLine.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../../../utilities/rout export const updateOneEntryLineRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/update-one-entry-line`, + method: "PATCH", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/entries/:idEntry/lines/:idEntryLine`, schemas: { body: v.object({ idEntryLine: entryLineSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/createOneEntryLine.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/createOneEntryLine.ts index c50e7ec3..ad971040 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/createOneEntryLine.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/createOneEntryLine.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../../utilities/routeDe export const createOneEntryLineRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/create-one-entry-line`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/entries/:idEntry/lines`, schemas: { body: v.object({ idYear: entryLineSchema.entries.idYear, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/readAllEntryLines.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/readAllEntryLines.ts index 552f95a0..14093f49 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/readAllEntryLines.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/readAllEntryLines.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../../utilities/routeDe export const readAllEntryLinesRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-all-entry-lines`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/entries/:idEntry/lines`, schemas: { body: v.object({ idYear: entryLineSchema.entries.idYear, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/updateManyEntryLines.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/updateManyEntryLines.ts index ec4b4ee1..cfc1f063 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/updateManyEntryLines.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLines/updateManyEntryLines.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../../utilities/routeDe export const updateManyEntryLinesRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/update-many-entry-lines`, + method: "PATCH", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/entries/:idEntry/lines`, schemas: { body: v.object({ idEntry: entryLineSchema.entries.idEntry, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryTags/addOneEntryTag.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryTags/addOneEntryTag.ts index 225d6b14..b2348123 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryTags/addOneEntryTag.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryTags/addOneEntryTag.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../../utilities/routeDe export const addOneEntryTagRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/add-one-entry-tag`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/entries/:idEntry/tags`, schemas: { body: v.object({ idYear: entryTagSchema.entries.idYear, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryTags/removeOneEntryTag.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryTags/removeOneEntryTag.ts index ae7f3da0..bb77d1ea 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryTags/removeOneEntryTag.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryTags/removeOneEntryTag.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../../utilities/routeDe export const removeOneEntryTagRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/remove-one-entry-tag`, + method: "DELETE", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/entries/:idEntry/tags/:idTag`, schemas: { body: v.object({ idYear: entryTagSchema.entries.idYear, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/readOneEntry.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/readOneEntry.ts index e81434f7..1ab0b636 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/readOneEntry.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/readOneEntry.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const readOneEntryRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-one-entry`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/entries/:idEntry`, schemas: { body: v.object({ idEntry: entrySchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/reverseOneEntry.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/reverseOneEntry.ts index 9780bcdd..7e560dac 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/reverseOneEntry.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/reverseOneEntry.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const reverseOneEntryRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/reverse-one-entry`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/entries/:idEntry/reverse`, schemas: { body: v.object({ idEntry: entrySchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/updateOneEntry.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/updateOneEntry.ts index 95465416..f068d17e 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/updateOneEntry.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/$idEntry/updateOneEntry.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const updateOneEntryRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/update-one-entry`, + method: "PATCH", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/entries/:idEntry`, schemas: { body: v.object({ idEntry: entrySchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/createOneEntry.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/createOneEntry.ts index cae13123..716f2a50 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/createOneEntry.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/createOneEntry.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../utilities/routeDefiniti export const createOneEntryRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/create-one-entry`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/entries`, schemas: { body: v.object({ idYear: entrySchema.entries.idYear, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/createOneEntryFromTemplate.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/createOneEntryFromTemplate.ts index 2c5180c1..2cb182bd 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/createOneEntryFromTemplate.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/createOneEntryFromTemplate.ts @@ -6,7 +6,8 @@ import { routeDefinition } from "../../../../../../../../utilities/routeDefiniti export const createOneEntryFromTemplateRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/create-one-entry-from-template`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/entries/from-template`, schemas: { body: v.object({ idYear: entrySchema.entries.idYear, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/readAllEntries.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/readAllEntries.ts index 1f91d99f..7aa48d53 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/readAllEntries.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/readAllEntries.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../utilities/routeDefiniti export const readAllEntriesRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-all-entries`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/entries`, schemas: { body: v.object({ idYear: entrySchema.entries.idYear, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/readAllEntryTags.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/readAllEntryTags.ts index c647ea2f..7ef5d0e6 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/readAllEntryTags.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/entries/readAllEntryTags.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../utilities/routeDefiniti export const readAllEntryTagsRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-all-entry-tags`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/entry-tags`, schemas: { body: v.object({ idYear: entryTagSchema.entries.idYear, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/exports/generateBalanceSheetXml.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/exports/generateBalanceSheetXml.ts new file mode 100644 index 00000000..8f53dec9 --- /dev/null +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/exports/generateBalanceSheetXml.ts @@ -0,0 +1,18 @@ +import * as v from "valibot" +import { routePath } from "../../../../../../../../components/index.js" +import { yearSchema } from "../../../../../../../../schemas/year.js" +import { routeDefinition } from "../../../../../../../../utilities/routeDefinition.js" + +export const generateBalanceSheetXmlRouteDefinition = routeDefinition({ + protocol: "http", + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/exports/balance-sheet`, + schemas: { + body: v.object({ + idYear: yearSchema.entries.id, + }), + return: v.object({ + url: v.string(), + }), + }, +}) diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/exports/generateFec.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/exports/generateFec.ts new file mode 100644 index 00000000..77f6dbd1 --- /dev/null +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/exports/generateFec.ts @@ -0,0 +1,18 @@ +import * as v from "valibot" +import { routePath } from "../../../../../../../../components/index.js" +import { yearSchema } from "../../../../../../../../schemas/year.js" +import { routeDefinition } from "../../../../../../../../utilities/routeDefinition.js" + +export const generateFecRouteDefinition = routeDefinition({ + protocol: "http", + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/exports/fec`, + schemas: { + body: v.object({ + idYear: yearSchema.entries.id, + }), + return: v.object({ + url: v.string(), + }), + }, +}) diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/exports/generateIncomeStatementXml.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/exports/generateIncomeStatementXml.ts new file mode 100644 index 00000000..ebc26252 --- /dev/null +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/exports/generateIncomeStatementXml.ts @@ -0,0 +1,18 @@ +import * as v from "valibot" +import { routePath } from "../../../../../../../../components/index.js" +import { yearSchema } from "../../../../../../../../schemas/year.js" +import { routeDefinition } from "../../../../../../../../utilities/routeDefinition.js" + +export const generateIncomeStatementXmlRouteDefinition = routeDefinition({ + protocol: "http", + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/exports/income-statement`, + schemas: { + body: v.object({ + idYear: yearSchema.entries.id, + }), + return: v.object({ + url: v.string(), + }), + }, +}) diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/exports/index.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/exports/index.ts new file mode 100644 index 00000000..e7d7a225 --- /dev/null +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/exports/index.ts @@ -0,0 +1,3 @@ +export { generateBalanceSheetXmlRouteDefinition } from "./generateBalanceSheetXml.js" +export { generateFecRouteDefinition } from "./generateFec.js" +export { generateIncomeStatementXmlRouteDefinition } from "./generateIncomeStatementXml.js" diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/deleteOneFile.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/deleteOneFile.ts index 380068a1..6731bd90 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/deleteOneFile.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/deleteOneFile.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const deleteOneFileRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/delete-one-file`, + method: "DELETE", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/files/:idFile`, schemas: { body: v.object({ idFile: fileSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/finalizeFileUpload.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/finalizeFileUpload.ts index b2dc0b1a..a9eeacb9 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/finalizeFileUpload.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/finalizeFileUpload.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const finalizeFileUploadRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/finalize-file-upload`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/files/:idFile/finalize`, schemas: { body: v.object({ idFile: fileSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/generateFileDeleteSignedUrl.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/generateFileDeleteSignedUrl.ts index df9243d9..b25c83d7 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/generateFileDeleteSignedUrl.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/generateFileDeleteSignedUrl.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const generateFileDeleteSignedUrlRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/generate-file-delete-signed-url`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/files/:idFile/delete-url`, schemas: { body: v.object({ idFile: fileSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/generateFileGetSignedUrl.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/generateFileGetSignedUrl.ts index 2664b685..49eee553 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/generateFileGetSignedUrl.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/generateFileGetSignedUrl.ts @@ -5,11 +5,11 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const generateFileGetSignedUrlRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/generate-file-get-signed-url`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/files/:idFile/download-url`, schemas: { body: v.object({ idFile: fileSchema.entries.id, - idYear: fileSchema.entries.idYear, }), return: v.object({ url: v.string(), diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/generateFilePutSignedUrl.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/generateFilePutSignedUrl.ts index 347e7c8e..c9f80fda 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/generateFilePutSignedUrl.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/generateFilePutSignedUrl.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const generateFilePutSignedUrlRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/generate-file-put-signed-url`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/files/:idFile/upload-url`, schemas: { body: v.object({ idFile: fileSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/ocrFile.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/ocrFile.ts index 8433d42d..1973ab10 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/ocrFile.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/ocrFile.ts @@ -5,11 +5,11 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const ocrFileRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/ocr-file`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/files/:idFile/ocr`, schemas: { body: v.object({ idFile: fileSchema.entries.id, - idYear: fileSchema.entries.idYear, }), return: v.object({ file: fileSchemaReturn, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/readOneFile.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/readOneFile.ts index 2ab709ec..bfd27d22 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/readOneFile.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/readOneFile.ts @@ -5,11 +5,11 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const readOneFileRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-one-file`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/files/:idFile`, schemas: { body: v.object({ idFile: fileSchema.entries.id, - idYear: fileSchema.entries.idYear, }), return: fileSchemaReturn, }, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/updateOneFile.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/updateOneFile.ts index 10e79b21..fd414a5f 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/updateOneFile.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/$idFile/updateOneFile.ts @@ -5,13 +5,14 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const updateOneFileRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/update-one-file`, + method: "PATCH", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/files/:idFile`, schemas: { body: v.object({ idFile: fileSchema.entries.id, - idYear: fileSchema.entries.idYear, reference: v.optional(fileSchema.entries.reference), name: v.optional(fileSchema.entries.name), + date: v.optional(fileSchema.entries.date), idFolder: v.optional(fileSchema.entries.idFolder), }), return: fileSchemaReturn, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/createOneFile.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/createOneFile.ts index 2411e3e3..215fc2fb 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/createOneFile.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/createOneFile.ts @@ -5,13 +5,13 @@ import { routeDefinition } from "../../../../../../../../utilities/routeDefiniti export const createOneFileRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/create-one-file`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/files`, schemas: { body: v.object({ - idYear: fileSchema.entries.idYear, idFolder: v.optional(fileSchema.entries.idFolder), reference: fileSchema.entries.reference, - name: v.optional(fileSchema.entries.name), + name: fileSchema.entries.name, hash: fileSchema.entries.hash, }), return: fileSchemaReturn, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/readAllFiles.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/readAllFiles.ts index 1ad5986d..9416bcd1 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/readAllFiles.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/files/readAllFiles.ts @@ -1,15 +1,14 @@ import * as v from "valibot" import { routePath } from "../../../../../../../../components/index.js" -import { fileSchema, fileSchemaReturn } from "../../../../../../../../schemas/file.js" +import { fileSchemaReturn } from "../../../../../../../../schemas/file.js" import { routeDefinition } from "../../../../../../../../utilities/routeDefinition.js" export const readAllFilesRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-all-files`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/files`, schemas: { - body: v.object({ - idYear: fileSchema.entries.idYear, - }), + body: v.object({}), return: v.array(fileSchemaReturn), }, }) diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/folders/$idFolder/deleteOneFolder.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/folders/$idFolder/deleteOneFolder.ts index 9203b787..d0271198 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/folders/$idFolder/deleteOneFolder.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/folders/$idFolder/deleteOneFolder.ts @@ -5,11 +5,11 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const deleteOneFolderRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/delete-one-folder`, + method: "DELETE", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/folders/:idFolder`, schemas: { body: v.object({ idFolder: folderSchema.entries.id, - idYear: folderSchema.entries.idYear, }), return: v.object({}), }, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/folders/$idFolder/readOneFolder.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/folders/$idFolder/readOneFolder.ts index 40d1af9d..87843874 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/folders/$idFolder/readOneFolder.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/folders/$idFolder/readOneFolder.ts @@ -5,11 +5,11 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const readOneFolderRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-one-folder`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/folders/:idFolder`, schemas: { body: v.object({ idFolder: folderSchema.entries.id, - idYear: folderSchema.entries.idYear, }), return: folderSchemaReturn, }, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/folders/$idFolder/updateOneFolder.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/folders/$idFolder/updateOneFolder.ts index 338ef137..6afc5550 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/folders/$idFolder/updateOneFolder.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/folders/$idFolder/updateOneFolder.ts @@ -5,11 +5,11 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const updateOneFolderRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/update-one-folder`, + method: "PATCH", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/folders/:idFolder`, schemas: { body: v.object({ idFolder: folderSchema.entries.id, - idYear: folderSchema.entries.idYear, name: v.optional(folderSchema.entries.name), idFolderParent: v.optional(folderSchema.entries.idFolderParent), }), diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/folders/createOneFolder.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/folders/createOneFolder.ts index 8b0b1a9a..856c685a 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/folders/createOneFolder.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/folders/createOneFolder.ts @@ -5,10 +5,10 @@ import { routeDefinition } from "../../../../../../../../utilities/routeDefiniti export const createOneFolderRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/create-one-folder`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/folders`, schemas: { body: v.object({ - idYear: folderSchema.entries.idYear, idFolderParent: v.optional(folderSchema.entries.idFolderParent), name: folderSchema.entries.name, }), diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/folders/readAllFolders.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/folders/readAllFolders.ts index 2ed2b7a0..d891d64d 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/folders/readAllFolders.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/folders/readAllFolders.ts @@ -1,15 +1,14 @@ import * as v from "valibot" import { routePath } from "../../../../../../../../components/index.js" -import { folderSchema, folderSchemaReturn } from "../../../../../../../../schemas/folder.js" +import { folderSchemaReturn } from "../../../../../../../../schemas/folder.js" import { routeDefinition } from "../../../../../../../../utilities/routeDefinition.js" export const readAllFoldersRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-all-folders`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/folders`, schemas: { - body: v.object({ - idYear: folderSchema.entries.idYear, - }), + body: v.object({}), return: v.array(folderSchemaReturn), }, }) diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/readOneYear.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/readOneYear.ts index 1579ec7b..061330c9 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/readOneYear.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/readOneYear.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../utilities/routeDefinition. export const readOneYearRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-one-year`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear`, schemas: { body: v.object({ idYear: yearSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/reports/generateBalanceSheetXml.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/reports/generateBalanceSheetXml.ts deleted file mode 100644 index a259db0a..00000000 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/reports/generateBalanceSheetXml.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as v from "valibot" -import { routePath } from "../../../../../../../../components/index.js" -import { yearSchema } from "../../../../../../../../schemas/year.js" -import { routeDefinition } from "../../../../../../../../utilities/routeDefinition.js" - -export const generateBalanceSheetXmlRouteDefinition = routeDefinition({ - protocol: "http", - path: `${routePath.auth}/generate-balance-sheet-xml`, - schemas: { - body: v.object({ - idYear: yearSchema.entries.id, - }), - return: v.object({ - url: v.string(), - }), - }, -}) diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/reports/generateIncomeStatementXml.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/reports/generateIncomeStatementXml.ts deleted file mode 100644 index 46ec6090..00000000 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/reports/generateIncomeStatementXml.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as v from "valibot" -import { routePath } from "../../../../../../../../components/index.js" -import { yearSchema } from "../../../../../../../../schemas/year.js" -import { routeDefinition } from "../../../../../../../../utilities/routeDefinition.js" - -export const generateIncomeStatementXmlRouteDefinition = routeDefinition({ - protocol: "http", - path: `${routePath.auth}/generate-income-statement-xml`, - schemas: { - body: v.object({ - idYear: yearSchema.entries.id, - }), - return: v.object({ - url: v.string(), - }), - }, -}) diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/reports/index.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/reports/index.ts deleted file mode 100644 index be42041a..00000000 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/reports/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { generateBalanceSheetXmlRouteDefinition } from "./generateBalanceSheetXml.js" -export { generateIncomeStatementXmlRouteDefinition } from "./generateIncomeStatementXml.js" diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/deleteOneAccount.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/deleteOneAccount.ts index 7800207d..8becee14 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/deleteOneAccount.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/deleteOneAccount.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../../utilities/routeDe export const deleteOneAccountRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/delete-one-account`, + method: "DELETE", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/accounts/:idAccount`, schemas: { body: v.object({ idAccount: accountSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/readOneAccount.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/readOneAccount.ts index 99638fde..3c1dfbbc 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/readOneAccount.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/readOneAccount.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../../utilities/routeDe export const readOneAccountRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-one-account`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/accounts/:idAccount`, schemas: { body: v.object({ idAccount: accountSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/updateOneAccount.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/updateOneAccount.ts index b9ca1ebf..ed90c971 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/updateOneAccount.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/updateOneAccount.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../../utilities/routeDe export const updateOneAccountRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/update-one-account`, + method: "PATCH", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/accounts/:idAccount`, schemas: { body: v.object({ idAccount: accountSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/createOneAccount.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/createOneAccount.ts index fc1ac463..317abf7d 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/createOneAccount.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/createOneAccount.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const createOneAccountRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/create-one-account`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/accounts`, schemas: { body: v.object({ idYear: accountSchema.entries.idYear, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/generateAccounts.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/generateAccounts.ts index b76a13b2..fe79d867 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/generateAccounts.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/generateAccounts.ts @@ -1,4 +1,3 @@ -/* import * as v from "valibot" import { routePath } from "../../../../../../../../../components/index.js" import { booleanSchema } from "../../../../../../../../../components/schemas/booleanSchema.js" @@ -7,7 +6,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const generateAccountsRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/generate-accounts`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/accounts/generate`, schemas: { body: v.object({ idYear: accountSchema.entries.idYear, @@ -17,4 +17,3 @@ export const generateAccountsRouteDefinition = routeDefinition({ return: v.array(accountSchemaReturn), }, }) -*/ diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/readAllAccounts.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/readAllAccounts.ts index cd19b290..6a9acdbd 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/readAllAccounts.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/accounts/readAllAccounts.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const readAllAccountsRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-all-accounts`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/accounts`, schemas: { body: v.object({ idYear: accountSchema.entries.idYear, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/deleteOneBalanceSheet.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/deleteOneBalanceSheet.ts index 73129b7c..512bd043 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/deleteOneBalanceSheet.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/deleteOneBalanceSheet.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../../utilities/routeDe export const deleteOneBalanceSheetRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/delete-one-balance-sheet`, + method: "DELETE", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/balance-sheets/:idBalanceSheet`, schemas: { body: v.object({ idBalanceSheet: balanceSheetSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/readOneBalanceSheet.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/readOneBalanceSheet.ts index bf5dff56..b01bcfff 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/readOneBalanceSheet.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/readOneBalanceSheet.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../../utilities/routeDe export const readOneBalanceSheetRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-one-balance-sheet`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/balance-sheets/:idBalanceSheet`, schemas: { body: v.object({ idBalanceSheet: balanceSheetSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/updateOneBalanceSheet.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/updateOneBalanceSheet.ts index 68cfc18e..64b9dfe6 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/updateOneBalanceSheet.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/updateOneBalanceSheet.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../../utilities/routeDe export const updateOneBalanceSheetRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/update-one-balance-sheet`, + method: "PATCH", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/balance-sheets/:idBalanceSheet`, schemas: { body: v.object({ idBalanceSheet: balanceSheetSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/connectAccountsToBalanceSheets.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/connectAccountsToBalanceSheets.ts index ad90b01b..2664645c 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/connectAccountsToBalanceSheets.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/connectAccountsToBalanceSheets.ts @@ -1,4 +1,3 @@ -/* import * as v from "valibot" import { routePath } from "../../../../../../../../../components/index.js" import { incomeStatementSchema } from "../../../../../../../../../schemas/incomeStatement.js" @@ -6,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const connectAccountsToBalanceSheetsRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/connect-accounts-to-balance-sheets`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/balance-sheets/connect-accounts`, schemas: { body: v.object({ idYear: incomeStatementSchema.entries.idYear, @@ -14,4 +14,3 @@ export const connectAccountsToBalanceSheetsRouteDefinition = routeDefinition({ return: v.object({}), }, }) -*/ diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/createOneBalanceSheet.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/createOneBalanceSheet.ts index 257f51c1..a5825eba 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/createOneBalanceSheet.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/createOneBalanceSheet.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const createOneBalanceSheetRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/create-one-balance-sheet`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/balance-sheets`, schemas: { body: v.object({ idYear: balanceSheetSchema.entries.idYear, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/generateBalanceSheets.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/generateBalanceSheets.ts index b7611c0c..abce8a80 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/generateBalanceSheets.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/generateBalanceSheets.ts @@ -1,4 +1,3 @@ -/* import * as v from "valibot" import { routePath } from "../../../../../../../../../components/index.js" import { balanceSheetSchema, balanceSheetSchemaReturn } from "../../../../../../../../../schemas/balanceSheet.js" @@ -6,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const generateBalanceSheetsRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/generate-balance-sheets`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/balance-sheets/generate`, schemas: { body: v.object({ idYear: balanceSheetSchema.entries.idYear, @@ -14,4 +14,3 @@ export const generateBalanceSheetsRouteDefinition = routeDefinition({ return: v.array(balanceSheetSchemaReturn), }, }) -*/ diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/readAllBalanceSheets.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/readAllBalanceSheets.ts index 810c24ca..9bd5b495 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/readAllBalanceSheets.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/readAllBalanceSheets.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const readAllBalanceSheetsRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-all-balance-sheets`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/balance-sheets`, schemas: { body: v.object({ idYear: balanceSheetSchema.entries.idYear, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/deleteOneComputationIncomeStatement.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/deleteOneComputationIncomeStatement.ts index 437c2eca..8f6869ca 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/deleteOneComputationIncomeStatement.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/deleteOneComputationIncomeStatement.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../../../../utilities/r export const deleteOneComputationIncomeStatementRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/delete-one-computation-income-statement`, + method: "DELETE", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/computations/:idComputation/income-statements/:idComputationIncomeStatement`, schemas: { body: v.object({ idComputationIncomeStatement: computationIncomeStatementSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/readOneComputationIncomeStatement.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/readOneComputationIncomeStatement.ts index 2f63ae00..47ac74a8 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/readOneComputationIncomeStatement.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/readOneComputationIncomeStatement.ts @@ -8,7 +8,8 @@ import { routeDefinition } from "../../../../../../../../../../../../utilities/r export const readOneComputationIncomeStatementRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-one-computation-income-statement`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/computations/:idComputation/income-statements/:idComputationIncomeStatement`, schemas: { body: v.object({ idComputationIncomeStatement: computationIncomeStatementSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/updateOneComputationIncomeStatement.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/updateOneComputationIncomeStatement.ts index 45111246..0fce6280 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/updateOneComputationIncomeStatement.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/updateOneComputationIncomeStatement.ts @@ -8,7 +8,8 @@ import { routeDefinition } from "../../../../../../../../../../../../utilities/r export const updateOneComputationIncomeStatementRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/update-one-computation-income-statement`, + method: "PATCH", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/computations/:idComputation/income-statements/:idComputationIncomeStatement`, schemas: { body: v.object({ idComputationIncomeStatement: computationIncomeStatementSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/createOneComputationIncomeStatement.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/createOneComputationIncomeStatement.ts index 9ba64a42..04956bf9 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/createOneComputationIncomeStatement.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/createOneComputationIncomeStatement.ts @@ -8,7 +8,8 @@ import { routeDefinition } from "../../../../../../../../../../../utilities/rout export const createOneComputationIncomeStatementRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/create-one-computation-income-statement`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/computations/:idComputation/income-statements`, schemas: { body: v.object({ idYear: computationIncomeStatementSchema.entries.idYear, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/readAllComputationIncomeStatements.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/readAllComputationIncomeStatements.ts index 1438848e..2c3d670f 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/readAllComputationIncomeStatements.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/computationIncomeStatements/readAllComputationIncomeStatements.ts @@ -8,7 +8,8 @@ import { routeDefinition } from "../../../../../../../../../../../utilities/rout export const readAllComputationIncomeStatementsRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-all-computation-income-statements`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/computations/:idComputation/income-statements`, schemas: { body: v.object({ idYear: computationIncomeStatementSchema.entries.idYear, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/deleteOneComputation.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/deleteOneComputation.ts index d9a67317..2c7bf41f 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/deleteOneComputation.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/deleteOneComputation.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../../utilities/routeDe export const deleteOneComputationRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/delete-one-computation`, + method: "DELETE", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/computations/:idComputation`, schemas: { body: v.object({ idComputation: computationSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/readOneComputation.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/readOneComputation.ts index b4024a0a..d0af7e4b 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/readOneComputation.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/readOneComputation.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../../utilities/routeDe export const readOneComputationRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-one-computation`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/computations/:idComputation`, schemas: { body: v.object({ idComputation: computationSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/updateOneComputation.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/updateOneComputation.ts index bba04783..7daa815d 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/updateOneComputation.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/$idComputation/updateOneComputation.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../../utilities/routeDe export const updateOneComputationRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/update-one-computation`, + method: "PATCH", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/computations/:idComputation`, schemas: { body: v.object({ idComputation: computationSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/createOneComputation.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/createOneComputation.ts index d4f466e0..41332a15 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/createOneComputation.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/createOneComputation.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const createOneComputationRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/create-one-computation`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/computations`, schemas: { body: v.object({ idYear: computationSchema.entries.idYear, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/generateComputations.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/generateComputations.ts index a4d08704..d47284f2 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/generateComputations.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/generateComputations.ts @@ -1,4 +1,3 @@ -/* import * as v from "valibot" import { routePath } from "../../../../../../../../../components/index.js" import { computationSchema, computationSchemaReturn } from "../../../../../../../../../schemas/computation.js" @@ -6,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const generateComputationsRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/generate-computations`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/computations/generate`, schemas: { body: v.object({ idYear: computationSchema.entries.idYear, @@ -14,4 +14,3 @@ export const generateComputationsRouteDefinition = routeDefinition({ return: v.array(computationSchemaReturn), }, }) -*/ diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/readAllComputations.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/readAllComputations.ts index 45344a08..88abf6e7 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/readAllComputations.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/computations/readAllComputations.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const readAllComputationsRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-all-computations`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/computations`, schemas: { body: v.object({ idYear: computationSchema.entries.idYear, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/closeYear.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/closeYear.ts index 133de514..7562b951 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/closeYear.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/closeYear.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const closeYearRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/close-year`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/close`, schemas: { body: v.object({ idYear: yearSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/deleteOneYear.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/deleteOneYear.ts index 5b8ebc5e..af23220d 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/deleteOneYear.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/deleteOneYear.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const deleteOneYearRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/delete-one-year`, + method: "DELETE", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear`, schemas: { body: v.object({ idYear: yearSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/openYear.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/openYear.ts index a431f48a..dd3d4b01 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/openYear.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/openYear.ts @@ -6,7 +6,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const openYearRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/open-year`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/open`, schemas: { body: v.object({ idYear: yearSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/settleBalanceSheet.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/settleBalanceSheet.ts index 4ec1a110..b476e184 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/settleBalanceSheet.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/settleBalanceSheet.ts @@ -6,7 +6,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const settleBalanceSheetRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/settle-balance-sheet`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/settle-balance-sheet`, schemas: { body: v.object({ idYear: yearSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/settleIncomeStatement.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/settleIncomeStatement.ts index 6594c69b..f6ee3729 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/settleIncomeStatement.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/settleIncomeStatement.ts @@ -7,7 +7,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const settleIncomeStatementRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/settle-income-statement`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/settle-income-statement`, schemas: { body: v.object({ idYear: yearSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/updateOneYear.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/updateOneYear.ts index 5f62155c..fec23514 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/updateOneYear.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/general/updateOneYear.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const updateOneYearRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/update-one-year`, + method: "PATCH", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear`, schemas: { body: v.object({ idYear: yearSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/deleteOneIncomeStatement.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/deleteOneIncomeStatement.ts index 0854de2c..1370da2c 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/deleteOneIncomeStatement.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/deleteOneIncomeStatement.ts @@ -8,7 +8,8 @@ import { routeDefinition } from "../../../../../../../../../../utilities/routeDe export const deleteOneIncomeStatementRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/delete-one-income-statement`, + method: "DELETE", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/income-statements/:idIncomeStatement`, schemas: { body: v.object({ idIncomeStatement: incomeStatementSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/readOneIncomeStatement.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/readOneIncomeStatement.ts index 4be747a9..66943c1b 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/readOneIncomeStatement.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/readOneIncomeStatement.ts @@ -8,7 +8,8 @@ import { routeDefinition } from "../../../../../../../../../../utilities/routeDe export const readOneIncomeStatementRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-one-income-statement`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/income-statements/:idIncomeStatement`, schemas: { body: v.object({ idIncomeStatement: incomeStatementSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/updateOneIncomeStatement.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/updateOneIncomeStatement.ts index ee5de726..f9b06037 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/updateOneIncomeStatement.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/updateOneIncomeStatement.ts @@ -8,7 +8,8 @@ import { routeDefinition } from "../../../../../../../../../../utilities/routeDe export const updateOneIncomeStatementRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/update-one-income-statement`, + method: "PATCH", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/income-statements/:idIncomeStatement`, schemas: { body: v.object({ idIncomeStatement: incomeStatementSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/connectAccountsToIncomeStatements.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/connectAccountsToIncomeStatements.ts index 175686f5..bc2e956f 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/connectAccountsToIncomeStatements.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/connectAccountsToIncomeStatements.ts @@ -1,4 +1,3 @@ -/* import * as v from "valibot" import { routePath } from "../../../../../../../../../components/index.js" import { incomeStatementSchema } from "../../../../../../../../../schemas/incomeStatement.js" @@ -6,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const connectAccountsToIncomeStatementsRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/connect-accounts-to-income-statements`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/income-statements/connect-accounts`, schemas: { body: v.object({ idYear: incomeStatementSchema.entries.idYear, @@ -14,4 +14,3 @@ export const connectAccountsToIncomeStatementsRouteDefinition = routeDefinition( return: v.object({}), }, }) -*/ diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/createOneIncomeStatement.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/createOneIncomeStatement.ts index 46163923..01d56e01 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/createOneIncomeStatement.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/createOneIncomeStatement.ts @@ -8,7 +8,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const createOneIncomeStatementRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/create-one-income-statement`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/income-statements`, schemas: { body: v.object({ idYear: incomeStatementSchema.entries.idYear, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/generateIncomeStatements.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/generateIncomeStatements.ts index 1842fd2e..9717b26d 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/generateIncomeStatements.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/generateIncomeStatements.ts @@ -1,12 +1,15 @@ -/* import * as v from "valibot" import { routePath } from "../../../../../../../../../components/index.js" -import { incomeStatementSchema, incomeStatementSchemaReturn } from "../../../../../../../../../schemas/incomeStatement.js" +import { + incomeStatementSchema, + incomeStatementSchemaReturn, +} from "../../../../../../../../../schemas/incomeStatement.js" import { routeDefinition } from "../../../../../../../../../utilities/routeDefinition.js" export const generateIncomeStatementsRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/generate-income-statements`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/income-statements/generate`, schemas: { body: v.object({ idYear: incomeStatementSchema.entries.idYear, @@ -14,4 +17,3 @@ export const generateIncomeStatementsRouteDefinition = routeDefinition({ return: v.array(incomeStatementSchemaReturn), }, }) -*/ diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/readAllIncomeStatements.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/readAllIncomeStatements.ts index 7389c57f..81d455ba 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/readAllIncomeStatements.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/readAllIncomeStatements.ts @@ -8,7 +8,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const readAllIncomeStatementsRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-all-income-statements`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/income-statements`, schemas: { body: v.object({ idYear: incomeStatementSchema.entries.idYear, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/deleteOneJournal.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/deleteOneJournal.ts index 145051c0..d710a51a 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/deleteOneJournal.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/deleteOneJournal.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../../utilities/routeDe export const deleteOneJournalRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/delete-one-journal`, + method: "DELETE", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/journals/:idJournal`, schemas: { body: v.object({ idJournal: journalSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/readOneJournal.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/readOneJournal.ts index 09e77925..0fea76bb 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/readOneJournal.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/readOneJournal.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../../utilities/routeDe export const readOneJournalRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-one-journal`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/journals/:idJournal`, schemas: { body: v.object({ idJournal: journalSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/updateOneJournal.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/updateOneJournal.ts index c8180b62..9ea2b786 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/updateOneJournal.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/updateOneJournal.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../../utilities/routeDe export const updateOneJournalRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/update-one-journal`, + method: "PATCH", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/journals/:idJournal`, schemas: { body: v.object({ idJournal: journalSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/createOneJournal.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/createOneJournal.ts index f3b9f009..617734f5 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/createOneJournal.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/createOneJournal.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const createOneJournalRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/create-one-journal`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/journals`, schemas: { body: v.object({ idYear: journalSchema.entries.idYear, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/generateJournals.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/generateJournals.ts index 85a17f7e..01710e72 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/generateJournals.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/generateJournals.ts @@ -1,4 +1,3 @@ -/* import * as v from "valibot" import { routePath } from "../../../../../../../../../components/index.js" import { journalSchema, journalSchemaReturn } from "../../../../../../../../../schemas/journal.js" @@ -6,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const generateJournalsRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/generate-journals`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/journals/generate`, schemas: { body: v.object({ idYear: journalSchema.entries.idYear, @@ -14,4 +14,3 @@ export const generateJournalsRouteDefinition = routeDefinition({ return: v.array(journalSchemaReturn), }, }) -*/ diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/readAllJournals.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/readAllJournals.ts index fddffa20..85e36c61 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/readAllJournals.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/journals/readAllJournals.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const readAllJournalsRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-all-journals`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/journals`, schemas: { body: v.object({ idYear: journalSchema.entries.idYear, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/deleteOneTag.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/deleteOneTag.ts index 3fb0a22e..fd9393d1 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/deleteOneTag.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/deleteOneTag.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../../utilities/routeDe export const deleteOneTagRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/delete-one-tag`, + method: "DELETE", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/tags/:idTag`, schemas: { body: v.object({ idTag: tagSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/readOneTag.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/readOneTag.ts index 58440703..0154a9ba 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/readOneTag.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/readOneTag.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../../utilities/routeDe export const readOneTagRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-one-tag`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/tags/:idTag`, schemas: { body: v.object({ idTag: tagSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/updateOneTag.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/updateOneTag.ts index 395e7bb2..7b2f0d46 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/updateOneTag.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/updateOneTag.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../../utilities/routeDe export const updateOneTagRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/update-one-tag`, + method: "PATCH", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/tags/:idTag`, schemas: { body: v.object({ idTag: tagSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/createOneTag.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/createOneTag.ts index bbb873c6..a0221ef0 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/createOneTag.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/createOneTag.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const createOneTagRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/create-one-tag`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/tags`, schemas: { body: v.object({ idYear: tagSchema.entries.idYear, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/readAllTags.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/readAllTags.ts index eca7e036..744b45aa 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/readAllTags.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/$idYear/yearSettings/tags/readAllTags.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../../../../utilities/routeDefin export const readAllTagsRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-all-tags`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/years/:idYear/tags`, schemas: { body: v.object({ idYear: tagSchema.entries.idYear, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/createOneYear.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/createOneYear.ts index 60ffed67..c4ab4add 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/createOneYear.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/createOneYear.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../../../utilities/routeDefinition.js" export const createOneYearRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/create-one-year`, + method: "POST", + path: `${routePath.v1}/organizations/:idOrganization/years`, schemas: { body: v.object({ idYearPrevious: v.optional(yearSchema.entries.idYearPrevious), diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/readAllYears.ts b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/readAllYears.ts index 9c67f46c..bd73c4a2 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/readAllYears.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/$idOrganization/years/readAllYears.ts @@ -5,9 +5,12 @@ import { routeDefinition } from "../../../../../../utilities/routeDefinition.js" export const readAllYearsRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-all-years`, + method: "GET", + path: `${routePath.v1}/organizations/:idOrganization/years`, schemas: { - body: v.object({}), + body: v.object({ + idOrganization: v.optional(v.string()), + }), return: v.array(yearSchemaReturn), }, }) diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/activateOrganizationMembership.ts b/packages/metadata/src/routes/dashboard/auth/organizations/activateOrganizationMembership.ts index e1cd61c9..5926ac6e 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/activateOrganizationMembership.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/activateOrganizationMembership.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../utilities/routeDefinition.js" export const activateOrganizationMembershipRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/activate-organization-membership`, + method: "POST", + path: `${routePath.v1}/organizations/activate-membership`, schemas: { body: v.object({ idOrganizationUser: organizationUserSchema.entries.id, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/addNewOrganization.ts b/packages/metadata/src/routes/dashboard/auth/organizations/addNewOrganization.ts index 249e8b06..335ad1a1 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/addNewOrganization.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/addNewOrganization.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../utilities/routeDefinition.js" export const addNewOrganizationRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/add-new-organization`, + method: "POST", + path: `${routePath.v1}/organizations`, schemas: { body: v.object({ scope: organizationSchema.entries.scope, diff --git a/packages/metadata/src/routes/dashboard/auth/organizations/getAllMyOrganizations.ts b/packages/metadata/src/routes/dashboard/auth/organizations/getAllMyOrganizations.ts index 0f0f4315..9d59793f 100644 --- a/packages/metadata/src/routes/dashboard/auth/organizations/getAllMyOrganizations.ts +++ b/packages/metadata/src/routes/dashboard/auth/organizations/getAllMyOrganizations.ts @@ -6,7 +6,8 @@ import { routeDefinition } from "../../../../utilities/routeDefinition.js" export const getAllMyOrganizationsRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/get-all-my-organization`, + method: "GET", + path: `${routePath.v1}/organizations`, schemas: { body: v.object({}), return: v.array( diff --git a/packages/metadata/src/routes/dashboard/auth/settings/activateUser.ts b/packages/metadata/src/routes/dashboard/auth/settings/activateUser.ts index 1bdd702c..d2633771 100644 --- a/packages/metadata/src/routes/dashboard/auth/settings/activateUser.ts +++ b/packages/metadata/src/routes/dashboard/auth/settings/activateUser.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../utilities/routeDefinition.js" export const activateUserRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/activate-user`, + method: "POST", + path: `${routePath.v1}/users/me/activate`, schemas: { body: v.object({ emailToken: v.nonNullable(userSchema.entries.emailToken), diff --git a/packages/metadata/src/routes/dashboard/auth/settings/deleteUser.ts b/packages/metadata/src/routes/dashboard/auth/settings/deleteUser.ts index 552c8cbd..4276c68c 100644 --- a/packages/metadata/src/routes/dashboard/auth/settings/deleteUser.ts +++ b/packages/metadata/src/routes/dashboard/auth/settings/deleteUser.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../utilities/routeDefinition.js" export const deleteUserRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/delete-user`, + method: "DELETE", + path: `${routePath.v1}/users/me`, schemas: { body: v.object({ currentPassword: v.nonNullable(stringSchema, "Le mot de passe doit être renseigné"), diff --git a/packages/metadata/src/routes/dashboard/auth/settings/readUserSession.ts b/packages/metadata/src/routes/dashboard/auth/settings/readUserSession.ts index 73c491c2..257d29b3 100644 --- a/packages/metadata/src/routes/dashboard/auth/settings/readUserSession.ts +++ b/packages/metadata/src/routes/dashboard/auth/settings/readUserSession.ts @@ -6,7 +6,8 @@ import { routeDefinition } from "../../../../utilities/routeDefinition.js" export const readUserSessionRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-user-session`, + method: "GET", + path: `${routePath.v1}/users/me`, schemas: { body: v.object({}), return: v.object({ diff --git a/packages/metadata/src/routes/dashboard/auth/settings/resendEmailValidation.ts b/packages/metadata/src/routes/dashboard/auth/settings/resendEmailValidation.ts index feef7019..0a83a49c 100644 --- a/packages/metadata/src/routes/dashboard/auth/settings/resendEmailValidation.ts +++ b/packages/metadata/src/routes/dashboard/auth/settings/resendEmailValidation.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../utilities/routeDefinition.js" export const resendEmailValidationRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/resend-email-validation`, + method: "POST", + path: `${routePath.v1}/users/me/email/resend-validation`, schemas: { body: v.object({}), return: userSchemaReturn, diff --git a/packages/metadata/src/routes/dashboard/auth/settings/updateUser.ts b/packages/metadata/src/routes/dashboard/auth/settings/updateUser.ts index 608dd634..06822c12 100644 --- a/packages/metadata/src/routes/dashboard/auth/settings/updateUser.ts +++ b/packages/metadata/src/routes/dashboard/auth/settings/updateUser.ts @@ -5,10 +5,12 @@ import { routeDefinition } from "../../../../utilities/routeDefinition.js" export const updateUserRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/update-user`, + method: "PATCH", + path: `${routePath.v1}/users/me`, schemas: { body: v.object({ alias: v.optional(userSchema.entries.alias), + dashboardMode: v.optional(userSchema.entries.dashboardMode), }), return: userSchemaReturn, }, diff --git a/packages/metadata/src/routes/dashboard/auth/settings/updateUserEmail.ts b/packages/metadata/src/routes/dashboard/auth/settings/updateUserEmail.ts index 03da76d4..cebe85ad 100644 --- a/packages/metadata/src/routes/dashboard/auth/settings/updateUserEmail.ts +++ b/packages/metadata/src/routes/dashboard/auth/settings/updateUserEmail.ts @@ -6,7 +6,8 @@ import { routeDefinition } from "../../../../utilities/routeDefinition.js" export const updateUserEmailRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/update-user-email`, + method: "PATCH", + path: `${routePath.v1}/users/me/email`, schemas: { body: v.object({ currentPassword: v.nonNullable(stringSchema, "Le mot de passe doit être renseigné"), diff --git a/packages/metadata/src/routes/dashboard/auth/settings/updateUserPassword.ts b/packages/metadata/src/routes/dashboard/auth/settings/updateUserPassword.ts index 10ad01c1..55269d3a 100644 --- a/packages/metadata/src/routes/dashboard/auth/settings/updateUserPassword.ts +++ b/packages/metadata/src/routes/dashboard/auth/settings/updateUserPassword.ts @@ -7,7 +7,8 @@ import { routeDefinition } from "../../../../utilities/routeDefinition.js" export const updateUserPasswordRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/update-user-password`, + method: "PATCH", + path: `${routePath.v1}/users/me/password`, schemas: { body: v.object({ currentPassword: v.nonNullable(stringSchema, "Le mot de passe actuel doit être renseigné"), diff --git a/packages/metadata/src/routes/dashboard/auth/settings/validateUserEmail.ts b/packages/metadata/src/routes/dashboard/auth/settings/validateUserEmail.ts index 8959edcf..ba2c7c79 100644 --- a/packages/metadata/src/routes/dashboard/auth/settings/validateUserEmail.ts +++ b/packages/metadata/src/routes/dashboard/auth/settings/validateUserEmail.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../utilities/routeDefinition.js" export const validateUserEmailRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/validate-user-email`, + method: "POST", + path: `${routePath.v1}/users/me/email/validate`, schemas: { body: v.object({ emailToken: v.nonNullable(userSchema.entries.emailToken), diff --git a/packages/metadata/src/routes/dashboard/auth/support/createOneTicket.ts b/packages/metadata/src/routes/dashboard/auth/support/createOneTicket.ts index 7f80cc8c..9cb3cb57 100644 --- a/packages/metadata/src/routes/dashboard/auth/support/createOneTicket.ts +++ b/packages/metadata/src/routes/dashboard/auth/support/createOneTicket.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../utilities/routeDefinition.js" export const createOneTicketRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/create-one-ticket`, + method: "POST", + path: `${routePath.v1}/support/tickets`, schemas: { body: v.object({ category: v.picklist(ticketType, "Valeur invalide"), diff --git a/packages/metadata/src/routes/dashboard/auth/support/createOneTicketMessage.ts b/packages/metadata/src/routes/dashboard/auth/support/createOneTicketMessage.ts index 64b9d9cd..cbf4f272 100644 --- a/packages/metadata/src/routes/dashboard/auth/support/createOneTicketMessage.ts +++ b/packages/metadata/src/routes/dashboard/auth/support/createOneTicketMessage.ts @@ -6,7 +6,8 @@ import { routeDefinition } from "../../../../utilities/routeDefinition.js" export const createOneTicketMessageRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/create-one-ticket-message`, + method: "POST", + path: `${routePath.v1}/support/tickets/:idTicket/messages`, schemas: { body: v.object({ idTicket: v.nonNullable(idSchema, "Ce champ est requis"), diff --git a/packages/metadata/src/routes/dashboard/auth/support/readAllTicketMessages.ts b/packages/metadata/src/routes/dashboard/auth/support/readAllTicketMessages.ts index b877616c..2b326d56 100644 --- a/packages/metadata/src/routes/dashboard/auth/support/readAllTicketMessages.ts +++ b/packages/metadata/src/routes/dashboard/auth/support/readAllTicketMessages.ts @@ -6,7 +6,8 @@ import { routeDefinition } from "../../../../utilities/routeDefinition.js" export const readAllTicketMessagesRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-all-ticket-messages`, + method: "GET", + path: `${routePath.v1}/support/tickets/:idTicket/messages`, schemas: { body: v.object({ idTicket: v.nonNullable(idSchema, "Ce champ est requis"), diff --git a/packages/metadata/src/routes/dashboard/auth/support/readAllTickets.ts b/packages/metadata/src/routes/dashboard/auth/support/readAllTickets.ts index 61ee9062..a0a49f8d 100644 --- a/packages/metadata/src/routes/dashboard/auth/support/readAllTickets.ts +++ b/packages/metadata/src/routes/dashboard/auth/support/readAllTickets.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../utilities/routeDefinition.js" export const readAllTicketsRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-all-tickets`, + method: "GET", + path: `${routePath.v1}/support/tickets`, schemas: { body: v.object({}), return: v.array(ticketSchemaReturn), diff --git a/packages/metadata/src/routes/dashboard/auth/support/readOneTicket.ts b/packages/metadata/src/routes/dashboard/auth/support/readOneTicket.ts index 6c7d03e9..13cd98e3 100644 --- a/packages/metadata/src/routes/dashboard/auth/support/readOneTicket.ts +++ b/packages/metadata/src/routes/dashboard/auth/support/readOneTicket.ts @@ -6,7 +6,8 @@ import { routeDefinition } from "../../../../utilities/routeDefinition.js" export const readOneTicketRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/read-one-ticket`, + method: "GET", + path: `${routePath.v1}/support/tickets/:idTicket`, schemas: { body: v.object({ idTicket: v.nonNullable(idSchema, "Ce champ est requis"), diff --git a/packages/metadata/src/routes/dashboard/auth/support/updateOneTicket.ts b/packages/metadata/src/routes/dashboard/auth/support/updateOneTicket.ts index 853ccb11..abb9abb0 100644 --- a/packages/metadata/src/routes/dashboard/auth/support/updateOneTicket.ts +++ b/packages/metadata/src/routes/dashboard/auth/support/updateOneTicket.ts @@ -6,7 +6,8 @@ import { routeDefinition } from "../../../../utilities/routeDefinition.js" export const updateOneTicketRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/update-one-ticket`, + method: "PATCH", + path: `${routePath.v1}/support/tickets/:idTicket`, schemas: { body: v.object({ idTicket: v.nonNullable(idSchema, "Ce champ est requis"), diff --git a/packages/metadata/src/routes/dashboard/auth/support/updateOneTicketStatus.ts b/packages/metadata/src/routes/dashboard/auth/support/updateOneTicketStatus.ts index 727bf20d..0eb37279 100644 --- a/packages/metadata/src/routes/dashboard/auth/support/updateOneTicketStatus.ts +++ b/packages/metadata/src/routes/dashboard/auth/support/updateOneTicketStatus.ts @@ -6,7 +6,8 @@ import { routeDefinition } from "../../../../utilities/routeDefinition.js" export const updateOneTicketStatusRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.auth}/update-one-ticket-status`, + method: "PATCH", + path: `${routePath.v1}/support/tickets/:idTicket/status`, schemas: { body: v.object({ idTicket: v.nonNullable(idSchema, "Ce champ est requis"), diff --git a/packages/metadata/src/routes/dashboard/public/mollieWebhook.ts b/packages/metadata/src/routes/dashboard/public/mollieWebhook.ts index da004143..b1b95d59 100644 --- a/packages/metadata/src/routes/dashboard/public/mollieWebhook.ts +++ b/packages/metadata/src/routes/dashboard/public/mollieWebhook.ts @@ -4,7 +4,8 @@ import { routeDefinition } from "../../../utilities/routeDefinition.js" export const mollieWebhookRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.public}/mollie-webhook`, + method: "POST", + path: `${routePath.v1}/webhooks/mollie`, schemas: { body: v.object({ id: v.string("Ce champ est requis"), diff --git a/packages/metadata/src/routes/dashboard/public/user/resetPassword.ts b/packages/metadata/src/routes/dashboard/public/user/resetPassword.ts index 5c90d14a..4d9d4415 100644 --- a/packages/metadata/src/routes/dashboard/public/user/resetPassword.ts +++ b/packages/metadata/src/routes/dashboard/public/user/resetPassword.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../utilities/routeDefinition.js" export const resetPasswordRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.public}/reset-password`, + method: "POST", + path: `${routePath.v1}/auth/reset-password`, schemas: { body: v.object({ email: v.nonNullable(userSchema.entries.email), diff --git a/packages/metadata/src/routes/dashboard/public/user/sendMagicLink.ts b/packages/metadata/src/routes/dashboard/public/user/sendMagicLink.ts index 89d68b7e..9ad0edba 100644 --- a/packages/metadata/src/routes/dashboard/public/user/sendMagicLink.ts +++ b/packages/metadata/src/routes/dashboard/public/user/sendMagicLink.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../utilities/routeDefinition.js" export const sendMagicLinkRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.public}/send-magic-link`, + method: "POST", + path: `${routePath.v1}/auth/magic-link`, schemas: { body: v.object({ email: v.nonNullable(userSchema.entries.email), diff --git a/packages/metadata/src/routes/dashboard/public/user/signIn.ts b/packages/metadata/src/routes/dashboard/public/user/signIn.ts index b3538c95..fc6108f7 100644 --- a/packages/metadata/src/routes/dashboard/public/user/signIn.ts +++ b/packages/metadata/src/routes/dashboard/public/user/signIn.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../utilities/routeDefinition.js" export const signInRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.public}/sign-in`, + method: "POST", + path: `${routePath.v1}/auth/sign-in`, schemas: { body: v.object({ email: userSchema.entries.email, diff --git a/packages/metadata/src/routes/dashboard/public/user/signOut.ts b/packages/metadata/src/routes/dashboard/public/user/signOut.ts index f7148565..95959012 100644 --- a/packages/metadata/src/routes/dashboard/public/user/signOut.ts +++ b/packages/metadata/src/routes/dashboard/public/user/signOut.ts @@ -4,7 +4,8 @@ import { routeDefinition } from "../../../../utilities/routeDefinition.js" export const signOutRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.public}/sign-out`, + method: "POST", + path: `${routePath.v1}/auth/sign-out`, schemas: { body: v.object({}), return: v.object({}), diff --git a/packages/metadata/src/routes/dashboard/public/user/signUp.ts b/packages/metadata/src/routes/dashboard/public/user/signUp.ts index 47cc961d..87a746f0 100644 --- a/packages/metadata/src/routes/dashboard/public/user/signUp.ts +++ b/packages/metadata/src/routes/dashboard/public/user/signUp.ts @@ -5,7 +5,8 @@ import { routeDefinition } from "../../../../utilities/routeDefinition.js" export const signUpRouteDefinition = routeDefinition({ protocol: "http", - path: `${routePath.public}/sign-up`, + method: "POST", + path: `${routePath.v1}/auth/sign-up`, schemas: { body: v.object({ email: userSchema.entries.email, diff --git a/packages/metadata/src/schemas/file.ts b/packages/metadata/src/schemas/file.ts index 8f4dcc18..453b5ec1 100644 --- a/packages/metadata/src/schemas/file.ts +++ b/packages/metadata/src/schemas/file.ts @@ -7,22 +7,23 @@ import type { fileModel } from "../models/file.js" export const fileSchema = v.object({ id: v.nonNullable(idSchema, "Ce champ est requis"), idOrganization: v.nonNullable(idSchema, "Ce champ est requis"), - idYear: v.nullable(idSchema), idFolder: v.nullable(idSchema), reference: v.nullable( varcharSchema({ maxLength: 256, }), ), - name: v.nullable( + name: v.nonNullable( varcharSchema({ maxLength: 256, }), + "Ce champ est requis", ), storageKey: v.nullable(stringSchema), type: v.nullable(stringSchema), size: v.nullable(integerSchema), hash: v.nullable(stringSchema), + date: v.nullable(dateTimeSchema), createdAt: v.nonNullable(dateTimeSchema, "Ce champ est requis"), lastUpdatedAt: v.nullable(dateTimeSchema), createdBy: v.nullable(idSchema), @@ -32,7 +33,6 @@ export const fileSchema = v.object({ export const fileSchemaReturn = v.pick(fileSchema, [ "id", "idOrganization", - "idYear", "idFolder", "reference", "name", @@ -42,6 +42,7 @@ export const fileSchemaReturn = v.pick(fileSchema, [ "createdAt", "lastUpdatedAt", "hash", + "date", "createdBy", "lastUpdatedBy", ]) diff --git a/packages/metadata/src/schemas/folder.ts b/packages/metadata/src/schemas/folder.ts index b4884784..12f0a4d2 100644 --- a/packages/metadata/src/schemas/folder.ts +++ b/packages/metadata/src/schemas/folder.ts @@ -7,7 +7,6 @@ import type { folderModel } from "../models/folder.js" export const folderSchema = v.object({ id: v.nonNullable(idSchema, "Ce champ est requis"), idOrganization: v.nonNullable(idSchema, "Ce champ est requis"), - idYear: v.nonNullable(idSchema, "Ce champ est requis"), idFolderParent: v.nullable(idSchema), name: v.nonNullable( varcharSchema({ @@ -24,7 +23,6 @@ export const folderSchema = v.object({ export const folderSchemaReturn = v.pick(folderSchema, [ "id", "idOrganization", - "idYear", "idFolderParent", "name", "createdAt", diff --git a/packages/metadata/src/schemas/user.ts b/packages/metadata/src/schemas/user.ts index 8d5b5a5b..c8c11022 100644 --- a/packages/metadata/src/schemas/user.ts +++ b/packages/metadata/src/schemas/user.ts @@ -22,6 +22,13 @@ export const userSchema = v.object({ passwordSalt: v.nonNullable(stringSchema, "Ce champ est requis"), createdAt: v.nonNullable(dateTimeSchema, "Ce champ est requis"), lastUpdatedAt: v.nullable(dateTimeSchema), + dashboardMode: v.nonNullable( + v.picklist([ + "tabs", + "classic", + ]), + "Ce champ est requis", + ), }) satisfies v.GenericSchema export const userSchemaReturn = v.pick(userSchema, [ @@ -38,4 +45,5 @@ export const userSchemaReturn = v.pick(userSchema, [ "passwordSalt", "createdAt", "lastUpdatedAt", + "dashboardMode", ]) diff --git a/packages/metadata/src/utilities/routeDefinition.ts b/packages/metadata/src/utilities/routeDefinition.ts index f52e2568..05072f69 100644 --- a/packages/metadata/src/utilities/routeDefinition.ts +++ b/packages/metadata/src/utilities/routeDefinition.ts @@ -8,6 +8,11 @@ export function routeDefinition< | v.ArraySchema, undefined>, >(parameters: { protocol: "http" | "ws" + /** + * HTTP method for this route. Defaults to "POST" for backward compatibility. + * Use "GET" for read operations, "PATCH" for updates, "DELETE" for deletions. + */ + method?: "GET" | "POST" | "PATCH" | "DELETE" path: TPath schemas: { body: TSchemaBody @@ -15,6 +20,7 @@ export function routeDefinition< } }) { return { + method: (parameters.method ?? "POST") as "GET" | "POST" | "PATCH" | "DELETE", path: parameters.path, schemas: parameters.schemas, } diff --git a/packages/tools/drizzle/0022_move_storage_to_org_level.sql b/packages/tools/drizzle/0022_move_storage_to_org_level.sql new file mode 100644 index 00000000..c5afa3d3 --- /dev/null +++ b/packages/tools/drizzle/0022_move_storage_to_org_level.sql @@ -0,0 +1,17 @@ +-- Move file/folder storage from year-scoped to organization-level +-- 1. Drop id_year from table_file and table_folder +-- 2. Update deduplication index to org+hash scope + +-- Step 1: Drop id_year column from table_file +ALTER TABLE "table_file" DROP COLUMN IF EXISTS "id_year"; + +-- Step 2: Drop id_year column from table_folder +ALTER TABLE "table_folder" DROP COLUMN IF EXISTS "id_year"; + +-- Step 3: Drop old per-year deduplication index +DROP INDEX IF EXISTS "table_file_id_organization_id_year_hash_unique"; + +-- Step 4: Create new org-level deduplication index +CREATE UNIQUE INDEX IF NOT EXISTS "table_file_id_organization_hash_unique" +ON "table_file" ("id_organization", "hash") +WHERE "hash" IS NOT NULL; diff --git a/packages/tools/drizzle/0023_add_date_to_file.sql b/packages/tools/drizzle/0023_add_date_to_file.sql new file mode 100644 index 00000000..39330cf5 --- /dev/null +++ b/packages/tools/drizzle/0023_add_date_to_file.sql @@ -0,0 +1,2 @@ +-- Add optional content date to table_file (e.g. invoice date, distinct from createdAt) +ALTER TABLE "table_file" ADD COLUMN "date" TIMESTAMP WITH TIME ZONE; diff --git a/packages/tools/drizzle/0024_make_file_name_not_null.sql b/packages/tools/drizzle/0024_make_file_name_not_null.sql new file mode 100644 index 00000000..0e7aa717 --- /dev/null +++ b/packages/tools/drizzle/0024_make_file_name_not_null.sql @@ -0,0 +1,2 @@ +UPDATE "table_file" SET "name" = '' WHERE "name" IS NULL; +ALTER TABLE "table_file" ALTER COLUMN "name" SET NOT NULL; diff --git a/packages/tools/drizzle/0025_add_user_dashboard_mode.sql b/packages/tools/drizzle/0025_add_user_dashboard_mode.sql new file mode 100644 index 00000000..ba7cc659 --- /dev/null +++ b/packages/tools/drizzle/0025_add_user_dashboard_mode.sql @@ -0,0 +1 @@ +ALTER TABLE "table_user" ADD COLUMN "dashboard_mode" varchar(16) DEFAULT 'tabs' NOT NULL; diff --git a/packages/tools/src/schemas.ts b/packages/tools/src/schemas.ts index e1e2c6af..8af92de2 100644 --- a/packages/tools/src/schemas.ts +++ b/packages/tools/src/schemas.ts @@ -6,6 +6,11 @@ // file using relative paths that drizzle-kit's resolver can handle. // ============================================================================= +export { + agentMessageModel, + agentMessageRelations, + agentMessageStateEnum, +} from "../../metadata/src/models/AgentMessage.ts" export { accountBalanceSheetColumnEnum, accountBalanceSheetFlowEnum, @@ -13,11 +18,6 @@ export { accountRelations, accountTypeEnum, } from "../../metadata/src/models/account.ts" -export { - agentMessageModel, - agentMessageRelations, - agentMessageStateEnum, -} from "../../metadata/src/models/agentMessage.ts" export { agentSessionModel, agentSessionRelations } from "../../metadata/src/models/agentSession.ts" export { apiKeyModel, apiKeyRelations } from "../../metadata/src/models/apiKey.ts" export { balanceSheetModel, balanceSheetRelations } from "../../metadata/src/models/balanceSheet.ts" diff --git a/packages/tools/src/seed/seed.ts b/packages/tools/src/seed/seed.ts index 276b315f..98cac5a0 100644 --- a/packages/tools/src/seed/seed.ts +++ b/packages/tools/src/seed/seed.ts @@ -63,13 +63,21 @@ async function triggerSeededMonthlyBilling() { return } - const response = await fetch(`${apiBaseUrl}${SEED_INTERNAL_API_PATH}`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "x-internal-api-key": internalApiKey, - }, - }) + let response: Response + try { + response = await fetch(`${apiBaseUrl}${SEED_INTERNAL_API_PATH}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-internal-api-key": internalApiKey, + }, + }) + } catch (err) { + console.warn( + `Skipping seeded monthly billing generation: API unreachable at ${apiBaseUrl} (${(err as Error).message}).`, + ) + return + } if (!response.ok) { const body = await response.text() diff --git a/packages/tools/tsconfig.json b/packages/tools/tsconfig.json index 18c124d4..a664e050 100644 --- a/packages/tools/tsconfig.json +++ b/packages/tools/tsconfig.json @@ -16,10 +16,16 @@ "declarationMap": true, "sourceMap": true, "verbatimModuleSyntax": false, - "rootDir": ".", - "outDir": "./build" + "rootDir": "./src", + "outDir": "./build", + "incremental": true, + "tsBuildInfoFile": "./.tsbuildinfo" }, "exclude": ["./node_modules", "./build"], "include": ["./src"], - "references": [{ "path": "../metadata" }] + "references": [ + { + "path": "../metadata" + } + ] } diff --git a/packages/ui/package.json b/packages/ui/package.json index 42c33366..be479954 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -29,8 +29,6 @@ }, "dependencies": { "@pandacss/dev": "1.11.0", - "@radix-ui/react-popover": "1.1.15", - "@radix-ui/react-switch": "1.2.6", "@tabler/icons-react": "3.42.0", "@tanstack/react-router": "1.169.2", "@tanstack/react-virtual": "3.13.24", diff --git a/packages/ui/src/components/buttons/Button.tsx b/packages/ui/src/components/buttons/Button.tsx new file mode 100644 index 00000000..8c704d60 --- /dev/null +++ b/packages/ui/src/components/buttons/Button.tsx @@ -0,0 +1,88 @@ +import { type ComponentProps, createContext, type MouseEvent, type ReactNode, useRef, useState } from "react" +import { css, cx } from "../../utilities/cn.ts" +import { sleep } from "../../utilities/sleep.ts" + +/** + * Context for passing loading state from Button to ButtonContent + */ +export const ButtonLoadingContext = createContext(false) + +/** + * Button component - a neutral container for clickable elements + * Handles click events, loading state, and disabled state + * Use composition with ButtonContent for styled button content + * + * @example + * + */ +export function Button( + props: Omit, "children" | "disabled"> & { + hasLoader?: boolean + children: ReactNode + title?: string + isDisabled?: boolean + }, +) { + const [isLoading, setIsLoading] = useState(false) + const isLoadingRef = useRef(false) + + async function handleClick(e: MouseEvent) { + if (props.onClick === undefined) return + if (!props.hasLoader) { + props.onClick(e) + return + } + + if (isLoadingRef.current) return + isLoadingRef.current = true + setIsLoading(true) + + try { + await Promise.all([ + sleep(100), + props.onClick(e), + ]) + } finally { + isLoadingRef.current = false + setIsLoading(false) + } + } + + const { hasLoader, className, isDisabled, title, children, onClick: _onClick, ...buttonProps } = props + + return ( + + + + ) +} diff --git a/packages/ui/src/components/buttons/ButtonGhostContent.tsx b/packages/ui/src/components/buttons/ButtonGhostContent.tsx new file mode 100644 index 00000000..837a07fc --- /dev/null +++ b/packages/ui/src/components/buttons/ButtonGhostContent.tsx @@ -0,0 +1,140 @@ +import { sva } from "@arrhes/ui/css" +import { type ButtonContentProps, renderButtonContent } from "./buttonContent" +import { useButtonLoading } from "./useButtonLoading" + +const ghostRecipe = sva({ + slots: [ + "container", + "leftIcon", + "text", + "rightIcon", + ], + base: { + container: { + width: "fit-content", + display: "flex", + justifyContent: "center", + alignItems: "center", + gap: "0.5rem", + paddingInline: "0.5rem", + height: "2rem", + minWidth: "2rem", + borderRadius: "md", + boxSizing: "border-box", + transition: "all", + transitionDuration: "50ms", + transitionTimingFunction: "ease-in-out", + borderWidth: "1px", + borderStyle: "solid", + borderColor: "transparent", + backgroundColor: "transparent", + _hover: { + backgroundColor: "neutral/5", + }, + _current: { + backgroundColor: "primary/5", + }, + _disabled: { + opacity: 0.5, + }, + }, + leftIcon: { + minWidth: "14px", + width: "14px", + minHeight: "14px", + height: "14px", + flexShrink: 0, + stroke: "neutral/70", + _disabled: { + stroke: "neutral/50", + }, + _current: { + stroke: "primary", + }, + }, + text: { + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + fontSize: "0.875rem", + lineHeight: "1rem", + fontWeight: "400", + color: "neutral/75", + _disabled: { + color: "neutral/50", + }, + _current: { + color: "primary", + }, + }, + rightIcon: { + minWidth: "14px", + width: "14px", + minHeight: "14px", + height: "14px", + stroke: "neutral/50", + }, + }, + variants: { + color: { + default: { + container: {}, + }, + neutral: { + container: {}, + }, + danger: { + container: { + _hover: { + backgroundColor: "error/5", + _disabled: { + backgroundColor: "transparent", + }, + }, + }, + leftIcon: { + stroke: "error", + _disabled: { + stroke: "neutral/50", + }, + }, + text: { + color: "error", + _disabled: { + color: "neutral/50", + }, + }, + }, + success: { + container: { + _hover: { + backgroundColor: "success/8", + }, + }, + leftIcon: { + stroke: "success", + _disabled: { + stroke: "neutral/50", + }, + }, + text: { + color: "success", + _disabled: { + color: "neutral/50", + }, + }, + }, + }, + }, + defaultVariants: { + color: "default", + }, +}) + +export function ButtonGhostContent(props: ButtonContentProps) { + const classes = ghostRecipe({ + color: props.color ?? "default", + }) + const contextLoading = useButtonLoading() + return renderButtonContent(props, classes, contextLoading) +} diff --git a/packages/ui/src/components/buttons/ButtonOutlineContent.tsx b/packages/ui/src/components/buttons/ButtonOutlineContent.tsx new file mode 100644 index 00000000..b4e4b54b --- /dev/null +++ b/packages/ui/src/components/buttons/ButtonOutlineContent.tsx @@ -0,0 +1,120 @@ +import { sva } from "@arrhes/ui/css" +import { type ButtonContentProps, renderButtonContent } from "./buttonContent" +import { useButtonLoading } from "./useButtonLoading" + +const outlineRecipe = sva({ + slots: [ + "container", + "leftIcon", + "text", + "rightIcon", + ], + base: { + container: { + width: "fit-content", + display: "flex", + justifyContent: "center", + alignItems: "center", + gap: "0.5rem", + paddingInline: "0.5rem", + height: "2rem", + minWidth: "2rem", + borderRadius: "md", + boxSizing: "border-box", + transition: "all", + transitionDuration: "50ms", + transitionTimingFunction: "ease-in-out", + border: "1px solid", + borderColor: "neutral/20", + backgroundColor: "transparent", + boxShadow: "inset 0 1px 0 rgba(87, 87, 87, 0.05)", + _hover: { + backgroundColor: "primary/5", + borderColor: "primary/50", + }, + _active: { + backgroundColor: "neutral/10", + }, + _disabled: { + opacity: 0.5, + }, + }, + leftIcon: { + minWidth: "14px", + width: "14px", + minHeight: "14px", + height: "14px", + flexShrink: 0, + stroke: "neutral", + }, + text: { + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + fontSize: "0.875rem", + lineHeight: "1rem", + fontWeight: "400", + color: "neutral/75", + }, + rightIcon: { + minWidth: "14px", + width: "14px", + minHeight: "14px", + height: "14px", + stroke: "neutral/50", + }, + }, + variants: { + color: { + default: {}, + neutral: {}, + danger: { + container: { + borderColor: "error/40", + _hover: { + backgroundColor: "error/5", + borderColor: "error/50", + }, + }, + leftIcon: { + stroke: "error", + }, + text: { + color: "error", + }, + rightIcon: { + stroke: "error/50", + }, + }, + success: { + container: { + borderColor: "success/40", + _hover: { + backgroundColor: "success/5", + borderColor: "success/50", + }, + }, + leftIcon: { + stroke: "success", + }, + text: { + color: "success", + }, + rightIcon: { + stroke: "success/50", + }, + }, + }, + }, + defaultVariants: { + color: "default", + }, +}) + +export function ButtonOutlineContent(props: ButtonContentProps) { + const classes = outlineRecipe({ + color: props.color ?? "default", + }) + const contextLoading = useButtonLoading() + return renderButtonContent(props, classes, contextLoading) +} diff --git a/packages/ui/src/components/buttons/ButtonPlainContent.tsx b/packages/ui/src/components/buttons/ButtonPlainContent.tsx new file mode 100644 index 00000000..2c328162 --- /dev/null +++ b/packages/ui/src/components/buttons/ButtonPlainContent.tsx @@ -0,0 +1,100 @@ +import { sva } from "@arrhes/ui/css" +import { type ButtonContentProps, renderButtonContent } from "./buttonContent" +import { useButtonLoading } from "./useButtonLoading" + +const plainRecipe = sva({ + slots: [ + "container", + "leftIcon", + "text", + "rightIcon", + ], + base: { + container: { + width: "fit-content", + display: "flex", + justifyContent: "center", + alignItems: "center", + gap: "0.5rem", + paddingInline: "0.5rem", + height: "2rem", + minWidth: "2rem", + borderRadius: "md", + boxSizing: "border-box", + transition: "all", + transitionDuration: "50ms", + transitionTimingFunction: "ease-in-out", + border: "1px solid", + borderColor: "primary", + backgroundColor: "primary", + color: "white", + stroke: "white", + boxShadow: "inset 0 1px 0 rgba(255,255,255,0.25)", + _hover: { + backgroundColor: "primary/90", + }, + _active: { + backgroundColor: "primary/90", + }, + _disabled: { + opacity: 0.5, + cursor: "not-allowed", + backgroundColor: "primary", + }, + }, + leftIcon: { + minWidth: "14px", + width: "14px", + minHeight: "14px", + height: "14px", + flexShrink: 0, + stroke: "white", + }, + text: { + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + fontSize: "0.875rem", + lineHeight: "1rem", + fontWeight: "400", + color: "white", + }, + rightIcon: { + minWidth: "14px", + width: "14px", + minHeight: "14px", + height: "14px", + stroke: "white/50", + }, + }, + variants: { + color: { + default: {}, + neutral: {}, + danger: { + container: { + backgroundColor: "error", + borderColor: "rgba(31, 35, 40, 0.15)", + _hover: { + backgroundColor: "#c2341f", + }, + _active: { + backgroundColor: "#a22015", + }, + }, + }, + success: {}, + }, + }, + defaultVariants: { + color: "default", + }, +}) + +export function ButtonPlainContent(props: ButtonContentProps) { + const classes = plainRecipe({ + color: props.color ?? "default", + }) + const contextLoading = useButtonLoading() + return renderButtonContent(props, classes, contextLoading) +} diff --git a/packages/ui/src/components/buttons/linkButton.tsx b/packages/ui/src/components/buttons/LinkButton.tsx similarity index 100% rename from packages/ui/src/components/buttons/linkButton.tsx rename to packages/ui/src/components/buttons/LinkButton.tsx diff --git a/packages/ui/src/components/buttons/linkContent.tsx b/packages/ui/src/components/buttons/LinkContent.tsx similarity index 100% rename from packages/ui/src/components/buttons/linkContent.tsx rename to packages/ui/src/components/buttons/LinkContent.tsx diff --git a/packages/ui/src/components/buttons/button.tsx b/packages/ui/src/components/buttons/button.tsx deleted file mode 100644 index 027ee552..00000000 --- a/packages/ui/src/components/buttons/button.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { - type ComponentProps, - createContext, - type MouseEvent, - type ReactNode, - useContext, - useRef, - useState, -} from "react" -import { css, cx } from "../../utilities/cn.ts" -import { sleep } from "../../utilities/sleep.ts" - -/** - * Context for passing loading state from Button to ButtonContent - */ -const ButtonLoadingContext = createContext(false) - -/** - * Hook to access the loading state from a parent Button - * Returns false if not within a Button context - */ -export function useButtonLoading() { - return useContext(ButtonLoadingContext) -} - -/** - * Button component - a neutral container for clickable elements - * Handles click events, loading state, and disabled state - * Use composition with ButtonContent for styled button content - * - * @example - * - */ -export function Button( - props: Omit, "children" | "disabled"> & { - hasLoader?: boolean - children: ReactNode - title?: string - isDisabled?: boolean - }, -) { - const [isLoading, setIsLoading] = useState(false) - const isLoadingRef = useRef(false) - - async function handleClick(e: MouseEvent) { - if (props.onClick === undefined) return - if (!props.hasLoader) { - props.onClick(e) - return - } - - if (isLoadingRef.current) return - isLoadingRef.current = true - setIsLoading(true) - - try { - await Promise.all([ - sleep(100), - props.onClick(e), - ]) - } finally { - isLoadingRef.current = false - setIsLoading(false) - } - } - - const { hasLoader, className, isDisabled, title, children, onClick: _onClick, ...buttonProps } = props - - return ( - - - - ) -} diff --git a/packages/ui/src/components/buttons/buttonContent.tsx b/packages/ui/src/components/buttons/buttonContent.tsx index 2dbe6344..991657cd 100644 --- a/packages/ui/src/components/buttons/buttonContent.tsx +++ b/packages/ui/src/components/buttons/buttonContent.tsx @@ -1,7 +1,7 @@ import type { Icon, IconProps, ReactNode } from "@tabler/icons-react" import { cloneElement, type ReactElement } from "react" import { css, cx } from "../../utilities/cn.ts" -import { CircularLoader } from "../layouts/circularLoader" +import { CircularLoader } from "../layouts/CircularLoader" export type ButtonColor = "default" | "neutral" | "danger" | "success" diff --git a/packages/ui/src/components/buttons/buttonGhostContent.tsx b/packages/ui/src/components/buttons/buttonGhostContent.tsx deleted file mode 100644 index 4236d9b9..00000000 --- a/packages/ui/src/components/buttons/buttonGhostContent.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { sva } from "@arrhes/ui/css" -import { useButtonLoading } from "./button" -import { type ButtonContentProps, renderButtonContent } from "./buttonContent" - -const ghostRecipe = sva({ - slots: [ - "container", - "leftIcon", - "text", - "rightIcon", - ], - base: { - container: { - width: "fit-content", - display: "flex", - justifyContent: "center", - alignItems: "center", - gap: "0.5rem", - paddingInline: "0.5rem", - height: "2rem", - minWidth: "2rem", - borderRadius: "md", - boxSizing: "border-box", - transition: "all", - transitionDuration: "50ms", - transitionTimingFunction: "ease-in-out", - borderWidth: "1px", - borderStyle: "solid", - borderColor: "transparent", - backgroundColor: "transparent", - _hover: { - backgroundColor: "neutral/5", - }, - _current: { - backgroundColor: "primary/5", - }, - _disabled: { - opacity: 0.5, - }, - }, - leftIcon: { - minWidth: "14px", - width: "14px", - minHeight: "14px", - height: "14px", - flexShrink: 0, - stroke: "neutral/70", - _disabled: { - stroke: "neutral/50", - }, - _current: { - stroke: "primary", - }, - }, - text: { - overflow: "hidden", - textOverflow: "ellipsis", - whiteSpace: "nowrap", - fontSize: "0.875rem", - lineHeight: "1rem", - fontWeight: "400", - color: "neutral/75", - _disabled: { - color: "neutral/50", - }, - _current: { - color: "primary", - }, - }, - rightIcon: { - minWidth: "14px", - width: "14px", - minHeight: "14px", - height: "14px", - stroke: "neutral/50", - }, - }, - variants: { - color: { - default: { - container: {}, - }, - neutral: { - container: {}, - }, - danger: { - container: { - _hover: { - backgroundColor: "error/5", - _disabled: { - backgroundColor: "transparent", - }, - }, - }, - leftIcon: { - stroke: "error", - _disabled: { - stroke: "neutral/50", - }, - }, - text: { - color: "error", - _disabled: { - color: "neutral/50", - }, - }, - }, - success: { - container: { - _hover: { - backgroundColor: "success/8", - }, - }, - leftIcon: { - stroke: "success", - _disabled: { - stroke: "neutral/50", - }, - }, - text: { - color: "success", - _disabled: { - color: "neutral/50", - }, - }, - }, - }, - }, - defaultVariants: { - color: "default", - }, -}) - -export function ButtonGhostContent(props: ButtonContentProps) { - const classes = ghostRecipe({ - color: props.color ?? "default", - }) - const contextLoading = useButtonLoading() - return renderButtonContent(props, classes, contextLoading) -} diff --git a/packages/ui/src/components/buttons/buttonOutlineContent.tsx b/packages/ui/src/components/buttons/buttonOutlineContent.tsx deleted file mode 100644 index 5f136c39..00000000 --- a/packages/ui/src/components/buttons/buttonOutlineContent.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { sva } from "@arrhes/ui/css" -import { useButtonLoading } from "./button" -import { type ButtonContentProps, renderButtonContent } from "./buttonContent" - -const outlineRecipe = sva({ - slots: [ - "container", - "leftIcon", - "text", - "rightIcon", - ], - base: { - container: { - width: "fit-content", - display: "flex", - justifyContent: "center", - alignItems: "center", - gap: "0.5rem", - paddingInline: "0.5rem", - height: "2rem", - minWidth: "2rem", - borderRadius: "md", - boxSizing: "border-box", - transition: "all", - transitionDuration: "50ms", - transitionTimingFunction: "ease-in-out", - border: "1px solid", - borderColor: "neutral/20", - backgroundColor: "transparent", - boxShadow: "inset 0 1px 0 rgba(87, 87, 87, 0.05)", - _hover: { - backgroundColor: "primary/5", - borderColor: "primary/50", - }, - _active: { - backgroundColor: "neutral/10", - }, - _disabled: { - opacity: 0.5, - }, - }, - leftIcon: { - minWidth: "14px", - width: "14px", - minHeight: "14px", - height: "14px", - flexShrink: 0, - stroke: "neutral", - }, - text: { - overflow: "hidden", - textOverflow: "ellipsis", - whiteSpace: "nowrap", - fontSize: "0.875rem", - lineHeight: "1rem", - fontWeight: "400", - color: "neutral/75", - }, - rightIcon: { - minWidth: "14px", - width: "14px", - minHeight: "14px", - height: "14px", - stroke: "neutral/50", - }, - }, - variants: { - color: { - default: {}, - neutral: {}, - danger: { - container: { - borderColor: "error/40", - _hover: { - backgroundColor: "error/5", - borderColor: "error/50", - }, - }, - leftIcon: { - stroke: "error", - }, - text: { - color: "error", - }, - rightIcon: { - stroke: "error/50", - }, - }, - success: { - container: { - borderColor: "success/40", - _hover: { - backgroundColor: "success/5", - borderColor: "success/50", - }, - }, - leftIcon: { - stroke: "success", - }, - text: { - color: "success", - }, - rightIcon: { - stroke: "success/50", - }, - }, - }, - }, - defaultVariants: { - color: "default", - }, -}) - -export function ButtonOutlineContent(props: ButtonContentProps) { - const classes = outlineRecipe({ - color: props.color ?? "default", - }) - const contextLoading = useButtonLoading() - return renderButtonContent(props, classes, contextLoading) -} diff --git a/packages/ui/src/components/buttons/buttonPlainContent.tsx b/packages/ui/src/components/buttons/buttonPlainContent.tsx deleted file mode 100644 index 71b9da81..00000000 --- a/packages/ui/src/components/buttons/buttonPlainContent.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { sva } from "@arrhes/ui/css" -import { useButtonLoading } from "./button" -import { type ButtonContentProps, renderButtonContent } from "./buttonContent" - -const plainRecipe = sva({ - slots: [ - "container", - "leftIcon", - "text", - "rightIcon", - ], - base: { - container: { - width: "fit-content", - display: "flex", - justifyContent: "center", - alignItems: "center", - gap: "0.5rem", - paddingInline: "0.5rem", - height: "2rem", - minWidth: "2rem", - borderRadius: "md", - boxSizing: "border-box", - transition: "all", - transitionDuration: "50ms", - transitionTimingFunction: "ease-in-out", - border: "1px solid", - borderColor: "primary", - backgroundColor: "primary", - color: "white", - stroke: "white", - boxShadow: "inset 0 1px 0 rgba(255,255,255,0.25)", - _hover: { - backgroundColor: "primary/90", - }, - _active: { - backgroundColor: "primary/90", - }, - _disabled: { - opacity: 0.5, - cursor: "not-allowed", - backgroundColor: "primary", - }, - }, - leftIcon: { - minWidth: "14px", - width: "14px", - minHeight: "14px", - height: "14px", - flexShrink: 0, - stroke: "white", - }, - text: { - overflow: "hidden", - textOverflow: "ellipsis", - whiteSpace: "nowrap", - fontSize: "0.875rem", - lineHeight: "1rem", - fontWeight: "400", - color: "white", - }, - rightIcon: { - minWidth: "14px", - width: "14px", - minHeight: "14px", - height: "14px", - stroke: "white/50", - }, - }, - variants: { - color: { - default: {}, - neutral: {}, - danger: { - container: { - backgroundColor: "error", - borderColor: "rgba(31, 35, 40, 0.15)", - _hover: { - backgroundColor: "#c2341f", - }, - _active: { - backgroundColor: "#a22015", - }, - }, - }, - success: {}, - }, - }, - defaultVariants: { - color: "default", - }, -}) - -export function ButtonPlainContent(props: ButtonContentProps) { - const classes = plainRecipe({ - color: props.color ?? "default", - }) - const contextLoading = useButtonLoading() - return renderButtonContent(props, classes, contextLoading) -} diff --git a/packages/ui/src/components/buttons/useButtonLoading.tsx b/packages/ui/src/components/buttons/useButtonLoading.tsx new file mode 100644 index 00000000..3f94a6b2 --- /dev/null +++ b/packages/ui/src/components/buttons/useButtonLoading.tsx @@ -0,0 +1,10 @@ +import { useContext } from "react" +import { ButtonLoadingContext } from "./Button.js" + +/** + * Hook to access the loading state from a parent Button + * Returns false if not within a Button context + */ +export function useButtonLoading() { + return useContext(ButtonLoadingContext) +} diff --git a/packages/ui/src/components/formats/formatBase.tsx b/packages/ui/src/components/formats/FormatBase.tsx similarity index 100% rename from packages/ui/src/components/formats/formatBase.tsx rename to packages/ui/src/components/formats/FormatBase.tsx diff --git a/packages/ui/src/components/formats/FormatError.tsx b/packages/ui/src/components/formats/FormatError.tsx new file mode 100644 index 00000000..41ff039e --- /dev/null +++ b/packages/ui/src/components/formats/FormatError.tsx @@ -0,0 +1,26 @@ +import type { ComponentProps } from "react" +import { css } from "../../utilities/cn.js" +import { FormatBase } from "./FormatBase.js" + +export function FormatError(props: { text: string; className?: ComponentProps<"div">["className"] }) { + return ( + + + {props.text} + + + ) +} diff --git a/packages/ui/src/components/formats/FormatLink.tsx b/packages/ui/src/components/formats/FormatLink.tsx new file mode 100644 index 00000000..b7633d01 --- /dev/null +++ b/packages/ui/src/components/formats/FormatLink.tsx @@ -0,0 +1,53 @@ +import type { ComponentProps } from "react" +import { css } from "../../utilities/cn.js" +import { Button } from "../buttons/Button.js" +import { toast } from "../overlays/toast/useToast.js" +import { FormatBase } from "./FormatBase.js" +import { FormatNull } from "./FormatNull.js" + +type FormatLinkProps = { + text: string | null + className?: ComponentProps<"span">["className"] +} + +export function FormatLink(props: FormatLinkProps) { + if (!props.text) return + + const copyContent = (toCopy: string | null) => { + toast({ + title: "Contenu copié dans le presse-papier.", + variant: "information", + }) + return navigator.clipboard.writeText(!toCopy ? "" : toCopy) + } + + return ( + + + + ) +} diff --git a/packages/ui/src/components/formats/formatNull.tsx b/packages/ui/src/components/formats/FormatNull.tsx similarity index 100% rename from packages/ui/src/components/formats/formatNull.tsx rename to packages/ui/src/components/formats/FormatNull.tsx diff --git a/packages/ui/src/components/formats/FormatText.tsx b/packages/ui/src/components/formats/FormatText.tsx new file mode 100644 index 00000000..f61ac86d --- /dev/null +++ b/packages/ui/src/components/formats/FormatText.tsx @@ -0,0 +1,31 @@ +import type { ComponentProps, ReactNode } from "react" +import { css, cx } from "../../utilities/cn.js" +import { FormatNull } from "./FormatNull.js" + +export function FormatText(props: { + wrap?: boolean + className?: ComponentProps<"span">["className"] + children?: ReactNode +}) { + if (!props.children) return + return ( + + {props.children} + + ) +} diff --git a/packages/ui/src/components/formats/display/FormatBoolean.tsx b/packages/ui/src/components/formats/display/FormatBoolean.tsx new file mode 100644 index 00000000..ee64eb37 --- /dev/null +++ b/packages/ui/src/components/formats/display/FormatBoolean.tsx @@ -0,0 +1,18 @@ +import type { ComponentProps } from "react" +import { Chip } from "../../layouts/Chip.js" +import { FormatBase } from "../FormatBase.js" + +export function FormatBoolean(props: { + boolean?: boolean | null + text?: string + className?: ComponentProps<"div">["className"] +}) { + return ( + + + + ) +} diff --git a/packages/ui/src/components/formats/display/FormatDate.tsx b/packages/ui/src/components/formats/display/FormatDate.tsx new file mode 100644 index 00000000..fdc0cec7 --- /dev/null +++ b/packages/ui/src/components/formats/display/FormatDate.tsx @@ -0,0 +1,25 @@ +import type { ComponentProps } from "react" +import { css, cx } from "../../../utilities/cn.js" +import { FormatNull } from "../FormatNull.js" +import { FormatText } from "../FormatText.js" +import { formatDate } from "../formatDate.js" + +export function FormatDate(props: { + date?: string | Date | undefined | null + className?: ComponentProps<"div">["className"] +}) { + if (!props.date) return + if (String(new Date(props.date)) === "Invalid Date") return + return ( + + {formatDate(props.date)} + + ) +} diff --git a/packages/ui/src/components/formats/display/FormatDateTime.tsx b/packages/ui/src/components/formats/display/FormatDateTime.tsx new file mode 100644 index 00000000..048c87d9 --- /dev/null +++ b/packages/ui/src/components/formats/display/FormatDateTime.tsx @@ -0,0 +1,44 @@ +import type { ComponentProps } from "react" +import { css } from "../../../utilities/cn.js" +import { FormatNull } from "../FormatNull.js" +import { formatDateTime } from "../formatDateTime.js" + +export function FormatDateTime(props: { + date?: string | Date | undefined | null + className?: ComponentProps<"div">["className"] +}) { + const formatted = formatDateTime(props.date) + if (!formatted) return + + const [datePart, timePart] = formatted.split(" ") + + return ( +
+ + {datePart} + + + {timePart} + +
+ ) +} diff --git a/packages/ui/src/components/formats/display/FormatFileSize.tsx b/packages/ui/src/components/formats/display/FormatFileSize.tsx new file mode 100644 index 00000000..9f03af92 --- /dev/null +++ b/packages/ui/src/components/formats/display/FormatFileSize.tsx @@ -0,0 +1,27 @@ +import type { ComponentProps } from "react" +import { css } from "../../../utilities/cn.js" +import { FormatBase } from "../FormatBase.js" +import { FormatNull } from "../FormatNull.js" +import { formatFileSize } from "../formatFileSize.js" + +export function FormatFileSize(props: { size?: number | null; className?: ComponentProps<"div">["className"] }) { + if (props.size === undefined || props.size === null) return + return ( + + + {formatFileSize(props.size)} + + + ) +} diff --git a/packages/ui/src/components/formats/display/FormatPrice.tsx b/packages/ui/src/components/formats/display/FormatPrice.tsx new file mode 100644 index 00000000..4bbca95f --- /dev/null +++ b/packages/ui/src/components/formats/display/FormatPrice.tsx @@ -0,0 +1,38 @@ +import type { ComponentProps } from "react" +import { css, cx } from "../../../utilities/cn.js" +import { FormatNull } from "../FormatNull.js" +import { formatPrice } from "../formatPrice.js" + +export function FormatPrice(props: { price?: number | null | string; className?: ComponentProps<"div">["className"] }) { + if (props.price === undefined || props.price === null) { + return + } + const price = Number(props.price) + const processedPrice = Math.abs(price) < 0.009 ? 0 : price + return ( + + {processedPrice < 0 + ? `-${formatPrice({ + price: Math.abs(processedPrice), + })}` + : formatPrice({ + price: Math.abs(processedPrice), + })} + + ) +} diff --git a/packages/ui/src/components/formats/display/FormatSelect.tsx b/packages/ui/src/components/formats/display/FormatSelect.tsx new file mode 100644 index 00000000..ff8cc2cb --- /dev/null +++ b/packages/ui/src/components/formats/display/FormatSelect.tsx @@ -0,0 +1,26 @@ +import type { ComponentProps } from "react" +import { Chip, type ChipColors } from "../../layouts/Chip.js" +import { FormatBase } from "../FormatBase.js" +import { FormatNull } from "../FormatNull.js" +import { formatSelect } from "../formatSelect.js" + +export function FormatSelect(props: { + option?: string | null + options: Array<{ + key: string + label: string + }> + color?: ChipColors + className?: ComponentProps<"div">["className"] +}) { + const option = formatSelect(props.option, props.options) + if (!option) return + return ( + + + + ) +} diff --git a/packages/ui/src/components/formats/formatBoolean.tsx b/packages/ui/src/components/formats/formatBoolean.tsx index db306454..35e97a80 100644 --- a/packages/ui/src/components/formats/formatBoolean.tsx +++ b/packages/ui/src/components/formats/formatBoolean.tsx @@ -1,23 +1,4 @@ -import type { ComponentProps } from "react" -import { Chip } from "../layouts/chip.js" -import { FormatBase } from "./formatBase.js" - export function formatBoolean(boolean?: boolean | null) { if (!boolean) return "Non" return "Oui" } - -export function FormatBoolean(props: { - boolean?: boolean | null - text?: string - className?: ComponentProps<"div">["className"] -}) { - return ( - - - - ) -} diff --git a/packages/ui/src/components/formats/formatDate.tsx b/packages/ui/src/components/formats/formatDate.tsx index 58c99e7c..fa047689 100644 --- a/packages/ui/src/components/formats/formatDate.tsx +++ b/packages/ui/src/components/formats/formatDate.tsx @@ -1,8 +1,3 @@ -import type { ComponentProps } from "react" -import { css, cx } from "../../utilities/cn.js" -import { FormatNull } from "./formatNull.js" -import { FormatText } from "./formatText.js" - export function formatDate( rawDate?: string | Date | undefined | null, options?: { @@ -35,23 +30,3 @@ export function formatDate( return dateStr } - -export function FormatDate(props: { - date?: string | Date | undefined | null - className?: ComponentProps<"div">["className"] -}) { - if (!props.date) return - if (String(new Date(props.date)) === "Invalid Date") return - return ( - - {formatDate(props.date)} - - ) -} diff --git a/packages/ui/src/components/formats/formatDateTime.tsx b/packages/ui/src/components/formats/formatDateTime.tsx index 78400e1c..937b32b7 100644 --- a/packages/ui/src/components/formats/formatDateTime.tsx +++ b/packages/ui/src/components/formats/formatDateTime.tsx @@ -1,7 +1,3 @@ -import type { ComponentProps } from "react" -import { css } from "../../utilities/cn.js" -import { FormatNull } from "./formatNull.js" - export function formatDateTime(rawDate?: string | Date | undefined | null) { if (!rawDate || String(new Date(rawDate)) === "Invalid Date") return undefined @@ -26,45 +22,3 @@ export function formatDateTime(rawDate?: string | Date | undefined | null) { minute, ].join(":")}` } - -type FormatDateTime = { - date?: string | Date | undefined | null - className?: ComponentProps<"div">["className"] -} - -export function FormatDateTime(props: FormatDateTime) { - const formatted = formatDateTime(props.date) - if (!formatted) return - - const [datePart, timePart] = formatted.split(" ") - - return ( -
- - {datePart} - - - {timePart} - -
- ) -} diff --git a/packages/ui/src/components/formats/formatError.tsx b/packages/ui/src/components/formats/formatError.tsx deleted file mode 100644 index afc9a906..00000000 --- a/packages/ui/src/components/formats/formatError.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import type { ComponentProps } from "react" -import { css } from "../../utilities/cn.js" -import { FormatBase } from "./formatBase.js" - -export function FormatError(props: { text: string; className?: ComponentProps<"div">["className"] }) { - return ( - - - {props.text} - - - ) -} diff --git a/packages/ui/src/components/formats/formatFileSize.tsx b/packages/ui/src/components/formats/formatFileSize.tsx index e90c23a8..12c6a4ab 100644 --- a/packages/ui/src/components/formats/formatFileSize.tsx +++ b/packages/ui/src/components/formats/formatFileSize.tsx @@ -1,8 +1,3 @@ -import type { ComponentProps } from "react" -import { css } from "../../utilities/cn.js" -import { FormatBase } from "./formatBase.js" -import { FormatNull } from "./formatNull.js" - const sizes = [ "o", "ko", @@ -18,30 +13,3 @@ export function formatFileSize(size?: number | null) { const i = Math.floor(Math.log(size) / Math.log(1000)) return `${parseFloat((size / 1000 ** i).toFixed(2))} ${sizes.at(i)}` } - -type FormatFileSize = { - size?: number | null - className?: ComponentProps<"div">["className"] -} - -export function FormatFileSize(props: FormatFileSize) { - if (props.size === undefined || props.size === null) return - return ( - - - {formatFileSize(props.size)} - - - ) -} diff --git a/packages/ui/src/components/formats/formatLink.tsx b/packages/ui/src/components/formats/formatLink.tsx deleted file mode 100644 index 9310e82d..00000000 --- a/packages/ui/src/components/formats/formatLink.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import type { ComponentProps } from "react" -import { css } from "../../utilities/cn.js" -import { Button } from "../buttons/button.js" -import { toast } from "../overlays/toast/useToast.js" -import { FormatBase } from "./formatBase.js" -import { FormatNull } from "./formatNull.js" - -type FormatLinkProps = { - text: string | null - className?: ComponentProps<"span">["className"] -} - -export function FormatLink(props: FormatLinkProps) { - if (!props.text) return - - const copyContent = (toCopy: string | null) => { - toast({ - title: "Contenu copié dans le presse-papier.", - variant: "information", - }) - return navigator.clipboard.writeText(!toCopy ? "" : toCopy) - } - - return ( - - - - ) -} diff --git a/packages/ui/src/components/formats/formatPrice.tsx b/packages/ui/src/components/formats/formatPrice.tsx index 86f51d2a..c1d951fa 100644 --- a/packages/ui/src/components/formats/formatPrice.tsx +++ b/packages/ui/src/components/formats/formatPrice.tsx @@ -1,7 +1,3 @@ -import type { ComponentProps } from "react" -import { css, cx } from "../../utilities/cn.js" -import { FormatNull } from "./formatNull.js" - export function formatPrice(parameters: { price?: number | null | string }) { if (parameters.price === undefined || parameters.price === null) return "/" const price = Number(parameters.price) @@ -15,37 +11,3 @@ export function formatPrice(parameters: { price?: number | null | string }) { .format(processedPrice) .replace(/,/g, ".") } - -export function FormatPrice(props: { price?: number | null | string; className?: ComponentProps<"div">["className"] }) { - if (props.price === undefined || props.price === null) { - return - } - const price = Number(props.price) - const processedPrice = Math.abs(price) < 0.009 ? 0 : price - return ( - - {processedPrice < 0 - ? `-${formatPrice({ - price: Math.abs(processedPrice), - })}` - : formatPrice({ - price: Math.abs(processedPrice), - })} - - ) -} diff --git a/packages/ui/src/components/formats/formatSelect.tsx b/packages/ui/src/components/formats/formatSelect.tsx index 3fb9d505..793e5aba 100644 --- a/packages/ui/src/components/formats/formatSelect.tsx +++ b/packages/ui/src/components/formats/formatSelect.tsx @@ -1,8 +1,3 @@ -import type { ComponentProps } from "react" -import { Chip, type ChipColors } from "../layouts/chip.js" -import { FormatBase } from "./formatBase.js" -import { FormatNull } from "./formatNull.js" - export function formatSelect( key: string | null | undefined, options: { @@ -12,26 +7,3 @@ export function formatSelect( ) { return options.find((x) => x.key === key)?.label ?? "" } - -type FormatSelect = { - option?: string | null - options: Array<{ - key: string - label: string - }> - color?: ChipColors - className?: ComponentProps<"div">["className"] -} - -export function FormatSelect(props: FormatSelect) { - const option = formatSelect(props.option, props.options) - if (!option) return - return ( - - - - ) -} diff --git a/packages/ui/src/components/formats/formatText.tsx b/packages/ui/src/components/formats/formatText.tsx deleted file mode 100644 index 9561caf5..00000000 --- a/packages/ui/src/components/formats/formatText.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import type { ComponentProps, ReactNode } from "react" -import { css, cx } from "../../utilities/cn.js" -import { FormatNull } from "./formatNull.js" - -export function FormatText(props: { - wrap?: boolean - className?: ComponentProps<"span">["className"] - children?: ReactNode -}) { - if (!props.children) return - return ( - - {props.children} - - ) -} diff --git a/packages/ui/src/components/inputs/inputCheckbox.tsx b/packages/ui/src/components/inputs/InputCheckbox.tsx similarity index 100% rename from packages/ui/src/components/inputs/inputCheckbox.tsx rename to packages/ui/src/components/inputs/InputCheckbox.tsx diff --git a/packages/ui/src/components/inputs/InputCombobox.tsx b/packages/ui/src/components/inputs/InputCombobox.tsx new file mode 100644 index 00000000..6c87f429 --- /dev/null +++ b/packages/ui/src/components/inputs/InputCombobox.tsx @@ -0,0 +1,190 @@ +import { IconCheck, IconChevronDown } from "@tabler/icons-react" +import { type ComponentProps, useEffect, useState } from "react" +import type { FieldError } from "react-hook-form" +import { css, cx } from "../../utilities/cn.js" +import { debounce } from "../../utilities/debounce.js" +import { Button } from "../buttons/Button.js" +import { ButtonGhostContent } from "../buttons/ButtonGhostContent.js" +import { ButtonOutlineContent } from "../buttons/ButtonOutlineContent.js" +import { FormatNull } from "../formats/FormatNull.js" +import { CircularLoader } from "../layouts/CircularLoader.js" +import { Virtualizer } from "../layouts/Virtualizer.js" +import { Popover } from "../overlays/popover/popover.js" +import { InputText } from "./InputText.js" + +export function InputCombobox(props: { + error?: FieldError + placeholder?: string + value?: TValue | null + defaultValue?: TValue | null + onChange: (value?: TValue | null) => void + options: Array<{ + key: TValue + label: string + }> + isLoading?: boolean + isDisabled?: boolean + autoFocus?: boolean + className?: ComponentProps<"div">["className"] + allowEmpty?: boolean +}) { + const [open, setOpen] = useState(false) + const [rawQuery, setRawQuery] = useState(undefined) + const [currentOptions, setCurrentOptions] = useState(props.options) + const currentOption = props.options?.find((x) => x.key === (props.value ?? props.defaultValue)) + + useEffect(() => { + debounce({ + function: () => { + setCurrentOptions( + rawQuery === null || rawQuery === undefined || rawQuery === "" + ? props.options + : props.options.filter((x) => x.label.toLowerCase().includes(rawQuery.toLowerCase())), + ) + }, + }) + }, [ + rawQuery, + props.options, + ]) + + return ( + + + + + {open === false ? null : ( + + setRawQuery(value)} + /> +
+ {props.isLoading === true ? : null} + {currentOptions.length > 0 ? null : ( + + )} + + {(option) => { + const isSelected = currentOption?.key === option.key + return ( + + ) + }} + +
+
+ )} +
+ ) +} diff --git a/packages/ui/src/components/inputs/InputComboboxMultiple.tsx b/packages/ui/src/components/inputs/InputComboboxMultiple.tsx new file mode 100644 index 00000000..18796aa1 --- /dev/null +++ b/packages/ui/src/components/inputs/InputComboboxMultiple.tsx @@ -0,0 +1,240 @@ +import { IconChevronDown, IconX } from "@tabler/icons-react" +import { useEffect, useState } from "react" +import { css, cx } from "../../utilities/cn.js" +import { debounce } from "../../utilities/debounce.js" +import { Button } from "../buttons/Button.js" +import { ButtonGhostContent } from "../buttons/ButtonGhostContent.js" +import { ButtonOutlineContent } from "../buttons/ButtonOutlineContent.js" +import { FormatNull } from "../formats/FormatNull.js" +import { CircularLoader } from "../layouts/CircularLoader.js" +import { Virtualizer } from "../layouts/Virtualizer.js" +import { Popover } from "../overlays/popover/popover.js" +import { InputText } from "./InputText.js" + +export function InputComboboxMultiple(props: { + placeholder: string + emptyLabel?: string + options: Array<{ + key: TValue + label: string + }> + selectedOptions: Array<{ + key: TValue + label: string + }> + onChange: ( + newValues: Array<{ + key: TValue + label: string + }>, + ) => void + className?: string + autoFocus?: boolean + loading?: boolean + isDisabled?: boolean +}) { + const [open, setOpen] = useState(false) + const [rawQuery, setRawQuery] = useState(undefined) + const [currentOptions, setCurrentOptions] = useState< + Array<{ + key: TValue + label: string + }> + >([]) + + const handleUnselect = (index: number) => + props.onChange([ + ...props.selectedOptions.slice(0, index), + ...props.selectedOptions.slice(index + 1), + ]) + + const availableOptions = props.options.filter((option) => !props.selectedOptions.some((x) => x.key === option.key)) + + useEffect(() => { + debounce({ + function: () => { + setCurrentOptions( + rawQuery === null || rawQuery === undefined || rawQuery === "" + ? availableOptions + : availableOptions.filter((x) => x.label.toLowerCase().includes(rawQuery.toLowerCase())), + ) + }, + }) + }, [ + rawQuery, + availableOptions, + ]) + + return ( +
+
+ {props.selectedOptions.length === 0 ? ( + + ) : ( + props.selectedOptions.map((option, index) => ( +
+ + {option.label} + + +
+ )) + )} +
+ + + + + {!open ? null : ( + + setRawQuery(value)} + /> +
+ {props.loading === true ? : null} + {currentOptions.length > 0 ? null : ( + + )} + + {(option) => ( + + )} + +
+
+ )} +
+
+ ) +} diff --git a/packages/ui/src/components/inputs/inputCurrency.tsx b/packages/ui/src/components/inputs/InputCurrency.tsx similarity index 100% rename from packages/ui/src/components/inputs/inputCurrency.tsx rename to packages/ui/src/components/inputs/InputCurrency.tsx diff --git a/packages/ui/src/components/inputs/inputDate.tsx b/packages/ui/src/components/inputs/InputDate.tsx similarity index 100% rename from packages/ui/src/components/inputs/inputDate.tsx rename to packages/ui/src/components/inputs/InputDate.tsx diff --git a/packages/ui/src/components/inputs/inputDebounced.tsx b/packages/ui/src/components/inputs/InputDebounced.tsx similarity index 100% rename from packages/ui/src/components/inputs/inputDebounced.tsx rename to packages/ui/src/components/inputs/InputDebounced.tsx diff --git a/packages/ui/src/components/inputs/inputDisabled.tsx b/packages/ui/src/components/inputs/InputDisabled.tsx similarity index 100% rename from packages/ui/src/components/inputs/inputDisabled.tsx rename to packages/ui/src/components/inputs/InputDisabled.tsx diff --git a/packages/ui/src/components/inputs/InputFile.tsx b/packages/ui/src/components/inputs/InputFile.tsx new file mode 100644 index 00000000..c499c267 --- /dev/null +++ b/packages/ui/src/components/inputs/InputFile.tsx @@ -0,0 +1,100 @@ +import { type InputHTMLAttributes, useEffect, useRef, useState } from "react" +import type { FieldError } from "react-hook-form" +import { css } from "../../utilities/cn.js" +import { Button } from "../buttons/Button.js" + +export function InputFile( + props: Omit, "value" | "onChange"> & { + error?: FieldError + value?: File | null + onChange?: (value?: File | null | undefined) => void + type?: "image" + accept?: string + }, +) { + const inputRef = useRef(null) + const [selectedFile, setSelectedFile] = useState(props.value ?? null) + + useEffect(() => { + if (props.value === null) { + setSelectedFile(null) + return + } + + if (props.value !== undefined) { + setSelectedFile(props.value) + } + }, [ + props.value, + ]) + + return ( +
{ + event.preventDefault() + if (event.dataTransfer.files) { + const file = event.dataTransfer.files[0] + setSelectedFile(file ?? null) + props.onChange?.(file) + } + }} + onDragOver={(event) => event.preventDefault()} + > + { + if (event.target.files) { + const file = event.target.files[0] + setSelectedFile(file ?? null) + props.onChange?.(file) + } + }} + accept={props.accept ?? (!props.type ? "*" : "image/*")} + className={css({ + display: "none", + width: "100%", + height: "100%", + })} + /> + +
+ ) +} diff --git a/packages/ui/src/components/inputs/InputNumber.tsx b/packages/ui/src/components/inputs/InputNumber.tsx new file mode 100644 index 00000000..fa0fc747 --- /dev/null +++ b/packages/ui/src/components/inputs/InputNumber.tsx @@ -0,0 +1,70 @@ +import { IconMinus, IconPlus } from "@tabler/icons-react" +import { css } from "../../utilities/cn.js" +import { Button } from "../buttons/Button.js" +import { ButtonOutlineContent } from "../buttons/ButtonOutlineContent.js" + +export function InputNumber(props: { value: number; onChange: (value: number) => void; min?: number; label?: string }) { + const min = props.min ?? 0 + + return ( +
+ + { + const val = parseInt(e.target.value, 10) + props.onChange(Number.isNaN(val) || val < min ? min : val) + }} + className={css({ + width: "4rem", + height: "2rem", + textAlign: "center", + border: "1px solid token(colors.neutral/20)", + borderRadius: "md", + background: "transparent", + color: "neutral", + fontSize: "sm", + fontVariantNumeric: "tabular-nums", + outline: "none", + appearance: "textfield", + "&::-webkit-inner-spin-button": { + display: "none", + }, + "&::-webkit-outer-spin-button": { + display: "none", + }, + _focus: { + border: "1px solid token(colors.neutral/50)", + }, + })} + /> + +
+ ) +} diff --git a/packages/ui/src/components/inputs/InputPassword.tsx b/packages/ui/src/components/inputs/InputPassword.tsx new file mode 100644 index 00000000..2aadd24f --- /dev/null +++ b/packages/ui/src/components/inputs/InputPassword.tsx @@ -0,0 +1,113 @@ +import { IconEye, IconEyeClosed } from "@tabler/icons-react" +import type { InputHTMLAttributes } from "react" +import { useState } from "react" +import type { FieldError } from "react-hook-form" +import { css, cx } from "../../utilities/cn.js" +import { Button } from "../buttons/Button.js" + +export function InputPassword( + props: Omit, "value" | "onChange"> & { + error?: FieldError + value?: string | null + onChange: (value?: string | null | undefined) => void + autoFocus?: boolean + ref?: React.Ref + }, +) { + const [showPassword, setShowPassword] = useState(false) + const handleClickShowPassword = () => setShowPassword((show) => !show) + + function input(value: string | undefined | null) { + if (!value) return "" + return value + } + + function output(value: string) { + if (!value) return null + return value + } + + return ( +
+ props.onChange(output(e.currentTarget.value))} + /> + +
+ ) +} diff --git a/packages/ui/src/components/inputs/inputPrice.tsx b/packages/ui/src/components/inputs/InputPrice.tsx similarity index 100% rename from packages/ui/src/components/inputs/inputPrice.tsx rename to packages/ui/src/components/inputs/InputPrice.tsx diff --git a/packages/ui/src/components/inputs/inputRating.tsx b/packages/ui/src/components/inputs/InputRating.tsx similarity index 100% rename from packages/ui/src/components/inputs/inputRating.tsx rename to packages/ui/src/components/inputs/InputRating.tsx diff --git a/packages/ui/src/components/inputs/InputSelect.tsx b/packages/ui/src/components/inputs/InputSelect.tsx new file mode 100644 index 00000000..87127bce --- /dev/null +++ b/packages/ui/src/components/inputs/InputSelect.tsx @@ -0,0 +1,172 @@ +import { IconCheck, IconChevronDown } from "@tabler/icons-react" +import { type InputHTMLAttributes, useState } from "react" +import type { FieldError } from "react-hook-form" +import { css, cx } from "../../utilities/cn.js" +import { Button } from "../buttons/Button.js" +import { ButtonGhostContent } from "../buttons/ButtonGhostContent.js" +import { ButtonOutlineContent } from "../buttons/ButtonOutlineContent.js" +import { FormatNull } from "../formats/FormatNull.js" +import { CircularLoader } from "../layouts/CircularLoader.js" +import { Popover } from "../overlays/popover/popover.js" + +export function InputSelect( + props: Omit, "value" | "onChange"> & { + error?: FieldError + value?: TValue | null + defaultValue?: TValue | null + onChange?: (value?: TValue | null | undefined) => void + options: + | Array<{ + key: TValue + label: string + }> + | undefined + autoFocus?: boolean + allowEmpty?: boolean + isDisabled?: boolean + isLoading?: boolean + }, +) { + const [open, setOpen] = useState(false) + + function input(value: TValue | null | undefined) { + return value + } + + function output(value: TValue | undefined | null) { + if (!value) return null + return value + } + + const currentOption = props.options?.find((x) => x.key === input(props.value ?? props.defaultValue)) + return ( + + + + + +
+ {props.isLoading === true ? ( + + ) : props.options === undefined || props.options.length === 0 ? ( + + ) : ( + props.options.map((option) => { + const isSelected = currentOption?.key === option.key + return ( + + ) + }) + )} +
+
+
+ ) +} diff --git a/packages/ui/src/components/inputs/InputSwitch.tsx b/packages/ui/src/components/inputs/InputSwitch.tsx new file mode 100644 index 00000000..33511dc7 --- /dev/null +++ b/packages/ui/src/components/inputs/InputSwitch.tsx @@ -0,0 +1,70 @@ +import type { ComponentPropsWithRef } from "react" +import { css, cx } from "../../utilities/cn.js" + +type InputSwitch = { + value: boolean + onChange: (value: boolean) => void + ref?: ComponentPropsWithRef<"button">["ref"] + className?: string + autoFocus?: boolean + disabled?: boolean +} + +export function InputSwitch(props: InputSwitch) { + return ( + + ) +} diff --git a/packages/ui/src/components/inputs/inputText.tsx b/packages/ui/src/components/inputs/InputText.tsx similarity index 100% rename from packages/ui/src/components/inputs/inputText.tsx rename to packages/ui/src/components/inputs/InputText.tsx diff --git a/packages/ui/src/components/inputs/inputTextArea.tsx b/packages/ui/src/components/inputs/InputTextArea.tsx similarity index 100% rename from packages/ui/src/components/inputs/inputTextArea.tsx rename to packages/ui/src/components/inputs/InputTextArea.tsx diff --git a/packages/ui/src/components/inputs/InputToggle.tsx b/packages/ui/src/components/inputs/InputToggle.tsx new file mode 100644 index 00000000..69da2133 --- /dev/null +++ b/packages/ui/src/components/inputs/InputToggle.tsx @@ -0,0 +1,80 @@ +import type { ButtonHTMLAttributes, JSX } from "react" +import { css, cx } from "../../utilities/cn.js" +import { Button } from "../buttons/Button.js" +import { ButtonGhostContent } from "../buttons/ButtonGhostContent.js" + +export function InputToggle( + props: Omit, "value" | "onChange"> & { + value: TValue | null | undefined + onChange: (value: TValue | null | undefined) => void + options: Array<{ + icon?: JSX.Element + label?: string + value: TValue + }> + }, +) { + return ( +
+ {props.options.map((option, index) => { + const isSelected = props.value === option.value + return ( + + ) + })} +
+ ) +} diff --git a/packages/ui/src/components/inputs/inputCombobox.tsx b/packages/ui/src/components/inputs/inputCombobox.tsx deleted file mode 100644 index a21b19a1..00000000 --- a/packages/ui/src/components/inputs/inputCombobox.tsx +++ /dev/null @@ -1,191 +0,0 @@ -import { IconCheck, IconChevronDown } from "@tabler/icons-react" -import { type ComponentProps, useEffect, useState } from "react" -import type { FieldError } from "react-hook-form" -import { css, cx } from "../../utilities/cn.js" -import { debounce } from "../../utilities/debounce.js" -import { Button } from "../buttons/button.js" -import { ButtonGhostContent } from "../buttons/buttonGhostContent.js" -import { ButtonOutlineContent } from "../buttons/buttonOutlineContent.js" -import { FormatNull } from "../formats/formatNull.js" -import { CircularLoader } from "../layouts/circularLoader.js" -import { Virtualizer } from "../layouts/virtualizer.js" -import { Popover } from "../overlays/popover/popover.js" -import { InputText } from "./inputText.js" - -export function InputCombobox(props: { - error?: FieldError - placeholder?: string - value?: TValue | null - defaultValue?: TValue | null - onChange: (value?: TValue | null) => void - options: Array<{ - key: TValue - label: string - }> - isLoading?: boolean - isDisabled?: boolean - autoFocus?: boolean - className?: ComponentProps<"div">["className"] - allowEmpty?: boolean -}) { - const [open, setOpen] = useState(false) - const [rawQuery, setRawQuery] = useState(undefined) - const [currentOptions, setCurrentOptions] = useState(props.options) - const currentOption = props.options?.find((x) => x.key === (props.value ?? props.defaultValue)) - - useEffect(() => { - debounce({ - function: () => { - setCurrentOptions( - rawQuery === null || rawQuery === undefined || rawQuery === "" - ? props.options - : props.options.filter((x) => x.label.toLowerCase().includes(rawQuery.toLowerCase())), - ) - }, - }) - }, [ - rawQuery, - props.options, - ]) - - return ( - - - - - {open === false ? null : ( - - setRawQuery(value)} - /> -
- {props.isLoading === true ? : null} - {currentOptions.length > 0 ? null : ( - - )} - - {(option) => { - const isSelected = currentOption?.key === option.key - return ( - - ) - }} - -
-
- )} -
- ) -} diff --git a/packages/ui/src/components/inputs/inputComboboxMultiple.tsx b/packages/ui/src/components/inputs/inputComboboxMultiple.tsx deleted file mode 100644 index 75f575f8..00000000 --- a/packages/ui/src/components/inputs/inputComboboxMultiple.tsx +++ /dev/null @@ -1,242 +0,0 @@ -import { IconChevronDown, IconX } from "@tabler/icons-react" -import { useEffect, useState } from "react" -import { css, cx } from "../../utilities/cn.js" -import { debounce } from "../../utilities/debounce.js" -import { Button } from "../buttons/button.js" -import { ButtonGhostContent } from "../buttons/buttonGhostContent.js" -import { ButtonOutlineContent } from "../buttons/buttonOutlineContent.js" -import { FormatNull } from "../formats/formatNull.js" -import { CircularLoader } from "../layouts/circularLoader.js" -import { Virtualizer } from "../layouts/virtualizer.js" -import { Popover } from "../overlays/popover/popover.js" -import { InputText } from "./inputText.js" - -type InputComboboxMultiple = { - placeholder: string - emptyLabel?: string - options: Array<{ - key: TValue - label: string - }> - selectedOptions: Array<{ - key: TValue - label: string - }> - onChange: ( - newValues: Array<{ - key: TValue - label: string - }>, - ) => void - className?: string - autoFocus?: boolean - loading?: boolean - isDisabled?: boolean -} - -export function InputComboboxMultiple(props: InputComboboxMultiple) { - const [open, setOpen] = useState(false) - const [rawQuery, setRawQuery] = useState(undefined) - const [currentOptions, setCurrentOptions] = useState< - Array<{ - key: TValue - label: string - }> - >([]) - - const handleUnselect = (index: number) => - props.onChange([ - ...props.selectedOptions.slice(0, index), - ...props.selectedOptions.slice(index + 1), - ]) - - const availableOptions = props.options.filter((option) => !props.selectedOptions.some((x) => x.key === option.key)) - - useEffect(() => { - debounce({ - function: () => { - setCurrentOptions( - rawQuery === null || rawQuery === undefined || rawQuery === "" - ? availableOptions - : availableOptions.filter((x) => x.label.toLowerCase().includes(rawQuery.toLowerCase())), - ) - }, - }) - }, [ - rawQuery, - availableOptions, - ]) - - return ( -
-
- {props.selectedOptions.length === 0 ? ( - - ) : ( - props.selectedOptions.map((option, index) => ( -
- - {option.label} - - -
- )) - )} -
- - - - - {!open ? null : ( - - setRawQuery(value)} - /> -
- {props.loading === true ? : null} - {currentOptions.length > 0 ? null : ( - - )} - - {(option) => ( - - )} - -
-
- )} -
-
- ) -} diff --git a/packages/ui/src/components/inputs/inputFile.tsx b/packages/ui/src/components/inputs/inputFile.tsx deleted file mode 100644 index 7ef7c167..00000000 --- a/packages/ui/src/components/inputs/inputFile.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { type InputHTMLAttributes, useEffect, useRef, useState } from "react" -import type { FieldError } from "react-hook-form" -import { css } from "../../utilities/cn.js" -import { Button } from "../buttons/button.js" - -export function InputFile( - props: Omit, "value" | "onChange"> & { - error?: FieldError - value?: File | null - onChange?: (value?: File | null | undefined) => void - type?: "image" - accept?: string - }, -) { - const inputRef = useRef(null) - const [selectedFile, setSelectedFile] = useState(props.value ?? null) - - useEffect(() => { - if (props.value === null) { - setSelectedFile(null) - return - } - - if (props.value !== undefined) { - setSelectedFile(props.value) - } - }, [ - props.value, - ]) - - return ( -
{ - event.preventDefault() - if (event.dataTransfer.files) { - const file = event.dataTransfer.files[0] - setSelectedFile(file ?? null) - props.onChange?.(file) - } - }} - onDragOver={(event) => event.preventDefault()} - > - { - if (event.target.files) { - const file = event.target.files[0] - setSelectedFile(file ?? null) - props.onChange?.(file) - } - }} - accept={props.accept ?? (!props.type ? "*" : "image/*")} - className={css({ - display: "none", - width: "100%", - height: "100%", - })} - /> - -
- ) -} diff --git a/packages/ui/src/components/inputs/inputNumber.tsx b/packages/ui/src/components/inputs/inputNumber.tsx deleted file mode 100644 index 6da54289..00000000 --- a/packages/ui/src/components/inputs/inputNumber.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { IconMinus, IconPlus } from "@tabler/icons-react" -import { css } from "../../utilities/cn.js" -import { Button } from "../buttons/button.js" -import { ButtonOutlineContent } from "../buttons/buttonOutlineContent.js" - -export function InputNumber(props: { value: number; onChange: (value: number) => void; min?: number; label?: string }) { - const min = props.min ?? 0 - - return ( -
- - { - const val = parseInt(e.target.value, 10) - props.onChange(Number.isNaN(val) || val < min ? min : val) - }} - className={css({ - width: "4rem", - height: "2rem", - textAlign: "center", - border: "1px solid token(colors.neutral/20)", - borderRadius: "md", - background: "transparent", - color: "neutral", - fontSize: "sm", - fontVariantNumeric: "tabular-nums", - outline: "none", - appearance: "textfield", - "&::-webkit-inner-spin-button": { - display: "none", - }, - "&::-webkit-outer-spin-button": { - display: "none", - }, - _focus: { - border: "1px solid token(colors.neutral/50)", - }, - })} - /> - -
- ) -} diff --git a/packages/ui/src/components/inputs/inputPassword.tsx b/packages/ui/src/components/inputs/inputPassword.tsx deleted file mode 100644 index fa3decb4..00000000 --- a/packages/ui/src/components/inputs/inputPassword.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { IconEye, IconEyeClosed } from "@tabler/icons-react" -import type { InputHTMLAttributes } from "react" -import { useState } from "react" -import type { FieldError } from "react-hook-form" -import { css, cx } from "../../utilities/cn.js" -import { Button } from "../buttons/button.js" - -export function InputPassword( - props: Omit, "value" | "onChange"> & { - error?: FieldError - value?: string | null - onChange: (value?: string | null | undefined) => void - autoFocus?: boolean - ref?: React.Ref - }, -) { - const [showPassword, setShowPassword] = useState(false) - const handleClickShowPassword = () => setShowPassword((show) => !show) - - function input(value: string | undefined | null) { - if (!value) return "" - return value - } - - function output(value: string) { - if (!value) return null - return value - } - - return ( -
- props.onChange(output(e.currentTarget.value))} - /> - -
- ) -} diff --git a/packages/ui/src/components/inputs/inputSelect.tsx b/packages/ui/src/components/inputs/inputSelect.tsx deleted file mode 100644 index a5135f7d..00000000 --- a/packages/ui/src/components/inputs/inputSelect.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import { IconCheck, IconChevronDown } from "@tabler/icons-react" -import { type InputHTMLAttributes, useState } from "react" -import type { FieldError } from "react-hook-form" -import { css, cx } from "../../utilities/cn.js" -import { Button } from "../buttons/button.js" -import { ButtonGhostContent } from "../buttons/buttonGhostContent.js" -import { ButtonOutlineContent } from "../buttons/buttonOutlineContent.js" -import { FormatNull } from "../formats/formatNull.js" -import { CircularLoader } from "../layouts/circularLoader.js" -import { Popover } from "../overlays/popover/popover.js" - -export function InputSelect( - props: Omit, "value" | "onChange"> & { - error?: FieldError - value?: TValue | null - defaultValue?: TValue | null - onChange?: (value?: TValue | null | undefined) => void - options: - | Array<{ - key: TValue - label: string - }> - | undefined - autoFocus?: boolean - allowEmpty?: boolean - isDisabled?: boolean - isLoading?: boolean - }, -) { - const [open, setOpen] = useState(false) - - function input(value: TValue | null | undefined) { - return value - } - - function output(value: TValue | undefined | null) { - if (!value) return null - return value - } - - const currentOption = props.options?.find((x) => x.key === input(props.value ?? props.defaultValue)) - return ( - - - - - -
- {props.isLoading === true ? ( - - ) : props.options === undefined || props.options.length === 0 ? ( - - ) : ( - props.options.map((option) => { - const isSelected = currentOption?.key === option.key - return ( - - ) - }) - )} -
-
-
- ) -} diff --git a/packages/ui/src/components/inputs/inputSwitch.tsx b/packages/ui/src/components/inputs/inputSwitch.tsx deleted file mode 100644 index 74e35749..00000000 --- a/packages/ui/src/components/inputs/inputSwitch.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import * as SwitchPrimitives from "@radix-ui/react-switch" -import type { ComponentProps } from "react" -import { css, cx } from "../../utilities/cn.js" - -type InputSwitch = Omit, "value" | "onChange"> & { - value: boolean - onChange: (value: boolean) => void -} - -export function InputSwitch(props: InputSwitch) { - function input(value: boolean | undefined | null) { - if (!value) return false - return value - } - - function output(value: boolean) { - return value - } - - return ( - props.onChange(output(checked))} - autoFocus={props.autoFocus} - > - - - ) -} diff --git a/packages/ui/src/components/inputs/inputToggle.tsx b/packages/ui/src/components/inputs/inputToggle.tsx deleted file mode 100644 index 5331cc2c..00000000 --- a/packages/ui/src/components/inputs/inputToggle.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import type * as SwitchPrimitives from "@radix-ui/react-switch" -import type { ComponentProps, JSX } from "react" -import { css, cx } from "../../utilities/cn.js" -import { Button } from "../buttons/button.js" -import { ButtonGhostContent } from "../buttons/buttonGhostContent.js" - -export function InputToggle( - props: Omit, "value" | "onChange"> & { - value: TValue | null | undefined - onChange: (value: TValue | null | undefined) => void - options: Array<{ - icon?: JSX.Element - label?: string - value: TValue - }> - }, -) { - return ( -
- {props.options.map((option, index) => { - const isSelected = props.value === option.value - return ( - - ) - })} -
- ) -} diff --git a/packages/ui/src/components/layouts/badge.tsx b/packages/ui/src/components/layouts/Badge.tsx similarity index 100% rename from packages/ui/src/components/layouts/badge.tsx rename to packages/ui/src/components/layouts/Badge.tsx diff --git a/packages/ui/src/components/layouts/chip.tsx b/packages/ui/src/components/layouts/Chip.tsx similarity index 100% rename from packages/ui/src/components/layouts/chip.tsx rename to packages/ui/src/components/layouts/Chip.tsx diff --git a/packages/ui/src/components/layouts/circularLoader.tsx b/packages/ui/src/components/layouts/CircularLoader.tsx similarity index 100% rename from packages/ui/src/components/layouts/circularLoader.tsx rename to packages/ui/src/components/layouts/CircularLoader.tsx diff --git a/packages/ui/src/components/layouts/Kbd.tsx b/packages/ui/src/components/layouts/Kbd.tsx new file mode 100644 index 00000000..b70eeecc --- /dev/null +++ b/packages/ui/src/components/layouts/Kbd.tsx @@ -0,0 +1,24 @@ +import type { ComponentProps } from "react" +import { css } from "../../utilities/cn.ts" + +export function Kbd(props: { children: ComponentProps<"kbd">["children"] }) { + return ( + + {props.children} + + ) +} diff --git a/packages/ui/src/components/layouts/logo.tsx b/packages/ui/src/components/layouts/Logo.tsx similarity index 100% rename from packages/ui/src/components/layouts/logo.tsx rename to packages/ui/src/components/layouts/Logo.tsx diff --git a/packages/ui/src/components/layouts/PageNavigation.tsx b/packages/ui/src/components/layouts/PageNavigation.tsx new file mode 100644 index 00000000..535a43d3 --- /dev/null +++ b/packages/ui/src/components/layouts/PageNavigation.tsx @@ -0,0 +1,75 @@ +import { useMatches, useRouterState } from "@tanstack/react-router" +import type { JSX } from "react" +import { css } from "../../utilities/cn.js" +import { ButtonGhostContent } from "../buttons/ButtonGhostContent.js" +import { LinkButton } from "../buttons/LinkButton.js" + +export function PageNavigation(props: { + tabs: + | Array<{ + label: string + icon: JSX.Element + to: any + params?: any + }> + | undefined +}) { + const routeMatches = useMatches() + const currentPath = useRouterState({ + select: (state) => state.matches.at(-1)?.routeId, + }) as unknown as string | undefined + + return ( +
+ {props.tabs === undefined ? null : ( +
+ {props.tabs.map((tab) => { + const matchRoute = routeMatches.find((match) => match.fullPath === tab.to) + const isActive = + matchRoute === undefined || !currentPath ? false : currentPath.includes(matchRoute.routeId) + + return ( + + + + ) + })} +
+ )} +
+ ) +} diff --git a/packages/ui/src/components/layouts/separator.tsx b/packages/ui/src/components/layouts/Separator.tsx similarity index 100% rename from packages/ui/src/components/layouts/separator.tsx rename to packages/ui/src/components/layouts/Separator.tsx diff --git a/packages/ui/src/components/layouts/virtualizer.tsx b/packages/ui/src/components/layouts/Virtualizer.tsx similarity index 100% rename from packages/ui/src/components/layouts/virtualizer.tsx rename to packages/ui/src/components/layouts/Virtualizer.tsx diff --git a/packages/ui/src/components/layouts/pageNavigation.tsx b/packages/ui/src/components/layouts/pageNavigation.tsx deleted file mode 100644 index bdb013ca..00000000 --- a/packages/ui/src/components/layouts/pageNavigation.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { useMatches, useRouterState } from "@tanstack/react-router" -import type { JSX } from "react" -import { css } from "../../utilities/cn.js" -import { ButtonGhostContent } from "../buttons/buttonGhostContent.js" -import { LinkButton } from "../buttons/linkButton.js" - -export function PageNavigation(props: { - tabs: - | Array<{ - label: string - icon: JSX.Element - to: any - params?: any - }> - | undefined -}) { - const routeMatches = useMatches() - const currentPath = useRouterState({ - select: (state) => state.matches.at(-1)?.routeId, - }) as unknown as string | undefined - - return ( -
- {props.tabs === undefined ? null : ( -
- {props.tabs.map((tab) => { - const matchRoute = routeMatches.find((match) => match.fullPath === tab.to) - const isActive = - matchRoute === undefined || !currentPath ? false : currentPath.includes(matchRoute.routeId) - - return ( - - - - ) - })} -
- )} -
- ) -} diff --git a/packages/ui/src/components/overlays/dialog/DialogBody.tsx b/packages/ui/src/components/overlays/dialog/DialogBody.tsx new file mode 100644 index 00000000..0ac8d473 --- /dev/null +++ b/packages/ui/src/components/overlays/dialog/DialogBody.tsx @@ -0,0 +1,22 @@ +import type { HTMLAttributes } from "react" +import { css, cx } from "../../../utilities/cn.js" + +export function DialogBody(props: HTMLAttributes) { + return ( +
+ {props.children} +
+ ) +} diff --git a/packages/ui/src/components/overlays/dialog/DialogContent.tsx b/packages/ui/src/components/overlays/dialog/DialogContent.tsx new file mode 100644 index 00000000..8ebffea9 --- /dev/null +++ b/packages/ui/src/components/overlays/dialog/DialogContent.tsx @@ -0,0 +1,33 @@ +import type { HTMLAttributes } from "react" +import { css, cx } from "../../../utilities/cn.js" + +export function DialogContent(props: HTMLAttributes) { + return ( +
+ {props.children} +
+ ) +} diff --git a/packages/ui/src/components/overlays/dialog/DialogDescription.tsx b/packages/ui/src/components/overlays/dialog/DialogDescription.tsx new file mode 100644 index 00000000..cab5831e --- /dev/null +++ b/packages/ui/src/components/overlays/dialog/DialogDescription.tsx @@ -0,0 +1,17 @@ +import type { HTMLAttributes } from "react" +import { css, cx } from "../../../utilities/cn.js" + +export function DialogDescription(props: HTMLAttributes) { + return ( +

+ ) +} diff --git a/packages/ui/src/components/overlays/dialog/DialogFooter.tsx b/packages/ui/src/components/overlays/dialog/DialogFooter.tsx new file mode 100644 index 00000000..959c534b --- /dev/null +++ b/packages/ui/src/components/overlays/dialog/DialogFooter.tsx @@ -0,0 +1,23 @@ +import type { HTMLAttributes } from "react" +import { css, cx } from "../../../utilities/cn.js" + +export function DialogFooter(props: HTMLAttributes) { + return ( +

+ ) +} diff --git a/packages/ui/src/components/overlays/dialog/DialogHeader.tsx b/packages/ui/src/components/overlays/dialog/DialogHeader.tsx new file mode 100644 index 00000000..2b455a46 --- /dev/null +++ b/packages/ui/src/components/overlays/dialog/DialogHeader.tsx @@ -0,0 +1,51 @@ +import { IconX } from "@tabler/icons-react" +import type { HTMLAttributes } from "react" +import { useModalItem } from "../../../stores/modalStore.js" +import { css, cx } from "../../../utilities/cn.js" +import { ButtonGhostContent } from "../../buttons/ButtonGhostContent.js" + +export function DialogHeader(props: HTMLAttributes) { + const modalItem = useModalItem() + + return ( +
+
+ {props.children} +
+ {modalItem !== null && ( + + )} +
+ ) +} diff --git a/packages/ui/src/components/overlays/dialog/DialogTitle.tsx b/packages/ui/src/components/overlays/dialog/DialogTitle.tsx new file mode 100644 index 00000000..7f0af8bc --- /dev/null +++ b/packages/ui/src/components/overlays/dialog/DialogTitle.tsx @@ -0,0 +1,17 @@ +import type { HTMLAttributes } from "react" +import { css, cx } from "../../../utilities/cn.js" + +export function DialogTitle(props: HTMLAttributes) { + return ( +

+ ) +} diff --git a/packages/ui/src/components/overlays/dialog/dialog.tsx b/packages/ui/src/components/overlays/dialog/dialog.tsx new file mode 100644 index 00000000..132ff7a1 --- /dev/null +++ b/packages/ui/src/components/overlays/dialog/dialog.tsx @@ -0,0 +1,15 @@ +import { DialogBody } from "./DialogBody.js" +import { DialogContent } from "./DialogContent.js" +import { DialogDescription } from "./DialogDescription.js" +import { DialogFooter } from "./DialogFooter.js" +import { DialogHeader } from "./DialogHeader.js" +import { DialogTitle } from "./DialogTitle.js" + +export const Dialog = { + Body: DialogBody, + Content: DialogContent, + Description: DialogDescription, + Footer: DialogFooter, + Header: DialogHeader, + Title: DialogTitle, +} diff --git a/packages/ui/src/components/overlays/popover/PopoverContent.tsx b/packages/ui/src/components/overlays/popover/PopoverContent.tsx new file mode 100644 index 00000000..55b19152 --- /dev/null +++ b/packages/ui/src/components/overlays/popover/PopoverContent.tsx @@ -0,0 +1,131 @@ +import type { ReactNode } from "react" +import { createPortal } from "react-dom" +import { css, cx } from "../../../utilities/cn.js" +import { usePopoverContext } from "./popoverRoot.js" + +// --------------------------------------------------------------------------- +// CSS anchor positioning rules — injected once by PopoverContent renders. +// Uses data attributes so we cover all side/align combos with a stylesheet. +// --------------------------------------------------------------------------- + +const ANCHOR_CSS = ` +[data-popover-content][data-side="bottom"][data-align="start"] { + position: fixed; position-anchor: var(--pos-anchor); + top: calc(anchor(bottom) + var(--pos-side-offset, 4px)); + left: calc(anchor(left) + var(--pos-align-offset, 0px)); +} +[data-popover-content][data-side="bottom"][data-align="center"] { + position: fixed; position-anchor: var(--pos-anchor); + top: calc(anchor(bottom) + var(--pos-side-offset, 4px)); + left: anchor(center); + translate: -50% 0; +} +[data-popover-content][data-side="bottom"][data-align="end"] { + position: fixed; position-anchor: var(--pos-anchor); + top: calc(anchor(bottom) + var(--pos-side-offset, 4px)); + right: calc(anchor(right) + var(--pos-align-offset, 0px)); +} +[data-popover-content][data-side="top"][data-align="start"] { + position: fixed; position-anchor: var(--pos-anchor); + bottom: calc(anchor(top) + var(--pos-side-offset, 4px)); + left: calc(anchor(left) + var(--pos-align-offset, 0px)); +} +[data-popover-content][data-side="top"][data-align="center"] { + position: fixed; position-anchor: var(--pos-anchor); + bottom: calc(anchor(top) + var(--pos-side-offset, 4px)); + left: anchor(center); + translate: -50% 0; +} +[data-popover-content][data-side="top"][data-align="end"] { + position: fixed; position-anchor: var(--pos-anchor); + bottom: calc(anchor(top) + var(--pos-side-offset, 4px)); + right: calc(anchor(right) + var(--pos-align-offset, 0px)); +} +[data-popover-content][data-side="right"][data-align="start"] { + position: fixed; position-anchor: var(--pos-anchor); + left: calc(anchor(right) + var(--pos-side-offset, 4px)); + top: calc(anchor(top) + var(--pos-align-offset, 0px)); +} +[data-popover-content][data-side="right"][data-align="center"] { + position: fixed; position-anchor: var(--pos-anchor); + left: calc(anchor(right) + var(--pos-side-offset, 4px)); + top: anchor(center); + translate: 0 -50%; +} +[data-popover-content][data-side="right"][data-align="end"] { + position: fixed; position-anchor: var(--pos-anchor); + left: calc(anchor(right) + var(--pos-side-offset, 4px)); + bottom: calc(anchor(bottom) + var(--pos-align-offset, 0px)); +} +[data-popover-content][data-side="left"][data-align="start"] { + position: fixed; position-anchor: var(--pos-anchor); + right: calc(anchor(left) + var(--pos-side-offset, 4px)); + top: calc(anchor(top) + var(--pos-align-offset, 0px)); +} +[data-popover-content][data-side="left"][data-align="center"] { + position: fixed; position-anchor: var(--pos-anchor); + right: calc(anchor(left) + var(--pos-side-offset, 4px)); + top: anchor(center); + translate: 0 -50%; +} +[data-popover-content][data-side="left"][data-align="end"] { + position: fixed; position-anchor: var(--pos-anchor); + right: calc(anchor(left) + var(--pos-side-offset, 4px)); + bottom: calc(anchor(bottom) + var(--pos-align-offset, 0px)); +} +` + +export function PopoverContent(props: { + children?: ReactNode + side?: "top" | "bottom" | "left" | "right" + align?: "start" | "center" | "end" + sideOffset?: number + alignOffset?: number + className?: string +}) { + const { isOpen, anchorName } = usePopoverContext() + const side = props.side ?? "bottom" + const align = props.align ?? "start" + const sideOffset = props.sideOffset ?? 4 + const alignOffset = props.alignOffset ?? 0 + + if (!isOpen || typeof document === "undefined") return null + + return createPortal( + <> + +
+ {props.children} +
+ , + document.body, + ) +} diff --git a/packages/ui/src/components/overlays/popover/popover.tsx b/packages/ui/src/components/overlays/popover/popover.tsx index 7135b96e..2da98283 100644 --- a/packages/ui/src/components/overlays/popover/popover.tsx +++ b/packages/ui/src/components/overlays/popover/popover.tsx @@ -1,5 +1,5 @@ +import { PopoverContent } from "./PopoverContent.js" import { PopoverClose } from "./popoverClose.js" -import { PopoverContent } from "./popoverContent.js" import { PopoverRoot } from "./popoverRoot.js" import { PopoverTrigger } from "./popoverTrigger.js" diff --git a/packages/ui/src/components/overlays/popover/popoverClose.tsx b/packages/ui/src/components/overlays/popover/popoverClose.tsx index a945fec4..58aab1f1 100644 --- a/packages/ui/src/components/overlays/popover/popoverClose.tsx +++ b/packages/ui/src/components/overlays/popover/popoverClose.tsx @@ -1,3 +1,32 @@ -import * as PopoverPrimitive from "@radix-ui/react-popover" +import { cloneElement, isValidElement, type ReactElement } from "react" +import { usePopoverContext } from "./popoverRoot.js" -export const PopoverClose = PopoverPrimitive.Close +export function PopoverClose(props: { children: ReactElement; asChild?: boolean }) { + const { setOpen } = usePopoverContext() + + function handleClick() { + setOpen(false) + } + + if (props.asChild && isValidElement(props.children)) { + const child = props.children as ReactElement> + return cloneElement(child, { + ...child.props, + onClick: (e: React.MouseEvent) => { + if (typeof child.props.onClick === "function") { + child.props.onClick(e) + } + handleClick() + }, + }) + } + + return ( + + ) +} diff --git a/packages/ui/src/components/overlays/popover/popoverContent.tsx b/packages/ui/src/components/overlays/popover/popoverContent.tsx deleted file mode 100644 index 24b444f6..00000000 --- a/packages/ui/src/components/overlays/popover/popoverContent.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import * as PopoverPrimitive from "@radix-ui/react-popover" -import type { ComponentProps } from "react" -import { css, cx } from "../../../utilities/cn.js" - -export function PopoverContent(props: ComponentProps) { - return ( - - - - ) -} diff --git a/packages/ui/src/components/overlays/popover/popoverRoot.tsx b/packages/ui/src/components/overlays/popover/popoverRoot.tsx index 1320adea..24630e32 100644 --- a/packages/ui/src/components/overlays/popover/popoverRoot.tsx +++ b/packages/ui/src/components/overlays/popover/popoverRoot.tsx @@ -1,3 +1,93 @@ -import * as PopoverPrimitive from "@radix-ui/react-popover" +import { + createContext, + type Dispatch, + type ReactNode, + type SetStateAction, + useCallback, + useContext, + useEffect, + useId, + useState, +} from "react" +import { usePopoverStore } from "../../../stores/popoverStore.js" -export const PopoverRoot = PopoverPrimitive.Root +// --------------------------------------------------------------------------- +// Context shared between Root / Trigger / Content / Close +// --------------------------------------------------------------------------- + +export type PopoverContextValue = { + id: string + anchorName: string + isOpen: boolean + setOpen: Dispatch> +} + +export const PopoverContext = createContext(null) + +export function usePopoverContext(): PopoverContextValue { + const ctx = useContext(PopoverContext) + if (ctx === null) throw new Error("Popover sub-component used outside ") + return ctx +} + +// --------------------------------------------------------------------------- +// PopoverRoot +// --------------------------------------------------------------------------- + +export function PopoverRoot(props: { + children: ReactNode + /** Optionally override the auto-generated id (must be a valid CSS ident fragment). */ + id?: string + /** Controlled open state. */ + open?: boolean + /** Controlled open-change callback. */ + onOpenChange?: (open: boolean) => void +}) { + const reactId = useId() + const id = props.id ?? reactId.replace(/:/g, "") + const anchorName = `--popover-${id}` + + const isControlled = props.open !== undefined + const [localOpen, setLocalOpen] = useState(false) + + const isOpen = isControlled ? (props.open ?? false) : localOpen + + const setOpen: Dispatch> = useCallback( + (valueOrUpdater) => { + const next = typeof valueOrUpdater === "function" ? valueOrUpdater(isOpen) : valueOrUpdater + if (!isControlled) setLocalOpen(next) + props.onOpenChange?.(next) + }, + [ + isControlled, + isOpen, + props.onOpenChange, + ], + ) + + const store = usePopoverStore() + + useEffect(() => { + store.register(id, setOpen) + return () => store.unregister(id) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + id, + store.unregister, + store.register, + setOpen, + ]) + + return ( + + {props.children} + + ) +} diff --git a/packages/ui/src/components/overlays/popover/popoverTrigger.tsx b/packages/ui/src/components/overlays/popover/popoverTrigger.tsx index d421191f..2481cd81 100644 --- a/packages/ui/src/components/overlays/popover/popoverTrigger.tsx +++ b/packages/ui/src/components/overlays/popover/popoverTrigger.tsx @@ -1,3 +1,48 @@ -import * as PopoverPrimitive from "@radix-ui/react-popover" +import { cloneElement, isValidElement, type ReactElement } from "react" +import { usePopoverContext } from "./popoverRoot.js" -export const PopoverTrigger = PopoverPrimitive.Trigger +export function PopoverTrigger(props: { + children: ReactElement + /** When true the child element is used directly as the trigger (no wrapper button). */ + asChild?: boolean +}) { + const { anchorName, isOpen, setOpen } = usePopoverContext() + + function handleClick(e: React.MouseEvent) { + e.stopPropagation() + setOpen(!isOpen) + } + + if (props.asChild && isValidElement(props.children)) { + const child = props.children as ReactElement> + return cloneElement(child, { + ...child.props, + "data-popover-trigger": "", + style: { + ...(child.props.style as React.CSSProperties | undefined), + anchorName, + }, + onClick: (e: React.MouseEvent) => { + if (typeof child.props.onClick === "function") { + child.props.onClick(e) + } + handleClick(e) + }, + }) + } + + return ( + + ) +} diff --git a/packages/ui/src/components/overlays/toast/ToastItem.tsx b/packages/ui/src/components/overlays/toast/ToastItem.tsx new file mode 100644 index 00000000..9f43aa63 --- /dev/null +++ b/packages/ui/src/components/overlays/toast/ToastItem.tsx @@ -0,0 +1,122 @@ +import { + type Icon, + IconAlertTriangle, + IconCircleCheck, + IconInfoSquare, + type IconProps, + IconX, +} from "@tabler/icons-react" +import type { ReactElement } from "react" +import { css } from "../../../utilities/cn.js" +import { Button } from "../../buttons/Button.js" +import { ButtonGhostContent } from "../../buttons/ButtonGhostContent.js" +import type { ToasterToast, ToastVariant } from "./useToast.js" + +const toastIcons: Record>> = { + error: ( + + ), + success: ( + + ), + warning: ( + + ), + information: ( + + ), +} + +export function ToastItem(props: { toast: ToasterToast; onDismiss: (id: string) => void }) { + const variant = props.toast.variant ?? "information" + + return ( +
+
+ {toastIcons[variant]} +
+
+ {props.toast.title === undefined ? null : ( + + {props.toast.title} + + )} + {props.toast.description === undefined ? null : ( + + {props.toast.description} + + )} +
+ +
+ ) +} diff --git a/packages/ui/src/components/overlays/toast/toastViewport.tsx b/packages/ui/src/components/overlays/toast/ToastViewport.tsx similarity index 100% rename from packages/ui/src/components/overlays/toast/toastViewport.tsx rename to packages/ui/src/components/overlays/toast/ToastViewport.tsx diff --git a/packages/ui/src/components/overlays/toast/toastProvider.tsx b/packages/ui/src/components/overlays/toast/ToasterProvider.tsx similarity index 100% rename from packages/ui/src/components/overlays/toast/toastProvider.tsx rename to packages/ui/src/components/overlays/toast/ToasterProvider.tsx diff --git a/packages/ui/src/components/overlays/toast/toast.tsx b/packages/ui/src/components/overlays/toast/toast.tsx index ceaeb818..453154b8 100644 --- a/packages/ui/src/components/overlays/toast/toast.tsx +++ b/packages/ui/src/components/overlays/toast/toast.tsx @@ -1,5 +1,5 @@ -import { ToastItem } from "./toastItem.js" -import { ToastViewport } from "./toastViewport.js" +import { ToastItem } from "./ToastItem.js" +import { ToastViewport } from "./ToastViewport.js" export const Toast = { Item: ToastItem, diff --git a/packages/ui/src/components/overlays/toast/toastItem.tsx b/packages/ui/src/components/overlays/toast/toastItem.tsx deleted file mode 100644 index 22e9b65c..00000000 --- a/packages/ui/src/components/overlays/toast/toastItem.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { - type Icon, - IconAlertTriangle, - IconCircleCheck, - IconInfoSquare, - type IconProps, - IconX, -} from "@tabler/icons-react" -import type { ReactElement } from "react" -import { css } from "../../../utilities/cn.js" -import { Button } from "../../buttons/button.js" -import { ButtonGhostContent } from "../../buttons/buttonGhostContent.js" -import type { ToasterToast, ToastVariant } from "./useToast.js" - -const toastIcons: Record>> = { - error: ( - - ), - success: ( - - ), - warning: ( - - ), - information: ( - - ), -} - -export function ToastItem(props: { toast: ToasterToast; onDismiss: (id: string) => void }) { - const variant = props.toast.variant ?? "information" - - return ( -
-
- {toastIcons[variant]} -
-
- {props.toast.title === undefined ? null : ( - - {props.toast.title} - - )} - {props.toast.description === undefined ? null : ( - - {props.toast.description} - - )} -
- -
- ) -} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index efbb41dd..25b053e8 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -1,59 +1,73 @@ // Utilities // Components - Buttons -export { Button, useButtonLoading } from "./components/buttons/button.js" +export { Button } from "./components/buttons/Button.js" +export { ButtonGhostContent } from "./components/buttons/ButtonGhostContent.js" +export { ButtonOutlineContent } from "./components/buttons/ButtonOutlineContent.js" +export { ButtonPlainContent } from "./components/buttons/ButtonPlainContent.js" export type { ButtonColor, ButtonContentProps } from "./components/buttons/buttonContent.js" export { renderButtonContent } from "./components/buttons/buttonContent.js" -export { ButtonGhostContent } from "./components/buttons/buttonGhostContent.js" -export { ButtonOutlineContent } from "./components/buttons/buttonOutlineContent.js" -export { ButtonPlainContent } from "./components/buttons/buttonPlainContent.js" -export { LinkButton } from "./components/buttons/linkButton.js" -export type { LinkContentProps } from "./components/buttons/linkContent.js" -export { LinkContent } from "./components/buttons/linkContent.js" +export { LinkButton } from "./components/buttons/LinkButton.js" +export type { LinkContentProps } from "./components/buttons/LinkContent.js" +export { LinkContent } from "./components/buttons/LinkContent.js" +export { useButtonLoading } from "./components/buttons/useButtonLoading.js" +export { FormatBoolean } from "./components/formats/display/FormatBoolean.js" +export { FormatDate } from "./components/formats/display/FormatDate.js" +export { FormatDateTime } from "./components/formats/display/FormatDateTime.js" +export { FormatFileSize } from "./components/formats/display/FormatFileSize.js" +export { FormatPrice } from "./components/formats/display/FormatPrice.js" +export { FormatSelect } from "./components/formats/display/FormatSelect.js" // Components - Formats -export { FormatBase } from "./components/formats/formatBase.js" -export { FormatBoolean, formatBoolean } from "./components/formats/formatBoolean.js" -export { FormatDate, formatDate } from "./components/formats/formatDate.js" -export { FormatDateTime, formatDateTime } from "./components/formats/formatDateTime.js" -export { FormatError } from "./components/formats/formatError.js" -export { FormatFileSize, formatFileSize } from "./components/formats/formatFileSize.js" -export { FormatLink } from "./components/formats/formatLink.js" -export { FormatNull } from "./components/formats/formatNull.js" -export { FormatPrice, formatPrice } from "./components/formats/formatPrice.js" -export { FormatSelect, formatSelect } from "./components/formats/formatSelect.js" -export { FormatText } from "./components/formats/formatText.js" -export { InputCheckbox } from "./components/inputs/inputCheckbox.js" -export { InputCombobox } from "./components/inputs/inputCombobox.js" -export { InputComboboxMultiple } from "./components/inputs/inputComboboxMultiple.js" -export { InputCurrency } from "./components/inputs/inputCurrency.js" -export { InputDate } from "./components/inputs/inputDate.js" -export { InputDebounced } from "./components/inputs/inputDebounced.js" -export { InputDisabled } from "./components/inputs/inputDisabled.js" -export { InputFile } from "./components/inputs/inputFile.js" -export { InputNumber } from "./components/inputs/inputNumber.js" -export { InputPassword } from "./components/inputs/inputPassword.js" -export { InputPrice } from "./components/inputs/inputPrice.js" -export { InputRating } from "./components/inputs/inputRating.js" -export { InputSelect } from "./components/inputs/inputSelect.js" -export { InputSwitch } from "./components/inputs/inputSwitch.js" +export { FormatBase } from "./components/formats/FormatBase.js" +export { FormatError } from "./components/formats/FormatError.js" +export { FormatLink } from "./components/formats/FormatLink.js" +export { FormatNull } from "./components/formats/FormatNull.js" +export { FormatText } from "./components/formats/FormatText.js" +export { formatBoolean } from "./components/formats/formatBoolean.js" +export { formatDate } from "./components/formats/formatDate.js" +export { formatDateTime } from "./components/formats/formatDateTime.js" +export { formatFileSize } from "./components/formats/formatFileSize.js" +export { formatPrice } from "./components/formats/formatPrice.js" +export { formatSelect } from "./components/formats/formatSelect.js" +export { InputCheckbox } from "./components/inputs/InputCheckbox.js" +export { InputCombobox } from "./components/inputs/InputCombobox.js" +export { InputComboboxMultiple } from "./components/inputs/InputComboboxMultiple.js" +export { InputCurrency } from "./components/inputs/InputCurrency.js" +export { InputDate } from "./components/inputs/InputDate.js" +export { InputDebounced } from "./components/inputs/InputDebounced.js" +export { InputDisabled } from "./components/inputs/InputDisabled.js" +export { InputFile } from "./components/inputs/InputFile.js" +export { InputNumber } from "./components/inputs/InputNumber.js" +export { InputPassword } from "./components/inputs/InputPassword.js" +export { InputPrice } from "./components/inputs/InputPrice.js" +export { InputRating } from "./components/inputs/InputRating.js" +export { InputSelect } from "./components/inputs/InputSelect.js" +export { InputSwitch } from "./components/inputs/InputSwitch.js" // Components - Inputs -export { InputText } from "./components/inputs/inputText.js" -export { InputTextArea } from "./components/inputs/inputTextArea.js" -export { InputToggle } from "./components/inputs/inputToggle.js" +export { InputText } from "./components/inputs/InputText.js" +export { InputTextArea } from "./components/inputs/InputTextArea.js" +export { InputToggle } from "./components/inputs/InputToggle.js" // Components - Layouts -export { Badge } from "./components/layouts/badge.js" -export { Chip, type ChipColors } from "./components/layouts/chip.js" -export { CircularLoader } from "./components/layouts/circularLoader.js" -export { Logo } from "./components/layouts/logo.js" -export { PageNavigation } from "./components/layouts/pageNavigation.js" -export { Separator } from "./components/layouts/separator.js" -export { Virtualizer } from "./components/layouts/virtualizer.js" +export { Badge } from "./components/layouts/Badge.js" +export { Chip, type ChipColors } from "./components/layouts/Chip.js" +export { CircularLoader } from "./components/layouts/CircularLoader.js" +export { Kbd } from "./components/layouts/Kbd.js" +export { Logo } from "./components/layouts/Logo.js" +export { PageNavigation } from "./components/layouts/PageNavigation.js" +export { Separator } from "./components/layouts/Separator.js" +export { Virtualizer } from "./components/layouts/Virtualizer.js" // Components - Overlays +export { Dialog } from "./components/overlays/dialog/dialog.js" export { Popover } from "./components/overlays/popover/popover.js" +export { ToasterProvider } from "./components/overlays/toast/ToasterProvider.js" export { Toast } from "./components/overlays/toast/toast.js" -export { ToasterProvider } from "./components/overlays/toast/toastProvider.js" export type { ToasterToast, ToastVariant } from "./components/overlays/toast/useToast.js" export { toast, useToast } from "./components/overlays/toast/useToast.js" +export type { ModalStoreValue } from "./stores/modalStore.js" +// Stores +export { ModalProvider, useModalItem, useModalStore } from "./stores/modalStore.js" +export type { PopoverStoreValue } from "./stores/popoverStore.js" +export { PopoverProvider, usePopoverStore } from "./stores/popoverStore.js" // Utilities export { cn } from "./utilities/cn.js" export { debounce } from "./utilities/debounce.js" diff --git a/packages/ui/src/stores/index.ts b/packages/ui/src/stores/index.ts new file mode 100644 index 00000000..74b9a8af --- /dev/null +++ b/packages/ui/src/stores/index.ts @@ -0,0 +1,4 @@ +export type { ModalStoreValue } from "./modalStore.js" +export { ModalItemContext, ModalProvider, ModalStoreContext, useModalItem, useModalStore } from "./modalStore.js" +export type { PopoverStoreValue } from "./popoverStore.js" +export { PopoverProvider, PopoverStoreContext, usePopoverStore } from "./popoverStore.js" diff --git a/packages/ui/src/stores/modalStore.tsx b/packages/ui/src/stores/modalStore.tsx new file mode 100644 index 00000000..4d954ff6 --- /dev/null +++ b/packages/ui/src/stores/modalStore.tsx @@ -0,0 +1,220 @@ +import { createContext, type ReactNode, useCallback, useContext, useEffect, useMemo, useReducer, useRef } from "react" + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +type ModalEntry = { + content: ReactNode + isOpen: boolean +} + +type ModalState = Record + +type ModalAction = + | { + type: "open" + id: string + content: ReactNode + } + | { + type: "close" + id: string + } + +// --------------------------------------------------------------------------- +// Reducer +// --------------------------------------------------------------------------- + +function reducer(state: ModalState, action: ModalAction): ModalState { + switch (action.type) { + case "open": + return { + ...state, + [action.id]: { + content: action.content, + isOpen: true, + }, + } + case "close": { + const entry = state[action.id] + if (entry === undefined) return state + return { + ...state, + [action.id]: { + ...entry, + isOpen: false, + }, + } + } + default: + return state + } +} + +// --------------------------------------------------------------------------- +// Contexts +// --------------------------------------------------------------------------- + +export type ModalStoreValue = { + open: (id: string, content: ReactNode) => void + close: (id: string) => void + isOpen: (id: string) => boolean +} + +export const ModalStoreContext = createContext(null) + +/** Provided by ModalProvider around each rendered modal entry so sub-components + * (DialogHeader close button) can close the modal without needing to know its id. */ +export const ModalItemContext = createContext<{ + closeModal: () => void +} | null>(null) + +// --------------------------------------------------------------------------- +// Hooks +// --------------------------------------------------------------------------- + +export function useModalStore(): ModalStoreValue { + const ctx = useContext(ModalStoreContext) + if (ctx === null) throw new Error("useModalStore must be used within ModalProvider") + return ctx +} + +export function useModalItem() { + return useContext(ModalItemContext) +} + +// --------------------------------------------------------------------------- +// ModalItem — one native per modal entry +// --------------------------------------------------------------------------- + +function ModalItem({ id, entry, close }: { id: string; entry: ModalEntry; close: (id: string) => void }) { + const dialogRef = useRef(null) + const wrapperRef = useRef(null) + + useEffect(() => { + const dialog = dialogRef.current + if (dialog === null) return + if (entry.isOpen) { + if (!dialog.open) dialog.showModal() + } else { + if (dialog.open) dialog.close() + } + }, [ + entry.isOpen, + ]) + + function handleCancel(e: React.SyntheticEvent) { + e.preventDefault() + close(id) + } + + function handleClick(e: React.MouseEvent) { + // Clicks on the centering wrapper itself = click in the backdrop area + if (e.target === wrapperRef.current) { + close(id) + } + } + + return ( + /* + * The UA styles are reset inline so we own the full layout. + * ::backdrop is styled via the injected + {children} + {Object.entries(state).map(([id, entry]) => ( + + ))} + + ) +} diff --git a/packages/ui/src/stores/popoverStore.tsx b/packages/ui/src/stores/popoverStore.tsx new file mode 100644 index 00000000..7f03b0ae --- /dev/null +++ b/packages/ui/src/stores/popoverStore.tsx @@ -0,0 +1,236 @@ +import { + createContext, + type Dispatch, + type ReactNode, + type SetStateAction, + useCallback, + useContext, + useEffect, + useMemo, + useReducer, + useRef, +} from "react" + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +type PopoverEntry = { + isOpen: boolean +} + +type PopoverState = Record + +type PopoverAction = + | { + type: "register" + id: string + } + | { + type: "unregister" + id: string + } + | { + type: "open" + id: string + } + | { + type: "close" + id: string + } + | { + type: "closeAll" + } + +// --------------------------------------------------------------------------- +// Reducer +// --------------------------------------------------------------------------- + +function reducer(state: PopoverState, action: PopoverAction): PopoverState { + switch (action.type) { + case "register": + return { + ...state, + [action.id]: { + isOpen: false, + }, + } + case "unregister": { + const { [action.id]: _removed, ...rest } = state + return rest + } + case "open": + if (state[action.id] === undefined) return state + if (state[action.id].isOpen) return state + return { + ...state, + [action.id]: { + isOpen: true, + }, + } + case "close": + if (state[action.id] === undefined) return state + if (!state[action.id].isOpen) return state + return { + ...state, + [action.id]: { + isOpen: false, + }, + } + case "closeAll": { + const hasOpen = Object.values(state).some((e) => e.isOpen) + if (!hasOpen) return state + const next: PopoverState = {} + for (const id of Object.keys(state)) { + next[id] = { + isOpen: false, + } + } + return next + } + default: + return state + } +} + +// --------------------------------------------------------------------------- +// Context +// --------------------------------------------------------------------------- + +export type PopoverStoreValue = { + open: (id: string) => void + close: (id: string) => void + closeAll: () => void + isOpen: (id: string) => boolean + register: (id: string, setOpen: Dispatch>) => void + unregister: (id: string) => void +} + +export const PopoverStoreContext = createContext(null) + +// --------------------------------------------------------------------------- +// Hook +// --------------------------------------------------------------------------- + +export function usePopoverStore(): PopoverStoreValue { + const ctx = useContext(PopoverStoreContext) + if (ctx === null) throw new Error("usePopoverStore must be used within PopoverProvider") + return ctx +} + +// --------------------------------------------------------------------------- +// Provider +// --------------------------------------------------------------------------- + +export function PopoverProvider({ children }: { children: ReactNode }) { + const [state, dispatch] = useReducer(reducer, {}) + + // Mutable map of setOpen callbacks from each PopoverRoot — stored in a ref + // so changes to these callbacks don't cause a store re-render. + const setOpenCallbacksRef = useRef>>>({}) + + const open = useCallback((id: string) => { + dispatch({ + type: "open", + id, + }) + setOpenCallbacksRef.current[id]?.(true) + }, []) + + const close = useCallback((id: string) => { + dispatch({ + type: "close", + id, + }) + setOpenCallbacksRef.current[id]?.(false) + }, []) + + const closeAll = useCallback(() => { + dispatch({ + type: "closeAll", + }) + for (const setter of Object.values(setOpenCallbacksRef.current)) { + setter(false) + } + }, []) + + const isOpen = useCallback( + (id: string) => state[id]?.isOpen ?? false, + [ + state, + ], + ) + + const register = useCallback((id: string, setOpen: Dispatch>) => { + dispatch({ + type: "register", + id, + }) + setOpenCallbacksRef.current[id] = setOpen + }, []) + + const unregister = useCallback((id: string) => { + dispatch({ + type: "unregister", + id, + }) + delete setOpenCallbacksRef.current[id] + }, []) + + const value = useMemo( + () => ({ + open, + close, + closeAll, + isOpen, + register, + unregister, + }), + [ + open, + close, + closeAll, + isOpen, + register, + unregister, + ], + ) + + // Close all open popovers when the user clicks outside any popover/trigger + useEffect(() => { + function handleClick(e: MouseEvent) { + const target = e.target as HTMLElement + if ( + target.closest("[data-popover-content]") === null && + target.closest("[data-popover-trigger]") === null + ) { + dispatch({ + type: "closeAll", + }) + for (const setter of Object.values(setOpenCallbacksRef.current)) { + setter(false) + } + } + } + document.addEventListener("click", handleClick, true) + return () => document.removeEventListener("click", handleClick, true) + }, []) + + // Close all open popovers on Escape + useEffect(() => { + function handleKeyDown(e: KeyboardEvent) { + if (e.key === "Escape") { + dispatch({ + type: "closeAll", + }) + for (const setter of Object.values(setOpenCallbacksRef.current)) { + setter(false) + } + } + } + document.addEventListener("keydown", handleKeyDown) + return () => document.removeEventListener("keydown", handleKeyDown) + }, []) + + return {children} +} diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index 3f14dfda..82783931 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -27,7 +27,9 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true, "ignoreDeprecations": "6.0", - "baseUrl": "." + "baseUrl": ".", + "incremental": true, + "tsBuildInfoFile": "./.tsbuildinfo" }, - "include": ["panda.config.ts", "src", "styled-system"] + "include": ["panda.config.ts", "src"] } diff --git a/packages/ui/tsconfig.panda.json b/packages/ui/tsconfig.panda.json new file mode 100644 index 00000000..a9a85538 --- /dev/null +++ b/packages/ui/tsconfig.panda.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["panda.config.ts", "src", "styled-system"] +} diff --git a/packages/website/package.json b/packages/website/package.json index da873c7c..715cb723 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -23,11 +23,6 @@ "@arrhes/application-metadata": "workspace:*", "@arrhes/ui": "workspace:*", "@hookform/resolvers": "5.2.2", - "@radix-ui/react-context-menu": "2.2.16", - "@radix-ui/react-dialog": "1.1.15", - "@radix-ui/react-popover": "1.1.15", - "@radix-ui/react-slot": "1.2.4", - "@radix-ui/react-tooltip": "1.2.8", "@react-pdf/renderer": "4.5.1", "@tabler/icons-react": "3.42.0", "@tanstack/react-query": "5.100.9", diff --git a/packages/website/plugins/DOC_PAGE_MANIFEST.ts b/packages/website/plugins/DOC_PAGE_MANIFEST.ts new file mode 100644 index 00000000..2c37bff6 --- /dev/null +++ b/packages/website/plugins/DOC_PAGE_MANIFEST.ts @@ -0,0 +1,339 @@ +// ─────────────────────────── Docs Search Index Plugin ───────────────────────────── + +interface DocPageManifestEntry { + path: string + file: string // relative from package root + section: string + navGroup: string + navLabel: string +} + +// Maps every static doc page route to its source file and nav metadata. +// Content strings are extracted automatically from the TSX source at build time. +export const DOC_PAGE_MANIFEST: DocPageManifestEntry[] = [ + // ── Général / Introduction ──────────────────────────────────────────────── + { + path: "/documentation", + file: "src/features/docs/general/rootGeneralDocPage.tsx", + section: "Général", + navGroup: "Introduction", + navLabel: "Accueil", + }, + { + path: "/documentation/fonctionnalités", + file: "src/features/docs/general/features/featuresGeneralDocPage.tsx", + section: "Général", + navGroup: "Introduction", + navLabel: "Fonctionnalités", + }, + { + path: "/documentation/architecture", + file: "src/features/docs/general/architecture/ArchitectureGeneralDocPage.tsx", + section: "Général", + navGroup: "Introduction", + navLabel: "Architecture", + }, + { + path: "/documentation/philosophie", + file: "src/features/docs/general/whitepaperGeneralDocPage.tsx", + section: "Général", + navGroup: "Introduction", + navLabel: "Philosophie", + }, + { + path: "/documentation/tarifs", + file: "src/features/docs/general/pricing/pricingGeneralDocPage.tsx", + section: "Général", + navGroup: "Introduction", + navLabel: "Tarifs", + }, + { + path: "/documentation/support", + file: "src/features/docs/general/supportGeneralDocPage.tsx", + section: "Général", + navGroup: "Introduction", + navLabel: "Support", + }, + // ── Général / Légal ─────────────────────────────────────────────────────── + { + path: "/documentation/mentions-légales", + file: "src/features/docs/general/legalGeneralDocPage.tsx", + section: "Général", + navGroup: "Légal", + navLabel: "Mentions légales", + }, + { + path: "/documentation/cgu", + file: "src/features/docs/general/termsGeneralDocPage.tsx", + section: "Général", + navGroup: "Légal", + navLabel: "Conditions Générales d'Utilisation", + }, + { + path: "/documentation/confidentialité", + file: "src/features/docs/general/privacyGeneralDocPage.tsx", + section: "Général", + navGroup: "Légal", + navLabel: "Politique de confidentialité", + }, + // ── Comptabilité / Introduction ─────────────────────────────────────────── + { + path: "/documentation/comptabilité", + file: "src/features/docs/accounting/introduction/rootAccountingDocPage.tsx", + section: "Comptabilité", + navGroup: "Introduction", + navLabel: "Accueil", + }, + { + path: "/documentation/comptabilité/introduction", + file: "src/features/docs/accounting/introduction/introductionAccountingDocPage.tsx", + section: "Comptabilité", + navGroup: "Introduction", + navLabel: "Introduction", + }, + { + path: "/documentation/comptabilité/introduction/partie-double", + file: "src/features/docs/accounting/introduction/doubleEntryAccountingDocPage.tsx", + section: "Comptabilité", + navGroup: "Introduction", + navLabel: "La partie double", + }, + { + path: "/documentation/comptabilité/introduction/écritures", + file: "src/features/docs/accounting/introduction/entriesAccountingDocPage.tsx", + section: "Comptabilité", + navGroup: "Introduction", + navLabel: "Les écritures", + }, + // ── Comptabilité / Comptes ──────────────────────────────────────────────── + { + path: "/documentation/comptabilité/introduction/comptes", + file: "src/features/docs/accounting/introduction/accountsAccountingDocPage.tsx", + section: "Comptabilité", + navGroup: "Comptes", + navLabel: "Introduction", + }, + { + path: "/documentation/comptabilité/introduction/classes", + file: "src/features/docs/accounting/introduction/classesAccountingDocPage.tsx", + section: "Comptabilité", + navGroup: "Comptes", + navLabel: "Classes de comptes", + }, + { + path: "/documentation/comptabilité/ressources/comptes", + file: "src/features/docs/accounting/resources/accounts/accountsResourcesAccountingDocPage.tsx", + section: "Comptabilité", + navGroup: "Comptes", + navLabel: "Liste des comptes", + }, + // ── Comptabilité / Documents ────────────────────────────────────────────── + { + path: "/documentation/comptabilité/documents", + file: "src/features/docs/accounting/reports/reportsAccountingDocPage.tsx", + section: "Comptabilité", + navGroup: "Documents", + navLabel: "Introduction", + }, + { + path: "/documentation/comptabilité/documents/journal", + file: "src/features/docs/accounting/reports/journalAccountingDocPage.tsx", + section: "Comptabilité", + navGroup: "Documents", + navLabel: "Journal", + }, + { + path: "/documentation/comptabilité/documents/grand-livre", + file: "src/features/docs/accounting/reports/ledgerAccountingDocPage.tsx", + section: "Comptabilité", + navGroup: "Documents", + navLabel: "Grand livre", + }, + { + path: "/documentation/comptabilité/documents/balance", + file: "src/features/docs/accounting/reports/balanceAccountingDocPage.tsx", + section: "Comptabilité", + navGroup: "Documents", + navLabel: "Balance", + }, + { + path: "/documentation/comptabilité/documents/bilan", + file: "src/features/docs/accounting/reports/balanceSheetAccountingDocPage.tsx", + section: "Comptabilité", + navGroup: "Documents", + navLabel: "Bilan", + }, + { + path: "/documentation/comptabilité/documents/compte-de-résultat", + file: "src/features/docs/accounting/reports/incomeStatementAccountingDocPage.tsx", + section: "Comptabilité", + navGroup: "Documents", + navLabel: "Compte de résultat", + }, + { + path: "/documentation/comptabilité/documents/annexe", + file: "src/features/docs/accounting/reports/notesAccountingDocPage.tsx", + section: "Comptabilité", + navGroup: "Documents", + navLabel: "Annexe", + }, + { + path: "/documentation/comptabilité/documents/fec", + file: "src/features/docs/accounting/reports/fecAccountingDocPage.tsx", + section: "Comptabilité", + navGroup: "Documents", + navLabel: "FEC", + }, + // ── Comptabilité / Scénarios ───────────────────────────────────────────── + { + path: "/documentation/comptabilité/ressources/scénarios", + file: "src/features/docs/accounting/resources/scenarios/scenariosResourcesAccountingDocPage.tsx", + section: "Comptabilité", + navGroup: "Scénarios", + navLabel: "Scénarios", + }, + // ── Comptabilité / Glossaire ────────────────────────────────────────────── + { + path: "/documentation/comptabilité/ressources/glossaire", + file: "src/features/docs/accounting/resources/glossary/glossaryResourcesAccountingDocPage.tsx", + section: "Comptabilité", + navGroup: "Glossaire", + navLabel: "Glossaire", + }, + // ── Dashboard / Guide d'utilisation ────────────────────────────────────── + { + path: "/documentation/dashboard", + file: "src/features/docs/dashboard/rootDashboardDocPage.tsx", + section: "Dashboard", + navGroup: "Guide d'utilisation", + navLabel: "Accueil", + }, + { + path: "/documentation/dashboard/démarrage", + file: "src/features/docs/dashboard/gettingStartedDashboardDocPage.tsx", + section: "Dashboard", + navGroup: "Guide d'utilisation", + navLabel: "Démarrage", + }, + { + path: "/documentation/dashboard/organisations", + file: "src/features/docs/dashboard/organizationsDashboardDocPage.tsx", + section: "Dashboard", + navGroup: "Guide d'utilisation", + navLabel: "Organisations", + }, + { + path: "/documentation/dashboard/exercices", + file: "src/features/docs/dashboard/yearsDashboardDocPage.tsx", + section: "Dashboard", + navGroup: "Guide d'utilisation", + navLabel: "Exercices", + }, + { + path: "/documentation/dashboard/écritures", + file: "src/features/docs/dashboard/entriesDashboardDocPage.tsx", + section: "Dashboard", + navGroup: "Guide d'utilisation", + navLabel: "Saisie des écritures", + }, + { + path: "/documentation/dashboard/stockage", + file: "src/features/docs/dashboard/filesDashboardDocPage.tsx", + section: "Dashboard", + navGroup: "Guide d'utilisation", + navLabel: "Stockage", + }, + { + path: "/documentation/dashboard/documents", + file: "src/features/docs/dashboard/reportsDashboardDocPage.tsx", + section: "Dashboard", + navGroup: "Guide d'utilisation", + navLabel: "Documents comptables", + }, + { + path: "/documentation/dashboard/facturation", + file: "src/features/docs/dashboard/BillingDashboardDocPage.tsx", + section: "Dashboard", + navGroup: "Guide d'utilisation", + navLabel: "Facturation", + }, + { + path: "/documentation/dashboard/màj", + file: "src/features/docs/dashboard/UpdatesDashboardDocPage.tsx", + section: "Dashboard", + navGroup: "Guide d'utilisation", + navLabel: "Mises à jour", + }, + // ── Dashboard / Assistant IA ────────────────────────────────────────────── + { + path: "/documentation/dashboard/assistant", + file: "src/features/docs/ai/rootAiDocPage.tsx", + section: "Dashboard", + navGroup: "Assistant IA", + navLabel: "Introduction", + }, + { + path: "/documentation/dashboard/assistant/modèles", + file: "src/features/docs/ai/modelsAiDocPage.tsx", + section: "Dashboard", + navGroup: "Assistant IA", + navLabel: "Modèles", + }, + { + path: "/documentation/dashboard/assistant/outils", + file: "src/features/docs/ai/toolsAiDocPage.tsx", + section: "Dashboard", + navGroup: "Assistant IA", + navLabel: "Outils", + }, + { + path: "/documentation/dashboard/assistant/ocr", + file: "src/features/docs/ai/ocrAiDocPage.tsx", + section: "Dashboard", + navGroup: "Assistant IA", + navLabel: "OCR", + }, + // ── API ─────────────────────────────────────────────────────────────────── + { + path: "/documentation/api", + file: "src/features/docs/api/rootApiDocPage.tsx", + section: "API", + navGroup: "API", + navLabel: "Présentation", + }, + { + path: "/documentation/api/introduction", + file: "src/features/docs/api/introductionApiDocPage.tsx", + section: "API", + navGroup: "API", + navLabel: "Introduction", + }, + { + path: "/documentation/api/authentification", + file: "src/features/docs/api/authenticationApiDocPage.tsx", + section: "API", + navGroup: "API", + navLabel: "Authentification", + }, + { + path: "/documentation/api/organisation", + file: "src/features/docs/api/organizationApiDocPage.tsx", + section: "API", + navGroup: "API", + navLabel: "Organisation", + }, + { + path: "/documentation/api/exercice", + file: "src/features/docs/api/yearApiDocPage.tsx", + section: "API", + navGroup: "API", + navLabel: "Exercice", + }, + { + path: "/documentation/api/stockage", + file: "src/features/docs/api/filesApiDocPage.tsx", + section: "API", + navGroup: "API", + navLabel: "Fichiers et documents", + }, +] diff --git a/packages/website/plugins/docsSearchIndexPlugin.ts b/packages/website/plugins/docsSearchIndexPlugin.ts new file mode 100644 index 00000000..79867051 --- /dev/null +++ b/packages/website/plugins/docsSearchIndexPlugin.ts @@ -0,0 +1,227 @@ +import { readFileSync } from "node:fs" +import { resolve } from "node:path" +import type { Plugin } from "vite" +import { DOC_PAGE_MANIFEST } from "./DOC_PAGE_MANIFEST.ts" + +/** + * Extracts searchable text from a TSX doc page source file. + * Pulls: title/description/text prop strings, DocList items, and JSX text nodes. + */ +function extractDocPageContent(source: string): string { + const parts: string[] = [] + + // Strip comments so we don't extract text from them + const stripped = source + .replace(/\{\/\*[\s\S]*?\*\/\}/g, "") // JSX block comments + .replace(/\/\/[^\n]*/g, "") // line comments + + // 1. Named text props (title, description, text, label, headers, variant-unrelated names) + const propRe = /\b(?:title|description)\s*=\s*"([^"]+)"/g + for (const m of stripped.matchAll(propRe)) parts.push(m[1]) + + // 2. String items inside items={["...", ...]} — DocList, DocTable rows, etc. + const itemsBlockRe = /\bitems\s*=\s*\{\s*\[([^\]]*?)\]\s*\}/gs + for (const m of stripped.matchAll(itemsBlockRe)) { + for (const s of m[1].matchAll(/"([^"]{4,})"/g)) parts.push(s[1]) + } + + // 3. DocTable rows={[["cell", "cell"], ...]} + const rowsBlockRe = /\brows\s*=\s*\{\s*\[([^\]]*?\][^\]]*?)\]\s*\}/gs + for (const m of stripped.matchAll(rowsBlockRe)) { + for (const s of m[1].matchAll(/"([^"]{4,})"/g)) parts.push(s[1]) + } + + // 4. JSX text nodes: text between > and < that is not whitespace-only + const textNodeRe = />([^<>{}\n]{8,})= 8 && !/^\s*$/.test(t)) parts.push(t) + } + + // Deduplicate consecutive identical strings and collapse whitespace + return [ + ...new Set(parts), + ] + .map((p) => p.replace(/\s+/g, " ").trim()) + .filter(Boolean) + .join(" ") +} + +const DOCS_SEARCH_VIRTUAL_MODULE_ID = "virtual:docs-search-index" +const RESOLVED_DOCS_SEARCH_VIRTUAL_MODULE_ID = `\0${DOCS_SEARCH_VIRTUAL_MODULE_ID}` + +interface GeneratedSearchEntry { + path: string + title: string + description: string + section: string + navGroup: string + navLabel: string + content: string +} + +/** + * Parses accountsData.ts source and returns one search entry per account. + * Content is compact: number + label + description + className + type + side. + */ +function extractAccountEntries(source: string): GeneratedSearchEntry[] { + const entries: GeneratedSearchEntry[] = [] + const chunks = source.split(/(?=\bdefineAccount\()/) + for (const chunk of chunks) { + if (!/^\s*defineAccount\s*\(\s*"/.test(chunk)) continue + const numMatch = chunk.match(/defineAccount\(\s*"([^"]+)"/) + if (!numMatch) continue + const number = numMatch[1] + const labelMatch = chunk.match(/defineAccount\(\s*"[^"]+",\s*"([^"]+)"/) + if (!labelMatch) continue + const label = labelMatch[1] + const description = chunk.match(/\bdescription\s*:\s*"([^"]+)"/)?.[1] ?? "" + const className = chunk.match(/\bclassName\s*:\s*"([^"]+)"/)?.[1] ?? "" + const type = chunk.match(/\btype\s*:\s*"([^"]+)"/)?.[1] ?? "" + const side = chunk.match(/\bside\s*:\s*"([^"]+)"/)?.[1] ?? "" + entries.push({ + path: `/documentation/comptabilité/ressources/comptes/${number}`, + title: `${number} — ${label}`, + description, + section: "Comptabilité", + navGroup: "Comptes", + navLabel: label, + content: [ + number, + label, + description, + className, + type, + side, + ] + .filter(Boolean) + .join(" "), + }) + } + return entries +} + +/** + * Parses glossaryData.ts source and returns one search entry per glossary term. + * Content: term + englishTranslation + definition + relatedTerms. + */ +function extractGlossaryEntries(source: string): GeneratedSearchEntry[] { + const toSlug = (term: string) => + term + .toLowerCase() + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .replace(/[^a-z0-9]+/g, "-") + .replace(/(^-|-$)/g, "") + + const entries: GeneratedSearchEntry[] = [] + const chunks = source.split(/(?=\bdefineTerm\()/) + for (const chunk of chunks) { + if (!/^\s*defineTerm\s*\(\s*"/.test(chunk)) continue + const m = chunk.match(/defineTerm\(\s*"([^"]+)",\s*"([^"]+)",\s*"([^"]+)"/) + if (!m) continue + const [, term, englishTranslation, definition] = m + const relatedTerms: string[] = [] + const rtBlock = chunk.match(/relatedTerms\s*:\s*\[([^\]]*)\]/) + if (rtBlock) { + for (const rt of rtBlock[1].matchAll(/"([^"]+)"/g)) relatedTerms.push(rt[1]) + } + entries.push({ + path: `/documentation/comptabilité/ressources/glossaire/${toSlug(term)}`, + title: term, + description: definition, + section: "Comptabilité", + navGroup: "Glossaire", + navLabel: term, + content: [ + term, + englishTranslation, + definition, + ...relatedTerms, + ].join(" "), + }) + } + return entries +} + +export function docsSearchIndexPlugin(): Plugin { + const pkgRoot = resolve(__dirname, "..") + const accountsDataPath = resolve(pkgRoot, "src/features/docs/accounting/resources/accounts/accountsData.ts") + const glossaryDataPath = resolve(pkgRoot, "src/features/docs/accounting/resources/glossary/glossaryData.ts") + + function buildIndex(): string { + const pageEntries = DOC_PAGE_MANIFEST.map((entry) => { + const filePath = resolve(pkgRoot, entry.file) + let source = "" + try { + source = readFileSync(filePath, "utf-8") + } catch { + console.warn(`[docs-search] Could not read ${entry.file}`) + } + const content = extractDocPageContent(source) + return { + path: entry.path, + title: extractTitle(source, entry.navLabel), + description: extractDescription(source, ""), + section: entry.section, + navGroup: entry.navGroup, + navLabel: entry.navLabel, + content: [ + entry.navGroup, + entry.navLabel, + content, + ] + .filter(Boolean) + .join(" "), + } + }) + + const accountEntries = extractAccountEntries(readFileSync(accountsDataPath, "utf-8")) + const glossaryEntries = extractGlossaryEntries(readFileSync(glossaryDataPath, "utf-8")) + + const entries = [ + ...pageEntries, + ...accountEntries, + ...glossaryEntries, + ] + return `export const docsSearchIndex = ${JSON.stringify(entries, null, 4)};` + } + + return { + name: "docs-search-index", + resolveId(id) { + if (id === DOCS_SEARCH_VIRTUAL_MODULE_ID) return RESOLVED_DOCS_SEARCH_VIRTUAL_MODULE_ID + }, + load(id) { + if (id === RESOLVED_DOCS_SEARCH_VIRTUAL_MODULE_ID) return buildIndex() + }, + handleHotUpdate({ file, server }) { + const isDocPage = DOC_PAGE_MANIFEST.some((e) => file.endsWith(e.file.replace(/\//g, "/"))) + const isDataFile = file === accountsDataPath || file === glossaryDataPath + if (isDocPage || isDataFile) { + const mod = server.moduleGraph.getModuleById(RESOLVED_DOCS_SEARCH_VIRTUAL_MODULE_ID) + if (mod) server.moduleGraph.invalidateModule(mod) + server.ws.send({ + type: "full-reload", + }) + } + }, + } +} + +/** Extract the `title` prop from a DocHeader (falls back to navLabel). */ +function extractTitle(source: string, fallback: string): string { + const m = source.match(/DocHeader[^>]*\btitle\s*=\s*"([^"]+)"/) + if (m) return m[1] + // Custom pages with

or plain heading text + const h1 = source.match(/]*>\s*([^<\n]{3,})\s*<\/h1>/) + if (h1) return h1[1].trim() + return fallback +} + +/** Extract the `description` prop from a DocHeader, falling back to

below

. */ +function extractDescription(source: string, fallback: string): string { + const m = source.match(/DocHeader[^>]*\bdescription\s*=\s*"([^"]+)"/) + if (m) return m[1] + return fallback +} diff --git a/packages/website/plugins/fontPreloadPlugin.ts b/packages/website/plugins/fontPreloadPlugin.ts new file mode 100644 index 00000000..c1119ca4 --- /dev/null +++ b/packages/website/plugins/fontPreloadPlugin.ts @@ -0,0 +1,24 @@ +import type { Plugin } from "vite" + +export function fontPreloadPlugin(): Plugin { + return { + name: "font-preload", + transformIndexHtml: { + order: "post", + handler(_html, ctx) { + const fontAssets = (ctx.bundle ? Object.keys(ctx.bundle) : []).filter((name) => name.endsWith(".woff2")) + return fontAssets.map((font) => ({ + tag: "link", + attrs: { + rel: "preload", + as: "font", + type: "font/woff2", + href: `/${font}`, + crossorigin: "anonymous", + }, + injectTo: "head" as const, + })) + }, + }, + } +} diff --git a/packages/website/plugins/prerenderPlugin.ts b/packages/website/plugins/prerenderPlugin.ts new file mode 100644 index 00000000..614729c2 --- /dev/null +++ b/packages/website/plugins/prerenderPlugin.ts @@ -0,0 +1,136 @@ +import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs" +import { dirname, resolve } from "node:path" +import react from "@vitejs/plugin-react" +import { type Plugin, build as viteBuild } from "vite" +import { DOC_PAGE_MANIFEST } from "./DOC_PAGE_MANIFEST" +import { docsSearchIndexPlugin } from "./docsSearchIndexPlugin" + +export function prerenderPlugin(): Plugin { + return { + name: "prerender", + async closeBundle() { + if (process.env.BUILD_PRERENDER) return + process.env.BUILD_PRERENDER = "1" + + try { + const pkgRoot = resolve(__dirname, "..") + const buildDir = resolve(pkgRoot, "build") + const renderBuildDir = resolve(pkgRoot, "build-render") + + const spaShell = readFileSync(resolve(buildDir, "index.html"), "utf-8") + + // Build a Node.js bundle of render.tsx so we can call renderToString() for each route + await viteBuild({ + root: resolve(pkgRoot, "src"), + base: "/", + envDir: pkgRoot, + plugins: [ + react({ + include: "**/*.tsx", + }), + docsSearchIndexPlugin(), + ], + build: { + ssr: "../plugins/render.tsx", + outDir: renderBuildDir, + emptyOutDir: true, + rollupOptions: { + output: { + entryFileNames: "render.js", + format: "es", + }, + }, + }, + logLevel: "warn", + }) + + // Load the render bundle and generate static HTML for each route + const { render } = (await import(`file://${resolve(renderBuildDir, "render.js")}`)) as { + render: (url: string) => Promise + } + + // Collect all routes to prerender + const routes: string[] = [ + "/", + "/connexion", + "/inscription", + "/mot-de-passe-oublié", + ...DOC_PAGE_MANIFEST.map((e) => e.path), + ] + + // Dynamic account slugs + const accountsDataPath = resolve( + pkgRoot, + "src/features/docs/accounting/resources/accounts/accountsData.ts", + ) + for (const m of readFileSync(accountsDataPath, "utf-8").matchAll( + /defineAccount\(\s*\n?\s*"([^"]+)"/g, + )) { + routes.push(`/documentation/comptabilité/ressources/comptes/${m[1]}`) + } + + // Dynamic glossary slugs + const toSlug = (term: string) => + term + .toLowerCase() + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .replace(/[^a-z0-9]+/g, "-") + .replace(/(^-|-$)/g, "") + const glossaryDataPath = resolve( + pkgRoot, + "src/features/docs/accounting/resources/glossary/glossaryData.ts", + ) + for (const m of readFileSync(glossaryDataPath, "utf-8").matchAll(/defineTerm\(\s*\n?\s*"([^"]+)"/g)) { + routes.push(`/documentation/comptabilité/ressources/glossaire/${toSlug(m[1])}`) + } + + // Scenario paths + const scenariosDataPath = resolve( + pkgRoot, + "src/features/docs/accounting/resources/scenarios/scenariosData.ts", + ) + for (const m of readFileSync(scenariosDataPath, "utf-8").matchAll( + /path:\s*"(\/documentation\/comptabilité\/scénarios\/[^"]+)"/g, + )) { + routes.push(m[1]) + } + + let count = 0 + for (const route of routes) { + try { + const appHtml = await render(route) + + // Extract the page-specific rendered by React and update <head> + const renderedTitle = /<title>([\s\S]*?)<\/title>/.exec(appHtml)?.[1] + let html = spaShell + if (renderedTitle) { + html = html.replace(/<title>[^<]*<\/title>/, `<title>${renderedTitle}`) + } + html = html.replace('
', `
${appHtml}
`) + + const outFile = + route === "/" + ? resolve(buildDir, "index.html") + : resolve(buildDir, route.slice(1), "index.html") + mkdirSync(dirname(outFile), { + recursive: true, + }) + writeFileSync(outFile, html, "utf-8") + count++ + } catch (err) { + console.warn(`[prerender] Failed to render ${route}:`, err) + } + } + + rmSync(renderBuildDir, { + recursive: true, + force: true, + }) + console.log(`[prerender] Generated ${count} static HTML files`) + } finally { + delete process.env.BUILD_PRERENDER + } + }, + } +} diff --git a/packages/website/plugins/render.tsx b/packages/website/plugins/render.tsx new file mode 100644 index 00000000..2ba806f1 --- /dev/null +++ b/packages/website/plugins/render.tsx @@ -0,0 +1,25 @@ +import { createMemoryHistory, createRouter, RouterProvider } from "@tanstack/react-router" +import { renderToString } from "react-dom/server" +import { applicationTree } from "../src/routes/applicationTree.js" + +export async function render(url: string): Promise { + const router = createRouter({ + routeTree: applicationTree, + history: createMemoryHistory({ + initialEntries: [ + url, + ], + }), + scrollRestoration: false, + context: { + title: undefined, + section: undefined, + description: undefined, + robots: undefined, + isAuthenticated: false, + userSession: undefined, + }, + }) + await router.load() + return renderToString() +} diff --git a/packages/website/plugins/sitemapPlugin.ts b/packages/website/plugins/sitemapPlugin.ts new file mode 100644 index 00000000..33e46f8a --- /dev/null +++ b/packages/website/plugins/sitemapPlugin.ts @@ -0,0 +1,360 @@ +import { readFileSync, writeFileSync } from "node:fs" +import { resolve } from "node:path" +import type { Plugin } from "vite" + +export function sitemapPlugin(): Plugin { + return { + name: "sitemap-generator", + closeBundle() { + const baseUrl = "https://arrhes.com" + const today = new Date().toISOString().split("T")[0] + + // Static public routes + const staticRoutes = [ + { + path: "/", + priority: "1.0", + changefreq: "weekly", + }, + { + path: "/connexion", + priority: "0.5", + changefreq: "monthly", + }, + { + path: "/inscription", + priority: "0.6", + changefreq: "monthly", + }, + { + path: "/mot-de-passe-oublié", + priority: "0.5", + changefreq: "monthly", + }, + + // General docs + { + path: "/documentation", + priority: "0.8", + changefreq: "weekly", + }, + { + path: "/documentation/fonctionnalités", + priority: "0.7", + changefreq: "monthly", + }, + { + path: "/documentation/philosophie", + priority: "0.5", + changefreq: "monthly", + }, + { + path: "/documentation/tarifs", + priority: "0.7", + changefreq: "monthly", + }, + { + path: "/documentation/support", + priority: "0.5", + changefreq: "monthly", + }, + { + path: "/documentation/mentions-légales", + priority: "0.3", + changefreq: "yearly", + }, + { + path: "/documentation/cgu", + priority: "0.3", + changefreq: "yearly", + }, + { + path: "/documentation/confidentialité", + priority: "0.3", + changefreq: "yearly", + }, + + // Accounting docs + { + path: "/documentation/comptabilité", + priority: "0.8", + changefreq: "weekly", + }, + { + path: "/documentation/comptabilité/introduction", + priority: "0.7", + changefreq: "monthly", + }, + { + path: "/documentation/comptabilité/partie-double", + priority: "0.7", + changefreq: "monthly", + }, + { + path: "/documentation/comptabilité/écritures", + priority: "0.7", + changefreq: "monthly", + }, + { + path: "/documentation/comptabilité/comptes/introduction", + priority: "0.7", + changefreq: "monthly", + }, + { + path: "/documentation/comptabilité/comptes/classes", + priority: "0.7", + changefreq: "monthly", + }, + { + path: "/documentation/comptabilité/comptes/liste", + priority: "0.8", + changefreq: "weekly", + }, + { + path: "/documentation/comptabilité/documents", + priority: "0.7", + changefreq: "monthly", + }, + { + path: "/documentation/comptabilité/documents/journal", + priority: "0.7", + changefreq: "monthly", + }, + { + path: "/documentation/comptabilité/documents/grand-livre", + priority: "0.7", + changefreq: "monthly", + }, + { + path: "/documentation/comptabilité/documents/balance", + priority: "0.7", + changefreq: "monthly", + }, + { + path: "/documentation/comptabilité/documents/bilan", + priority: "0.7", + changefreq: "monthly", + }, + { + path: "/documentation/comptabilité/documents/compte-de-résultat", + priority: "0.7", + changefreq: "monthly", + }, + { + path: "/documentation/comptabilité/documents/annexe", + priority: "0.7", + changefreq: "monthly", + }, + { + path: "/documentation/comptabilité/documents/fec", + priority: "0.7", + changefreq: "monthly", + }, + { + path: "/documentation/comptabilité/scénarios", + priority: "0.7", + changefreq: "monthly", + }, + { + path: "/documentation/comptabilité/glossaire", + priority: "0.7", + changefreq: "monthly", + }, + + // Dashboard docs + { + path: "/documentation/dashboard", + priority: "0.7", + changefreq: "monthly", + }, + { + path: "/documentation/dashboard/démarrage", + priority: "0.7", + changefreq: "monthly", + }, + { + path: "/documentation/dashboard/organisations", + priority: "0.6", + changefreq: "monthly", + }, + { + path: "/documentation/dashboard/exercices", + priority: "0.6", + changefreq: "monthly", + }, + { + path: "/documentation/dashboard/écritures", + priority: "0.6", + changefreq: "monthly", + }, + { + path: "/documentation/dashboard/stockage", + priority: "0.6", + changefreq: "monthly", + }, + { + path: "/documentation/dashboard/documents", + priority: "0.6", + changefreq: "monthly", + }, + { + path: "/documentation/dashboard/facturation", + priority: "0.6", + changefreq: "monthly", + }, + { + path: "/documentation/dashboard/màj", + priority: "0.6", + changefreq: "monthly", + }, + { + path: "/documentation/dashboard/assistant", + priority: "0.6", + changefreq: "monthly", + }, + { + path: "/documentation/dashboard/assistant/modèles", + priority: "0.6", + changefreq: "monthly", + }, + { + path: "/documentation/dashboard/assistant/outils", + priority: "0.6", + changefreq: "monthly", + }, + { + path: "/documentation/dashboard/assistant/ocr", + priority: "0.6", + changefreq: "monthly", + }, + + // API docs + { + path: "/documentation/api", + priority: "0.7", + changefreq: "monthly", + }, + { + path: "/documentation/api/introduction", + priority: "0.6", + changefreq: "monthly", + }, + { + path: "/documentation/api/authentification", + priority: "0.6", + changefreq: "monthly", + }, + { + path: "/documentation/api/organisation", + priority: "0.6", + changefreq: "monthly", + }, + { + path: "/documentation/api/exercice", + priority: "0.6", + changefreq: "monthly", + }, + { + path: "/documentation/api/stockage", + priority: "0.6", + changefreq: "monthly", + }, + ] + + // Extract dynamic account slugs from source + const accountsDataPath = resolve( + __dirname, + "../src/features/docs/accounting/resources/accounts/accountsData.ts", + ) + const accountsSrc = readFileSync(accountsDataPath, "utf-8") + const accountSlugs = [ + ...accountsSrc.matchAll(/defineAccount\(\s*\n?\s*"([^"]+)"/g), + ].map((m) => m[1]) + + // Extract dynamic glossary slugs from source + const glossaryDataPath = resolve( + __dirname, + "../src/features/docs/accounting/resources/glossary/glossaryData.ts", + ) + const glossarySrc = readFileSync(glossaryDataPath, "utf-8") + // The toSlug function: lowercase, NFD normalize, strip diacritics, replace non-alnum with -, trim - + const glossaryTerms = [ + ...glossarySrc.matchAll(/defineTerm\(\s*\n?\s*"([^"]+)"/g), + ].map((m) => m[1]) + const toSlug = (term: string) => + term + .toLowerCase() + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .replace(/[^a-z0-9]+/g, "-") + .replace(/(^-|-$)/g, "") + const glossarySlugs = glossaryTerms.map(toSlug) + + // Extract scenario paths directly from scenariosData.ts + const scenariosDataPath = resolve( + __dirname, + "../src/features/docs/accounting/resources/scenarios/scenariosData.ts", + ) + const scenariosSrc = readFileSync(scenariosDataPath, "utf-8") + const scenarioPaths = [ + ...scenariosSrc.matchAll(/path:\s*"(\/documentation\/comptabilité\/scénarios\/[^"]+)"/g), + ].map((m) => m[1]) + + // Build URL entries + const routeMap = new Map< + string, + { + changefreq: string + priority: string + } + >() + const addRoute = (path: string, changefreq: string, priority: string) => { + if (!routeMap.has(path)) { + routeMap.set(path, { + changefreq, + priority, + }) + } + } + + for (const route of staticRoutes) { + addRoute(route.path, route.changefreq, route.priority) + } + + for (const slug of accountSlugs) { + addRoute(`/documentation/comptabilité/comptes/liste/${slug}`, "monthly", "0.5") + } + + for (const slug of glossarySlugs) { + addRoute(`/documentation/comptabilité/glossaire/${slug}`, "monthly", "0.5") + } + + for (const path of scenarioPaths) { + addRoute(path, "monthly", "0.5") + } + + const urls = [ + ...routeMap.entries(), + ] + .sort(([pathA], [pathB]) => pathA.localeCompare(pathB, "fr")) + .map( + ([path, metadata]) => ` + ${encodeURI(`${baseUrl}${path}`)} + ${today} + ${metadata.changefreq} + ${metadata.priority} + `, + ) + + const sitemap = ` + +${urls.join("\n")} + +` + const outPath = resolve(__dirname, "../build/sitemap.xml") + writeFileSync(outPath, sitemap, "utf-8") + + console.log(`[sitemap] Generated sitemap.xml with ${urls.length} URLs`) + }, + } +} diff --git a/packages/website/src/components/inputDataCombobox.tsx b/packages/website/src/components/InputDataCombobox.tsx similarity index 100% rename from packages/website/src/components/inputDataCombobox.tsx rename to packages/website/src/components/InputDataCombobox.tsx diff --git a/packages/website/src/components/linkButton.tsx b/packages/website/src/components/LinkButton.tsx similarity index 100% rename from packages/website/src/components/linkButton.tsx rename to packages/website/src/components/LinkButton.tsx diff --git a/packages/website/src/components/document/DocCode.tsx b/packages/website/src/components/document/DocCode.tsx new file mode 100644 index 00000000..a5260b60 --- /dev/null +++ b/packages/website/src/components/document/DocCode.tsx @@ -0,0 +1,19 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import type { ReactNode } from "react" + +export function DocCode(props: { children: ReactNode }) { + return ( + + {props.children} + + ) +} diff --git a/packages/website/src/components/document/DocCodeBlock.tsx b/packages/website/src/components/document/DocCodeBlock.tsx new file mode 100644 index 00000000..da0293e5 --- /dev/null +++ b/packages/website/src/components/document/DocCodeBlock.tsx @@ -0,0 +1,88 @@ +import { css, cx } from "@arrhes/ui/utilities/cn.js" +import { IconCheck, IconCopy } from "@tabler/icons-react" +import { useState } from "react" + +export function DocCodeBlock(props: { children: string }) { + const [copied, setCopied] = useState(false) + + function handleCopy() { + void navigator.clipboard.writeText(props.children) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + + return ( +
+
+                {props.children}
+            
+ +
+ ) +} diff --git a/packages/website/src/components/document/DocDefinition.tsx b/packages/website/src/components/document/DocDefinition.tsx new file mode 100644 index 00000000..ac20ca80 --- /dev/null +++ b/packages/website/src/components/document/DocDefinition.tsx @@ -0,0 +1,45 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconBookmark } from "@tabler/icons-react" +import type { ReactNode } from "react" +import { DocTip } from "./DocTip.js" + +export function DocDefinition(props: { term?: string; children: ReactNode }) { + return ( + +
+ {props.term && ( +
+ {props.term} +
+ )} +
+ {props.children} +
+
+
+ ) +} diff --git a/packages/website/src/components/document/DocExample.tsx b/packages/website/src/components/document/DocExample.tsx new file mode 100644 index 00000000..7ebc8f46 --- /dev/null +++ b/packages/website/src/components/document/DocExample.tsx @@ -0,0 +1,14 @@ +import { IconInfoCircle } from "@tabler/icons-react" +import { DocTip } from "./DocTip.js" + +export function DocExample(props: { title?: string; children: React.ReactNode }) { + return ( + + {props.children} + + ) +} diff --git a/packages/website/src/components/document/DocGlossaryEntry.tsx b/packages/website/src/components/document/DocGlossaryEntry.tsx new file mode 100644 index 00000000..3e79823e --- /dev/null +++ b/packages/website/src/components/document/DocGlossaryEntry.tsx @@ -0,0 +1,17 @@ +import type { ReactNode } from "react" +import { DocDefinition } from "./DocDefinition.js" + +export function DocGlossaryEntry(props: { term: string; children: ReactNode }) { + const id = props.term + .toLowerCase() + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .replace(/[^a-z0-9]+/g, "-") + .replace(/(^-|-$)/g, "") + + return ( +
+ {props.children} +
+ ) +} diff --git a/packages/website/src/components/document/docHeader.tsx b/packages/website/src/components/document/DocHeader.tsx similarity index 100% rename from packages/website/src/components/document/docHeader.tsx rename to packages/website/src/components/document/DocHeader.tsx diff --git a/packages/website/src/components/document/DocIndexLink.tsx b/packages/website/src/components/document/DocIndexLink.tsx new file mode 100644 index 00000000..8d4a01f8 --- /dev/null +++ b/packages/website/src/components/document/DocIndexLink.tsx @@ -0,0 +1,33 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import type { ValidRoutes } from "../../routes/applicationRouter.js" +import { LinkButton } from "../LinkButton.js" + +export function DocIndexLink(props: { to: ValidRoutes; label: string }) { + return ( + + + {props.label} + + ) +} diff --git a/packages/website/src/components/document/docLastUpdate.tsx b/packages/website/src/components/document/DocLastUpdate.tsx similarity index 100% rename from packages/website/src/components/document/docLastUpdate.tsx rename to packages/website/src/components/document/DocLastUpdate.tsx diff --git a/packages/website/src/components/document/DocLink.tsx b/packages/website/src/components/document/DocLink.tsx new file mode 100644 index 00000000..8ecd957f --- /dev/null +++ b/packages/website/src/components/document/DocLink.tsx @@ -0,0 +1,27 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import type { ValidParams, ValidRoutes } from "../../routes/applicationRouter.js" +import { LinkButton } from "../LinkButton.js" + +export function DocLink(props: { to: ValidRoutes; params?: ValidParams; hash?: string; children: React.ReactNode }) { + return ( + + {props.children} + + ) +} diff --git a/packages/website/src/components/document/DocLinkCard.tsx b/packages/website/src/components/document/DocLinkCard.tsx new file mode 100644 index 00000000..41ae89e4 --- /dev/null +++ b/packages/website/src/components/document/DocLinkCard.tsx @@ -0,0 +1,98 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import type { ReactNode } from "react" +import type { ValidRoutes } from "../../routes/applicationRouter.js" +import { LinkButton } from "../LinkButton.js" + +export type DocLinkCardColor = "information" | "success" | "primary" | "warning" + +export function DocLinkCard(props: { + icon: ReactNode + iconColor?: DocLinkCardColor + title: string + description: string + to: ValidRoutes +}) { + const colorStyles: Record< + DocLinkCardColor, + { + bg: string + color: string + } + > = { + information: { + bg: "information/10", + color: "information", + }, + success: { + bg: "success/10", + color: "success", + }, + primary: { + bg: "primary/10", + color: "primary", + }, + warning: { + bg: "warning/10", + color: "warning", + }, + } + const style = colorStyles[props.iconColor ?? "primary"] + + return ( + +
+ {props.icon} +
+

+ {props.title} +

+

+ {props.description} +

+
+ ) +} diff --git a/packages/website/src/components/document/docList.tsx b/packages/website/src/components/document/DocList.tsx similarity index 100% rename from packages/website/src/components/document/docList.tsx rename to packages/website/src/components/document/DocList.tsx diff --git a/packages/website/src/components/document/docParagraph.tsx b/packages/website/src/components/document/DocParagraph.tsx similarity index 100% rename from packages/website/src/components/document/docParagraph.tsx rename to packages/website/src/components/document/DocParagraph.tsx diff --git a/packages/website/src/components/document/docRoot.tsx b/packages/website/src/components/document/DocRoot.tsx similarity index 100% rename from packages/website/src/components/document/docRoot.tsx rename to packages/website/src/components/document/DocRoot.tsx diff --git a/packages/website/src/components/document/DocRouteRequest.tsx b/packages/website/src/components/document/DocRouteRequest.tsx new file mode 100644 index 00000000..3c9136cb --- /dev/null +++ b/packages/website/src/components/document/DocRouteRequest.tsx @@ -0,0 +1,432 @@ +import type { routeDefinition } from "@arrhes/application-metadata/utilities" +import { css } from "@arrhes/ui/utilities/cn.js" +import { useState } from "react" + +const methodBadgeClassName: Record<"GET" | "POST" | "PATCH" | "DELETE", string> = { + GET: css({ + fontSize: "xs", + fontWeight: "bold", + paddingX: "0.5rem", + paddingY: "0.15rem", + borderRadius: "sm", + backgroundColor: "information/10", + color: "information", + textTransform: "uppercase", + letterSpacing: "wider", + }), + POST: css({ + fontSize: "xs", + fontWeight: "bold", + paddingX: "0.5rem", + paddingY: "0.15rem", + borderRadius: "sm", + backgroundColor: "success/10", + color: "success", + textTransform: "uppercase", + letterSpacing: "wider", + }), + PATCH: css({ + fontSize: "xs", + fontWeight: "bold", + paddingX: "0.5rem", + paddingY: "0.15rem", + borderRadius: "sm", + backgroundColor: "warning/10", + color: "warning", + textTransform: "uppercase", + letterSpacing: "wider", + }), + DELETE: css({ + fontSize: "xs", + fontWeight: "bold", + paddingX: "0.5rem", + paddingY: "0.15rem", + borderRadius: "sm", + backgroundColor: "error/10", + color: "error", + textTransform: "uppercase", + letterSpacing: "wider", + }), +} + +type SchemaLike = { + type: string + entries?: Readonly> + wrapped?: SchemaLike + pipe?: readonly SchemaLike[] + item?: SchemaLike + options?: readonly string[] +} + +function getBaseType(schema: SchemaLike): string { + if (schema.type === "optional" || schema.type === "nullable" || schema.type === "non_nullable") { + return getBaseType(schema.wrapped!) + } + if (schema.type === "pipe") { + return getBaseType(schema.pipe![0]!) + } + if (schema.type === "array") { + const itemType = getBaseType(schema.item!) + return `${itemType}[]` + } + if (schema.type === "object") { + return "object" + } + if (schema.type === "picklist") { + return schema.options?.join(" | ") ?? "string" + } + return schema.type +} + +function isOptional(schema: SchemaLike): boolean { + if (schema.type === "optional") return true + if (schema.type === "nullable") return true + if (schema.type === "non_nullable" || schema.type === "pipe") return false + return false +} + +function extractFields(schema: SchemaLike): { + name: string + type: string + required: boolean +}[] { + if (schema.type !== "object" || !schema.entries) return [] + + return Object.entries(schema.entries).map(([name, fieldSchema]) => ({ + name, + type: getBaseType(fieldSchema), + required: !isOptional(fieldSchema), + })) +} + +function extractReturnFields(schema: SchemaLike): + | { + name: string + type: string + }[] + | { + isArray: true + fields: { + name: string + type: string + }[] + } { + if (schema.type === "array" && schema.item) { + const fields = extractFields(schema.item).map(({ name, type }) => ({ + name, + type, + })) + return { + isArray: true, + fields, + } + } + return extractFields(schema).map(({ name, type }) => ({ + name, + type, + })) +} + +function FieldTable(props: { + fields: { + name: string + type: string + required?: boolean + }[] + showRequired: boolean +}) { + if (props.fields.length === 0) { + return ( +
+ Aucun champ requis +
+ ) + } + + return ( + + + + + + {props.showRequired && ( + + )} + + + + {props.fields.map((field) => ( + + + + {props.showRequired && ( + + )} + + ))} + +
+ Nom + + Type + + Requis +
+ {field.name} + + {field.type} + + {field.required ? "oui" : "non"} +
+ ) +} + +type Tab = "body" | "response" + +export function DocRouteRequest(props: { routeDefinition: ReturnType; description?: string }) { + const [activeTab, setActiveTab] = useState("body") + + const bodyFields = extractFields(props.routeDefinition.schemas.body as SchemaLike) + const returnData = extractReturnFields(props.routeDefinition.schemas.return as SchemaLike) + const isArrayReturn = !Array.isArray(returnData) && returnData.isArray + const returnFields = isArrayReturn + ? returnData.fields + : (returnData as { + name: string + type: string + }[]) + + return ( +
+ {/* Header: method + path + headers */} +
+
+ + {props.routeDefinition.method} + + + {props.routeDefinition.path.replace(/^\/v1/, "")} + +
+
+ + Content-Type: application/json + + + Authorization: Bearer {""} + +
+
+ + {/* Description */} + {props.description && ( +
+ {props.description} +
+ )} + + {/* Tabs */} +
+ + +
+ + {/* Tab content */} +
+ {activeTab === "body" && ( + + )} + {activeTab === "response" && ( + + )} +
+
+ ) +} diff --git a/packages/website/src/components/document/docSection.tsx b/packages/website/src/components/document/DocSection.tsx similarity index 100% rename from packages/website/src/components/document/docSection.tsx rename to packages/website/src/components/document/DocSection.tsx diff --git a/packages/website/src/components/document/DocSectionCard.tsx b/packages/website/src/components/document/DocSectionCard.tsx new file mode 100644 index 00000000..1477e34c --- /dev/null +++ b/packages/website/src/components/document/DocSectionCard.tsx @@ -0,0 +1,154 @@ +import { ButtonOutlineContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconChevronRight } from "@tabler/icons-react" +import type { ReactNode } from "react" +import type { ValidRoutes } from "../../routes/applicationRouter.js" +import { LinkButton } from "../LinkButton.js" + +export type DocSectionCardColor = "information" | "success" | "primary" | "warning" + +export function DocSectionCard(props: { + icon: ReactNode + iconColor?: DocSectionCardColor + title: string + description: string + links: { + to: ValidRoutes + label: string + }[] + ctaTo: ValidRoutes + ctaLabel: string +}) { + const colorStyles: Record< + DocSectionCardColor, + { + bg: string + color: string + } + > = { + information: { + bg: "information/10", + color: "information", + }, + success: { + bg: "success/10", + color: "success", + }, + primary: { + bg: "primary/10", + color: "primary", + }, + warning: { + bg: "warning/10", + color: "warning", + }, + } + const style = colorStyles[props.iconColor ?? "primary"] + + return ( +
+ {/* Icon */} +
+ {props.icon} +
+ + {/* Title */} +

+ {props.title} +

+ + {/* Description */} +

+ {props.description} +

+ + {/* Links */} +
+ {props.links.map((link) => ( + + + {link.label} + + ))} +
+ + {/* CTA */} + + } + /> + +
+ ) +} diff --git a/packages/website/src/components/document/docSourceRef.tsx b/packages/website/src/components/document/DocSourceRef.tsx similarity index 100% rename from packages/website/src/components/document/docSourceRef.tsx rename to packages/website/src/components/document/DocSourceRef.tsx diff --git a/packages/website/src/components/document/docSources.tsx b/packages/website/src/components/document/DocSources.tsx similarity index 100% rename from packages/website/src/components/document/docSources.tsx rename to packages/website/src/components/document/DocSources.tsx diff --git a/packages/website/src/components/document/DocTable.tsx b/packages/website/src/components/document/DocTable.tsx new file mode 100644 index 00000000..b7e4a090 --- /dev/null +++ b/packages/website/src/components/document/DocTable.tsx @@ -0,0 +1,82 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import type { ReactNode } from "react" + +export function DocTable(props: { headers: string[]; rows: (string | ReactNode)[][] }) { + return ( +
+ + + + {props.headers.map((header, index) => ( + + ))} + + + + {props.rows.map((row, rowIndex) => ( + + {row.map((cell, cellIndex) => ( + + ))} + + ))} + +
+ {header} +
+ {cell === "" ? "-" : cell} +
+
+ ) +} diff --git a/packages/website/src/components/document/docTextSection.tsx b/packages/website/src/components/document/DocTextSection.tsx similarity index 100% rename from packages/website/src/components/document/docTextSection.tsx rename to packages/website/src/components/document/DocTextSection.tsx diff --git a/packages/website/src/components/document/docTip.tsx b/packages/website/src/components/document/DocTip.tsx similarity index 100% rename from packages/website/src/components/document/docTip.tsx rename to packages/website/src/components/document/DocTip.tsx diff --git a/packages/website/src/components/document/docDefinition.tsx b/packages/website/src/components/document/docDefinition.tsx deleted file mode 100644 index 4255dadd..00000000 --- a/packages/website/src/components/document/docDefinition.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconBookmark } from "@tabler/icons-react" -import type { ReactNode } from "react" -import { DocTip } from "./docTip.js" - -export function DocDefinition(props: { term?: string; children: ReactNode }) { - return ( - -
- {props.term && ( -
- {props.term} -
- )} -
- {props.children} -
-
-
- ) -} diff --git a/packages/website/src/components/document/docExample.tsx b/packages/website/src/components/document/docExample.tsx deleted file mode 100644 index 8b2433c8..00000000 --- a/packages/website/src/components/document/docExample.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { IconInfoCircle } from "@tabler/icons-react" -import { DocTip } from "./docTip.js" - -export function DocExample(props: { title?: string; children: React.ReactNode }) { - return ( - - {props.children} - - ) -} diff --git a/packages/website/src/components/document/docGlossaryEntry.tsx b/packages/website/src/components/document/docGlossaryEntry.tsx deleted file mode 100644 index 7612bb1b..00000000 --- a/packages/website/src/components/document/docGlossaryEntry.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { ReactNode } from "react" -import { DocDefinition } from "./docDefinition.js" - -export function DocGlossaryEntry(props: { term: string; children: ReactNode }) { - const id = props.term - .toLowerCase() - .normalize("NFD") - .replace(/[\u0300-\u036f]/g, "") - .replace(/[^a-z0-9]+/g, "-") - .replace(/(^-|-$)/g, "") - - return ( -
- {props.children} -
- ) -} diff --git a/packages/website/src/components/document/docIndexLink.tsx b/packages/website/src/components/document/docIndexLink.tsx deleted file mode 100644 index 4d749e33..00000000 --- a/packages/website/src/components/document/docIndexLink.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" -import type { ValidRoutes } from "../../routes/applicationRouter.js" -import { LinkButton } from "../linkButton.js" - -export function DocIndexLink(props: { to: ValidRoutes; label: string }) { - return ( - - - {props.label} - - ) -} diff --git a/packages/website/src/components/document/docLink.tsx b/packages/website/src/components/document/docLink.tsx deleted file mode 100644 index 67b26399..00000000 --- a/packages/website/src/components/document/docLink.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" -import type { ValidParams, ValidRoutes } from "../../routes/applicationRouter.js" -import { LinkButton } from "../linkButton.js" - -export function DocLink(props: { to: ValidRoutes; params?: ValidParams; hash?: string; children: React.ReactNode }) { - return ( - - {props.children} - - ) -} diff --git a/packages/website/src/components/document/docLinkCard.tsx b/packages/website/src/components/document/docLinkCard.tsx deleted file mode 100644 index 60dc102d..00000000 --- a/packages/website/src/components/document/docLinkCard.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" -import type { ReactNode } from "react" -import type { ValidRoutes } from "../../routes/applicationRouter.js" -import { LinkButton } from "../linkButton.js" - -export type DocLinkCardColor = "information" | "success" | "primary" | "warning" - -export function DocLinkCard(props: { - icon: ReactNode - iconColor?: DocLinkCardColor - title: string - description: string - to: ValidRoutes -}) { - const colorStyles: Record< - DocLinkCardColor, - { - bg: string - color: string - } - > = { - information: { - bg: "information/10", - color: "information", - }, - success: { - bg: "success/10", - color: "success", - }, - primary: { - bg: "primary/10", - color: "primary", - }, - warning: { - bg: "warning/10", - color: "warning", - }, - } - const style = colorStyles[props.iconColor ?? "primary"] - - return ( - -
- {props.icon} -
-

- {props.title} -

-

- {props.description} -

-
- ) -} diff --git a/packages/website/src/components/document/docNextPage.tsx b/packages/website/src/components/document/docNextPage.tsx deleted file mode 100644 index f6fc2987..00000000 --- a/packages/website/src/components/document/docNextPage.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { ButtonOutlineContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconChevronRight } from "@tabler/icons-react" -import type { ValidParams, ValidRoutes } from "../../routes/applicationRouter.js" -import { LinkButton } from "../linkButton.js" - -export function DocNextPage(props: { to: ValidRoutes; params?: ValidParams; label: string; description?: string }) { - return ( -
- - Page suivante - - {props.description && ( -

- {props.description} -

- )} - - } - /> - -
- ) -} diff --git a/packages/website/src/components/document/docRouteRequest.tsx b/packages/website/src/components/document/docRouteRequest.tsx deleted file mode 100644 index 64b1a46a..00000000 --- a/packages/website/src/components/document/docRouteRequest.tsx +++ /dev/null @@ -1,397 +0,0 @@ -import type { routeDefinition } from "@arrhes/application-metadata/utilities" -import { css } from "@arrhes/ui/utilities/cn.js" -import { useState } from "react" - -type SchemaLike = { - type: string - entries?: Readonly> - wrapped?: SchemaLike - pipe?: readonly SchemaLike[] - item?: SchemaLike - options?: readonly string[] -} - -function getBaseType(schema: SchemaLike): string { - if (schema.type === "optional" || schema.type === "nullable" || schema.type === "non_nullable") { - return getBaseType(schema.wrapped!) - } - if (schema.type === "pipe") { - return getBaseType(schema.pipe![0]!) - } - if (schema.type === "array") { - const itemType = getBaseType(schema.item!) - return `${itemType}[]` - } - if (schema.type === "object") { - return "object" - } - if (schema.type === "picklist") { - return schema.options?.join(" | ") ?? "string" - } - return schema.type -} - -function isOptional(schema: SchemaLike): boolean { - if (schema.type === "optional") return true - if (schema.type === "nullable") return true - if (schema.type === "non_nullable" || schema.type === "pipe") return false - return false -} - -function extractFields(schema: SchemaLike): { - name: string - type: string - required: boolean -}[] { - if (schema.type !== "object" || !schema.entries) return [] - - return Object.entries(schema.entries).map(([name, fieldSchema]) => ({ - name, - type: getBaseType(fieldSchema), - required: !isOptional(fieldSchema), - })) -} - -function extractReturnFields(schema: SchemaLike): - | { - name: string - type: string - }[] - | { - isArray: true - fields: { - name: string - type: string - }[] - } { - if (schema.type === "array" && schema.item) { - const fields = extractFields(schema.item).map(({ name, type }) => ({ - name, - type, - })) - return { - isArray: true, - fields, - } - } - return extractFields(schema).map(({ name, type }) => ({ - name, - type, - })) -} - -function FieldTable(props: { - fields: { - name: string - type: string - required?: boolean - }[] - showRequired: boolean -}) { - if (props.fields.length === 0) { - return ( -
- Aucun champ requis -
- ) - } - - return ( - - - - - - {props.showRequired && ( - - )} - - - - {props.fields.map((field) => ( - - - - {props.showRequired && ( - - )} - - ))} - -
- Nom - - Type - - Requis -
- {field.name} - - {field.type} - - {field.required ? "oui" : "non"} -
- ) -} - -type Tab = "body" | "response" - -export function DocRouteRequest(props: { routeDefinition: ReturnType; description?: string }) { - const [activeTab, setActiveTab] = useState("body") - - const bodyFields = extractFields(props.routeDefinition.schemas.body as SchemaLike) - const returnData = extractReturnFields(props.routeDefinition.schemas.return as SchemaLike) - const isArrayReturn = !Array.isArray(returnData) && returnData.isArray - const returnFields = isArrayReturn - ? returnData.fields - : (returnData as { - name: string - type: string - }[]) - - return ( -
- {/* Header: method + path + headers */} -
-
- - POST - - - {props.routeDefinition.path} - -
-
- - Content-Type: application/json - - - Authorization: Bearer {""} - -
-
- - {/* Description */} - {props.description && ( -
- {props.description} -
- )} - - {/* Tabs */} -
- - -
- - {/* Tab content */} -
- {activeTab === "body" && ( - - )} - {activeTab === "response" && ( - - )} -
-
- ) -} diff --git a/packages/website/src/components/document/docSectionCard.tsx b/packages/website/src/components/document/docSectionCard.tsx deleted file mode 100644 index d93f00d2..00000000 --- a/packages/website/src/components/document/docSectionCard.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { ButtonOutlineContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconChevronRight } from "@tabler/icons-react" -import type { ReactNode } from "react" -import type { ValidRoutes } from "../../routes/applicationRouter.js" -import { LinkButton } from "../linkButton.js" - -export type DocSectionCardColor = "information" | "success" | "primary" | "warning" - -export function DocSectionCard(props: { - icon: ReactNode - iconColor?: DocSectionCardColor - title: string - description: string - links: { - to: ValidRoutes - label: string - }[] - ctaTo: ValidRoutes - ctaLabel: string -}) { - const colorStyles: Record< - DocSectionCardColor, - { - bg: string - color: string - } - > = { - information: { - bg: "information/10", - color: "information", - }, - success: { - bg: "success/10", - color: "success", - }, - primary: { - bg: "primary/10", - color: "primary", - }, - warning: { - bg: "warning/10", - color: "warning", - }, - } - const style = colorStyles[props.iconColor ?? "primary"] - - return ( -
- {/* Icon */} -
- {props.icon} -
- - {/* Title */} -

- {props.title} -

- - {/* Description */} -

- {props.description} -

- - {/* Links */} -
- {props.links.map((link) => ( - - - {link.label} - - ))} -
- - {/* CTA */} - - } - /> - -
- ) -} diff --git a/packages/website/src/components/document/docTable.tsx b/packages/website/src/components/document/docTable.tsx deleted file mode 100644 index 79189543..00000000 --- a/packages/website/src/components/document/docTable.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" - -export function DocTable(props: { headers: string[]; rows: string[][] }) { - return ( -
- - - - {props.headers.map((header, index) => ( - - ))} - - - - {props.rows.map((row, rowIndex) => ( - - {row.map((cell, cellIndex) => ( - - ))} - - ))} - -
- {header} -
- {cell === "" ? "-" : cell} -
-
- ) -} diff --git a/packages/website/src/components/forms/FormControl.tsx b/packages/website/src/components/forms/FormControl.tsx new file mode 100644 index 00000000..dba113fe --- /dev/null +++ b/packages/website/src/components/forms/FormControl.tsx @@ -0,0 +1,31 @@ +import { CircularLoader } from "@arrhes/ui" +import { cloneElement, type HTMLAttributes, isValidElement, type ReactElement, Suspense } from "react" +import { useFormField } from "./useFormField.js" + +type FormControl = HTMLAttributes & { + children?: ReactElement +} + +function Slot({ children, ...slotProps }: FormControl) { + if (!isValidElement(children)) return null + const element = children as ReactElement> + return cloneElement(element, { + ...slotProps, + ...element.props, + }) +} + +export function FormControl(props: FormControl) { + const { error, formItemId, formDescriptionId, formMessageId } = useFormField() + + return ( + }> + + + ) +} diff --git a/packages/website/src/components/forms/formError.tsx b/packages/website/src/components/forms/FormError.tsx similarity index 100% rename from packages/website/src/components/forms/formError.tsx rename to packages/website/src/components/forms/FormError.tsx diff --git a/packages/website/src/components/forms/formField.tsx b/packages/website/src/components/forms/FormField.tsx similarity index 100% rename from packages/website/src/components/forms/formField.tsx rename to packages/website/src/components/forms/FormField.tsx diff --git a/packages/website/src/components/forms/formGroup.tsx b/packages/website/src/components/forms/FormGroup.tsx similarity index 100% rename from packages/website/src/components/forms/formGroup.tsx rename to packages/website/src/components/forms/FormGroup.tsx diff --git a/packages/website/src/components/forms/formItem.tsx b/packages/website/src/components/forms/FormItem.tsx similarity index 100% rename from packages/website/src/components/forms/formItem.tsx rename to packages/website/src/components/forms/FormItem.tsx diff --git a/packages/website/src/components/forms/FormLabel.tsx b/packages/website/src/components/forms/FormLabel.tsx new file mode 100644 index 00000000..612dfc36 --- /dev/null +++ b/packages/website/src/components/forms/FormLabel.tsx @@ -0,0 +1,131 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconInfoSquare } from "@tabler/icons-react" +import type { HTMLAttributes } from "react" +import { TooltipContent } from "../overlays/tooltip/TooltipContent.js" +import { TooltipPortal } from "../overlays/tooltip/tooltipPortal.js" +import { TooltipProvider } from "../overlays/tooltip/tooltipProvider.js" +import { TooltipRoot as Tooltip } from "../overlays/tooltip/tooltipRoot.js" +import { TooltipTrigger } from "../overlays/tooltip/tooltipTrigger.js" +import { useFormField } from "./useFormField.js" + +type FormLabel = { + label: string | undefined + isRequired?: boolean + description?: string | undefined + tooltip?: string | undefined + labelProps?: HTMLAttributes +} + +export function FormLabel(props: FormLabel) { + const { formItemId } = useFormField() + + return ( + + ) +} diff --git a/packages/website/src/components/forms/formRoot.tsx b/packages/website/src/components/forms/FormRoot.tsx similarity index 100% rename from packages/website/src/components/forms/formRoot.tsx rename to packages/website/src/components/forms/FormRoot.tsx diff --git a/packages/website/src/components/forms/formSubmit.tsx b/packages/website/src/components/forms/FormSubmit.tsx similarity index 100% rename from packages/website/src/components/forms/formSubmit.tsx rename to packages/website/src/components/forms/FormSubmit.tsx diff --git a/packages/website/src/components/forms/formControl.tsx b/packages/website/src/components/forms/formControl.tsx deleted file mode 100644 index 77fd024c..00000000 --- a/packages/website/src/components/forms/formControl.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { CircularLoader } from "@arrhes/ui" -import { Slot } from "@radix-ui/react-slot" -import { type ComponentProps, Suspense } from "react" -import { useFormField } from "./useFormField.js" - -type FormControl = ComponentProps - -export function FormControl(props: FormControl) { - const { error, formItemId, formDescriptionId, formMessageId } = useFormField() - - return ( - }> - - - ) -} diff --git a/packages/website/src/components/forms/formLabel.tsx b/packages/website/src/components/forms/formLabel.tsx deleted file mode 100644 index 7f308ccc..00000000 --- a/packages/website/src/components/forms/formLabel.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" -import { Tooltip, TooltipContent, TooltipPortal, TooltipProvider, TooltipTrigger } from "@radix-ui/react-tooltip" -import { IconInfoSquare } from "@tabler/icons-react" -import type { HTMLAttributes } from "react" -import { useFormField } from "./useFormField.js" - -type FormLabel = { - label: string | undefined - isRequired?: boolean - description?: string | undefined - tooltip?: string | undefined - labelProps?: HTMLAttributes -} - -export function FormLabel(props: FormLabel) { - const { formItemId } = useFormField() - - return ( - - ) -} diff --git a/packages/website/src/components/layouts/banner.tsx b/packages/website/src/components/layouts/Banner.tsx similarity index 100% rename from packages/website/src/components/layouts/banner.tsx rename to packages/website/src/components/layouts/Banner.tsx diff --git a/packages/website/src/components/layouts/box.tsx b/packages/website/src/components/layouts/Box.tsx similarity index 100% rename from packages/website/src/components/layouts/box.tsx rename to packages/website/src/components/layouts/Box.tsx diff --git a/packages/website/src/components/layouts/columnVisibilityPopover.tsx b/packages/website/src/components/layouts/ColumnVisibilityPopover.tsx similarity index 100% rename from packages/website/src/components/layouts/columnVisibilityPopover.tsx rename to packages/website/src/components/layouts/ColumnVisibilityPopover.tsx diff --git a/packages/website/src/components/layouts/DataTable.tsx b/packages/website/src/components/layouts/DataTable.tsx new file mode 100644 index 00000000..fcd14878 --- /dev/null +++ b/packages/website/src/components/layouts/DataTable.tsx @@ -0,0 +1,786 @@ +import { Button, ButtonGhostContent, ButtonOutlineContent, CircularLoader, FormatNull, InputCheckbox } from "@arrhes/ui" +import { css, cx } from "@arrhes/ui/utilities/cn.js" +import { + IconChevronDown, + IconChevronLeft, + IconChevronRight, + IconDatabaseOff, + IconSortAscending, + IconSortDescending, +} from "@tabler/icons-react" +import { + type ColumnDef, + type ColumnFiltersState, + type ColumnSizingState, + flexRender, + getCoreRowModel, + getExpandedRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + type Row, + type RowData, + type RowSelectionState, + type SortingState, + useReactTable, + type VisibilityState, +} from "@tanstack/react-table" +import { type ComponentProps, Fragment, type ReactElement, useMemo, useRef, useState } from "react" +import { ColumnVisibilityPopover, type VisibilityColumn } from "./ColumnVisibilityPopover.js" +import { EmptyState } from "./EmptyState.js" +import { type FilterColumn, FilterPopover } from "./FilterPopover.js" +import { SearchBar } from "./SearchBar.js" +import { type SortDirection, SortPopover } from "./SortPopover.js" + +declare module "@tanstack/react-table" { + interface ColumnMeta { + fit?: boolean + } +} + +export function DataTable>(props: { + data: Array + isLoading?: boolean + columns: Array> + pageSize?: number + defaultColumnVisibility?: VisibilityState + onRowClick?: (context: Row) => void + renderSubComponent?: (context: { row: Row }) => ReactElement | null + getRowProps?: (row: Row) => ComponentProps<"tr"> + hideSearchBar?: boolean + enableRowSelection?: boolean | ((row: Row) => boolean) + getRowId?: (row: TData, index: number) => string + selectionActions?: (selectedRows: Array>) => ReactElement | null + resetSelectionTrigger?: unknown + emptyStateProps?: Parameters[0] +}) { + const memoizedData = useMemo( + () => props.data, + [ + props.data, + ], + ) + const [globalFilter, setGlobalFilter] = useState("") + const [sorting, setSorting] = useState([]) + const [columnFilters, setColumnFilters] = useState([]) + const [columnVisibility, setColumnVisibility] = useState(props.defaultColumnVisibility ?? {}) + const [rowSelection, setRowSelection] = useState({}) + const [columnSizingOverrides, setColumnSizingOverrides] = useState({}) + + // Reset selection when the trigger changes (e.g. folder navigation) + const prevResetTriggerRef = useRef(props.resetSelectionTrigger) + if (props.resetSelectionTrigger !== undefined && prevResetTriggerRef.current !== props.resetSelectionTrigger) { + prevResetTriggerRef.current = props.resetSelectionTrigger + if (Object.keys(rowSelection).length > 0) setRowSelection({}) + } + + const selectColumnDef = useMemo>( + () => ({ + id: "__select", + meta: { + fit: true, + }, + enableSorting: false, + enableGlobalFilter: false, + enableHiding: false, + header: ({ table }) => { + const selectedRows = table.getSelectedRowModel().rows + return ( +
+ table.toggleAllRowsSelected(checked)} + onClick={(event) => event.stopPropagation()} + /> + {selectedRows.length > 0 && props.selectionActions?.(selectedRows)} +
+ ) + }, + cell: ({ row }) => + row.getCanSelect() ? ( + row.toggleSelected(checked)} + onClick={(event) => event.stopPropagation()} + /> + ) : null, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + props.selectionActions, + ], + ) + + const memoizedUserColumns = useMemo( + () => + props.columns.map((column) => ({ + ...column, + enableMultiSort: true, + })), + [ + props.columns, + ], + ) + + const allColumns = useMemo( + () => + props.enableRowSelection !== undefined && props.enableRowSelection !== false + ? [ + selectColumnDef, + ...memoizedUserColumns, + ] + : memoizedUserColumns, + [ + props.enableRowSelection, + selectColumnDef, + memoizedUserColumns, + ], + ) + + const autoColumnSizing = useMemo(() => { + const maxWidth = 200 + const minWidth = 80 + const basePadding = 24 + const pixelsPerCharacter = 8 + const sampledRows = memoizedData.slice(0, 200) + const computedSizing: ColumnSizingState = {} + + for (const column of allColumns) { + const columnWithAccessor = column as typeof column & { + accessorKey?: keyof TData | string + accessorFn?: (row: TData, rowIndex: number) => unknown + } + const columnId = + column.id ?? + (typeof columnWithAccessor.accessorKey === "string" ? columnWithAccessor.accessorKey : undefined) + + if (!columnId || column.meta?.fit === true) { + continue + } + + let longestValueLength = typeof column.header === "string" ? column.header.length : 0 + + for (const [rowIndex, row] of sampledRows.entries()) { + let value: unknown = "" + + if (typeof columnWithAccessor.accessorFn === "function") { + value = columnWithAccessor.accessorFn(row, rowIndex) + } else if (typeof columnWithAccessor.accessorKey === "string") { + value = row[columnWithAccessor.accessorKey as keyof TData] + } + + const valueLength = String(value ?? "").length + if (valueLength > longestValueLength) { + longestValueLength = valueLength + } + } + + computedSizing[columnId] = Math.max( + minWidth, + Math.min(maxWidth, Math.ceil(longestValueLength * pixelsPerCharacter + basePadding)), + ) + } + + return computedSizing + }, [ + allColumns, + memoizedData, + ]) + + const columnSizing = useMemo( + () => ({ + ...autoColumnSizing, + ...columnSizingOverrides, + }), + [ + autoColumnSizing, + columnSizingOverrides, + ], + ) + + const table = useReactTable({ + data: memoizedData, + columns: allColumns, + getRowId: props.getRowId, + enableRowSelection: props.enableRowSelection, + getRowCanExpand: () => !!props.renderSubComponent, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getExpandedRowModel: getExpandedRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onGlobalFilterChange: setGlobalFilter, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + onColumnSizingChange: setColumnSizingOverrides, + enableMultiSort: true, + enableColumnResizing: true, + columnResizeMode: "onChange", + defaultColumn: { + minSize: 80, + size: 120, + maxSize: 200, + }, + initialState: { + pagination: { + pageSize: props.pageSize ?? 10, + }, + }, + state: { + globalFilter, + sorting, + columnFilters, + columnVisibility, + rowSelection, + columnSizing, + }, + }) + + if (props.isLoading) + return ( + + ) + const columnCount = table.getFlatHeaders().length + (props.renderSubComponent ? 1 : 0) + if (props.data.length === 0) { + return ( +
+ } + title={props.emptyStateProps?.title ?? "Aucun résultat"} + subtitle={props.emptyStateProps?.subtitle} + /> +
+ ) + } + + return ( +
+ {!props.hideSearchBar && ( +
+ setGlobalFilter(value)} + /> + {(() => { + const filterableColumns: Array = table + .getAllColumns() + .filter((col) => col.getCanFilter() && col.columnDef.header && col.columnDef.header !== " ") + .map((col) => ({ + id: col.id, + header: col.columnDef.header?.toString() ?? "", + })) + + if (filterableColumns.length === 0) return null + + const filterRecord: Record = {} + for (const col of table.getAllColumns()) { + const val = col.getFilterValue() + if (val !== undefined) filterRecord[col.id] = String(val) + } + + return ( + { + table.getColumn(columnId)?.setFilterValue(value) + }} + onClearAll={() => { + for (const col of table.getAllColumns()) { + col.setFilterValue(undefined) + } + }} + /> + ) + })()} + {(() => { + const sortableColumns = table + .getAllColumns() + .filter((col) => col.getCanSort() && col.columnDef.header && col.columnDef.header !== " ") + .map((col) => ({ + id: col.id, + header: col.columnDef.header?.toString() ?? "", + })) + + if (sortableColumns.length === 0) return null + + const currentSorting = table.getState().sorting + + function getSortDirection(columnId: string): SortDirection { + const existing = currentSorting.find((s) => s.id === columnId) + if (!existing) return false + return existing.desc ? "desc" : "asc" + } + + function toggleSort(columnId: string) { + const existing = currentSorting.find((s) => s.id === columnId) + if (!existing) { + table.setSorting([ + ...currentSorting, + { + id: columnId, + desc: false, + }, + ]) + } else if (!existing.desc) { + table.setSorting( + currentSorting.map((s) => + s.id === columnId + ? { + ...s, + desc: true, + } + : s, + ), + ) + } else { + table.setSorting(currentSorting.filter((s) => s.id !== columnId)) + } + } + + return ( + table.setSorting([])} + activeSortCount={currentSorting.length} + /> + ) + })()} + {(() => { + const visibilityColumns: Array = table + .getAllLeafColumns() + .filter((col) => col.columnDef.header && col.columnDef.header !== " ") + .map((col) => ({ + id: col.id, + header: col.columnDef.header?.toString() ?? "", + isVisible: col.getIsVisible(), + canHide: col.getCanHide(), + })) + + const hasHideableColumns = visibilityColumns.some((column) => column.canHide) + if (!hasHideableColumns) return null + + return ( + { + table.getColumn(columnId)?.toggleVisibility(isVisible) + }} + onShowAll={() => { + for (const col of table.getAllLeafColumns()) { + if (!col.getCanHide()) continue + col.toggleVisibility(true) + } + }} + onDisableAll={() => { + for (const col of table.getAllLeafColumns()) { + if (!col.getCanHide()) continue + col.toggleVisibility(false) + } + }} + /> + ) + })()} +
+ )} +
+ + + + {props.renderSubComponent && ( + + ) + })} + + + + {table.getRowModel().rows.length > 0 ? null : ( + + + + )} + {table.getRowModel().rows.map((row) => { + const { + className: rowExtraClassName, + onClick: _rowOnClick, + ...rowExtraProps + } = props.getRowProps?.(row) ?? {} + return ( + + { + event.stopPropagation() + if (!props.onRowClick) return + props.onRowClick(row) + }} + className={cx( + css({ + width: "100%", + borderBottom: "1px solid", + borderBottomColor: "neutral/5", + _last: { + borderBottom: "0", + }, + }), + !props.onRowClick + ? undefined + : css({ + cursor: "pointer", + _hover: { + backgroundColor: "neutral/5", + }, + }), + row.getIsExpanded() + ? css({ + borderBottomColor: "neutral/10", + }) + : undefined, + rowExtraClassName, + )} + > + {props.renderSubComponent && ( + + )} + {row.getVisibleCells().map((cell) => { + const isFit = cell.column.columnDef.meta?.fit === true + const boundedCellSize = Math.min(cell.column.getSize(), 200) + return ( + + ) + })} + + {row.getIsExpanded() && props.renderSubComponent && ( + + + + )} + + ) + })} + +
+ )} + {table.getFlatHeaders().map((header) => { + const isFit = header.column.columnDef.meta?.fit === true + const boundedHeaderSize = Math.min(header.getSize(), 200) + return ( + +
+ {header.column.columnDef.header === undefined ? null : typeof header.column + .columnDef.header === "function" ? ( + flexRender(header.column.columnDef.header, header.getContext()) + ) : ( + + )} +
+ {!isFit && ( +
{ + setColumnSizingOverrides((state) => { + const nextState = { + ...state, + } + delete nextState[header.column.id] + return nextState + }) + }} + onMouseDown={header.getResizeHandler()} + onTouchStart={header.getResizeHandler()} + className={css({ + position: "absolute", + top: 0, + right: 0, + width: "0.5rem", + height: "100%", + cursor: "col-resize", + userSelect: "none", + touchAction: "none", + backgroundColor: "transparent", + transition: "background-color 120ms ease", + _hover: { + backgroundColor: "neutral/10", + }, + })} + /> + )} +
+ +
+
+ +
+
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+ {props.renderSubComponent({ + row, + })} +
+
+ {table.getPageCount() > 1 && ( +
+ + {table.getFilteredRowModel().rows.length} résultat + {table.getFilteredRowModel().rows.length > 1 ? "s" : ""} + +
+ + + Page {table.getState().pagination.pageIndex + 1} sur {table.getPageCount()} + + +
+
+ )} +
+ ) +} diff --git a/packages/website/src/components/layouts/dataWrapper.tsx b/packages/website/src/components/layouts/DataWrapper.tsx similarity index 100% rename from packages/website/src/components/layouts/dataWrapper.tsx rename to packages/website/src/components/layouts/DataWrapper.tsx diff --git a/packages/website/src/components/layouts/emptyState.tsx b/packages/website/src/components/layouts/EmptyState.tsx similarity index 100% rename from packages/website/src/components/layouts/emptyState.tsx rename to packages/website/src/components/layouts/EmptyState.tsx diff --git a/packages/website/src/components/layouts/filterPopover.tsx b/packages/website/src/components/layouts/FilterPopover.tsx similarity index 100% rename from packages/website/src/components/layouts/filterPopover.tsx rename to packages/website/src/components/layouts/FilterPopover.tsx diff --git a/packages/website/src/components/layouts/logo.tsx b/packages/website/src/components/layouts/Logo.tsx similarity index 100% rename from packages/website/src/components/layouts/logo.tsx rename to packages/website/src/components/layouts/Logo.tsx diff --git a/packages/website/src/components/layouts/searchBar.tsx b/packages/website/src/components/layouts/SearchBar.tsx similarity index 100% rename from packages/website/src/components/layouts/searchBar.tsx rename to packages/website/src/components/layouts/SearchBar.tsx diff --git a/packages/website/src/components/layouts/sortPopover.tsx b/packages/website/src/components/layouts/SortPopover.tsx similarity index 100% rename from packages/website/src/components/layouts/sortPopover.tsx rename to packages/website/src/components/layouts/SortPopover.tsx diff --git a/packages/website/src/components/layouts/SubPageContent.tsx b/packages/website/src/components/layouts/SubPageContent.tsx new file mode 100644 index 00000000..bf8919fa --- /dev/null +++ b/packages/website/src/components/layouts/SubPageContent.tsx @@ -0,0 +1,104 @@ +import { Button, ButtonGhostContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import type { Icon, IconProps } from "@tabler/icons-react" +import { type ReactElement, type ReactNode, useState } from "react" + +type SectionItem = { + key: string + label: string + icon?: ReactElement> + content: ReactNode +} + +type Section = { + title?: string + icon?: ReactElement> + items: SectionItem[] +} + +/** State-based tab navigation — no TanStack Router required. */ +export function SubPageContent(props: { + sections: Record + defaultKey?: string + children?: ReactNode +}) { + const allItems = Object.values(props.sections).flatMap((s) => s.items) + const firstKey = props.defaultKey ?? allItems[0]?.key ?? "" + const [activeKey, setActiveKey] = useState(firstKey) + const activeContent = allItems.find((i) => i.key === activeKey)?.content ?? null + + return ( +
+ {/* Tab bar */} +
+
+ {allItems.map((item) => ( + + ))} +
+
+ {props.children} +
+
+ + {/* Active tab content */} +
+ {activeContent} +
+
+ ) +} diff --git a/packages/website/src/components/layouts/SubPageLayout.tsx b/packages/website/src/components/layouts/SubPageLayout.tsx new file mode 100644 index 00000000..4da8c2a8 --- /dev/null +++ b/packages/website/src/components/layouts/SubPageLayout.tsx @@ -0,0 +1,227 @@ +import { Button, ButtonGhostContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { type Icon, IconMenu, type IconProps } from "@tabler/icons-react" +import { Outlet, useMatches, useRouterState } from "@tanstack/react-router" +import { cloneElement, type ReactElement, useState } from "react" +import type { ValidParams, ValidRoutes } from "../../routes/applicationRouter.js" +import { LinkButton } from "../LinkButton.js" + +export function SubPageLayout(props: { + sections: Record< + string, + { + title?: string + icon?: ReactElement> + items: Array<{ + label: string + icon?: ReactElement> + to: ValidRoutes + params: ValidParams + }> + } + > +}) { + const [isMenuOpen, setIsMenuOpen] = useState(false) + + const routeMatches = useMatches() + const currentPath = useRouterState({ + select: (state) => state.matches.at(-1)?.routeId, + }) + + const asideContent = ( +
+ {Object.entries(props.sections).map(([key, section]) => ( +
+ {(section.title || section.icon) && ( +
+ {section.icon && + cloneElement(section.icon, { + size: 14, + className: css({ + stroke: "neutral/40", + }), + })} + {section.title && ( + + {section.title} + + )} +
+ )} +
+ {section.items.map((item) => { + const normalizedTo = (item.to ?? "").replace(/\/+$/, "") + const matchRoute = [ + ...routeMatches, + ] + .reverse() + .find((match) => match.fullPath.replace(/\/+$/, "") === normalizedTo) + const isActive = matchRoute === undefined ? false : currentPath === matchRoute.routeId + + return ( + { + setIsMenuOpen(false) + }} + > + + + ) + })} +
+
+ ))} +
+ ) + + return ( +
+
+ {props.sections === undefined ? null : ( + + )} + {/* Main content */} +
+
+ + {isMenuOpen === false ? null : asideContent} +
+
+ +
+
+
+
+ ) +} diff --git a/packages/website/src/components/layouts/title.tsx b/packages/website/src/components/layouts/TitleComponent.tsx similarity index 100% rename from packages/website/src/components/layouts/title.tsx rename to packages/website/src/components/layouts/TitleComponent.tsx diff --git a/packages/website/src/components/layouts/virtualizer.tsx b/packages/website/src/components/layouts/Virtualizer.tsx similarity index 100% rename from packages/website/src/components/layouts/virtualizer.tsx rename to packages/website/src/components/layouts/Virtualizer.tsx diff --git a/packages/website/src/components/layouts/card/cardContent.tsx b/packages/website/src/components/layouts/card/CardContent.tsx similarity index 100% rename from packages/website/src/components/layouts/card/cardContent.tsx rename to packages/website/src/components/layouts/card/CardContent.tsx diff --git a/packages/website/src/components/layouts/card/cardHeader.tsx b/packages/website/src/components/layouts/card/CardHeader.tsx similarity index 100% rename from packages/website/src/components/layouts/card/cardHeader.tsx rename to packages/website/src/components/layouts/card/CardHeader.tsx diff --git a/packages/website/src/components/layouts/card/cardRoot.tsx b/packages/website/src/components/layouts/card/CardRoot.tsx similarity index 100% rename from packages/website/src/components/layouts/card/cardRoot.tsx rename to packages/website/src/components/layouts/card/CardRoot.tsx diff --git a/packages/website/src/components/layouts/card/card.tsx b/packages/website/src/components/layouts/card/card.tsx index 5a782166..02981b9b 100644 --- a/packages/website/src/components/layouts/card/card.tsx +++ b/packages/website/src/components/layouts/card/card.tsx @@ -1,6 +1,6 @@ -import { CardContent } from "./cardContent.js" -import { CardHeader } from "./cardHeader.js" -import { CardRoot } from "./cardRoot.js" +import { CardContent } from "./CardContent.js" +import { CardHeader } from "./CardHeader.js" +import { CardRoot } from "./CardRoot.js" export const Card = { Root: CardRoot, diff --git a/packages/website/src/components/layouts/commandPalette/CommandPalette.tsx b/packages/website/src/components/layouts/commandPalette/CommandPalette.tsx new file mode 100644 index 00000000..daa3f784 --- /dev/null +++ b/packages/website/src/components/layouts/commandPalette/CommandPalette.tsx @@ -0,0 +1,528 @@ +import { getAllMyOrganizationsRouteDefinition } from "@arrhes/application-metadata/routes" +import { css, cx } from "@arrhes/ui/utilities/cn.js" +import { IconBuilding, IconCalendar, IconSearch, IconUser } from "@tabler/icons-react" +import { cloneElement, useEffect, useMemo, useRef, useState } from "react" +import { createPortal } from "react-dom" +import type { OpenTabArgs } from "../../../contexts/tabs/tabDefinitions.js" +import { useTabs } from "../../../contexts/tabs/useTabs.js" +import { levenshtein } from "../../../utilities/levenshtein.js" +import { useDataFromAPI } from "../../../utilities/useHTTPData.js" +import { + type OrgSearchRoute, + orgSearchRoutes, + type SearchRoute, + userSearchRoutes, + type YearSearchRoute, + yearSearchRoutes, +} from "./searchRoutes.js" + +// ─── Fuzzy filter ───────────────────────────────────────────────────────────── + +function normalize(s: string): string { + return s + .normalize("NFD") + .replace(/\p{Diacritic}/gu, "") + .toLowerCase() +} + +function scoreMatch(query: string, target: string): number { + const q = normalize(query) + const t = normalize(target) + if (t.includes(q)) return 0 + return levenshtein(q, t.slice(0, Math.max(t.length, q.length))) +} + +function filterRoutes< + T extends { + label: string + description?: string + }, +>(query: string, routes: T[], groupLabel?: string): T[] { + if (!query) return routes + const groupScore = groupLabel ? scoreMatch(query, groupLabel) : 99 + return routes + .map((r) => ({ + ...r, + _score: Math.min( + scoreMatch(query, r.label), + r.description ? scoreMatch(query, r.description) : 99, + groupScore, + ), + })) + .filter((r) => r._score <= 3) + .sort((a, b) => a._score - b._score) +} + +// ─── Component ──────────────────────────────────────────────────────────────── + +export function CommandPalette(props: { selectedOrgId: string | null; selectedYearId: string | null }) { + const { selectedOrgId, selectedYearId } = props + const [open, setOpen] = useState(false) + const [query, setQuery] = useState("") + const [selected, setSelected] = useState(0) + const [groupFilter, setGroupFilter] = useState<"user" | "org" | "year" | null>(null) + const { openTab } = useTabs() + const inputRef = useRef(null) + + const orgsData = useDataFromAPI({ + routeDefinition: getAllMyOrganizationsRouteDefinition, + body: {}, + }) + + // Reset on open + useEffect(() => { + if (open) { + setQuery("") + setSelected(0) + setGroupFilter(null) + setTimeout(() => inputRef.current?.focus(), 50) + } + }, [ + open, + ]) + + // Open via custom DOM event + useEffect(() => { + function handleOpen() { + setOpen(true) + } + window.addEventListener("arrhes:open-palette", handleOpen) + return () => window.removeEventListener("arrhes:open-palette", handleOpen) + }, []) + + // Close when a tab is opened + useEffect(() => { + function handleTabOpened() { + setOpen(false) + } + window.addEventListener("arrhes:tab-opened", handleTabOpened) + return () => window.removeEventListener("arrhes:tab-opened", handleTabOpened) + }, []) + + // Ctrl/Cmd+K shortcut + useEffect(() => { + function handleKey(e: KeyboardEvent) { + if ((e.ctrlKey || e.metaKey) && e.key === "k") { + e.preventDefault() + setOpen(true) + } + } + document.addEventListener("keydown", handleKey) + return () => document.removeEventListener("keydown", handleKey) + }, []) + + // ─── Result computation ──────────────────────────────────────────────────── + + const allOrgs = orgsData.data ?? [] + + // User section: static pages + matching orgs (when no org selected) + const filteredUserPages = filterRoutes(query, userSearchRoutes, "Utilisateur") + + const filteredOrgItems: SearchRoute[] = useMemo(() => { + if (!selectedOrgId) { + // Show orgs as navigable items — open their exercices tab. + return filterRoutes( + query, + allOrgs.map((ou) => ({ + label: ou.organization.name, + description: "Organisation", + tabComponent: "exercices", + tabProps: { + idOrganization: ou.organization.id, + }, + })), + "Organisations", + ) + } + // Show org-level sub-routes. + return filterRoutes( + query, + orgSearchRoutes.map( + (r: OrgSearchRoute): SearchRoute => ({ + label: r.label, + description: r.description, + tabComponent: r.tabComponent, + tabProps: { + idOrganization: selectedOrgId, + }, + }), + ), + "Organisation", + ) + }, [ + query, + allOrgs, + selectedOrgId, + ]) + + const filteredYearItems: SearchRoute[] = useMemo(() => { + if (!selectedOrgId || !selectedYearId) return [] + return filterRoutes( + query, + yearSearchRoutes.map( + (r: YearSearchRoute): SearchRoute => ({ + label: r.label, + description: r.description, + tabComponent: r.tabComponent, + tabProps: { + idOrganization: selectedOrgId, + idYear: selectedYearId, + }, + }), + ), + "Exercice", + ) + }, [ + query, + selectedOrgId, + selectedYearId, + ]) + + const orgLabel = selectedOrgId ? "Organisation" : "Organisations" + + const allItems: Array< + SearchRoute & { + group: string + } + > = [ + ...(groupFilter === null || groupFilter === "user" + ? filteredUserPages.map((r) => ({ + ...r, + group: "Utilisateur", + })) + : []), + ...(groupFilter === null || groupFilter === "org" + ? filteredOrgItems.map((r) => ({ + ...r, + group: orgLabel, + })) + : []), + ...(groupFilter === null || groupFilter === "year" + ? filteredYearItems.map((r) => ({ + ...r, + group: "Exercice", + })) + : []), + ] + + function activate(item: SearchRoute) { + setOpen(false) + openTab( + { + component: item.tabComponent, + props: item.tabProps, + } as OpenTabArgs, + { + newTab: true, + }, + ) + } + + function handleKeyDown(e: React.KeyboardEvent) { + if (e.key === "ArrowDown") { + e.preventDefault() + setSelected((s) => Math.min(s + 1, allItems.length - 1)) + } else if (e.key === "ArrowUp") { + e.preventDefault() + setSelected((s) => Math.max(s - 1, 0)) + } else if (e.key === "Enter") { + e.preventDefault() + const item = allItems[selected] + if (item) activate(item) + } else if (e.key === "Escape") { + setOpen(false) + } + } + + // Keep selected index in bounds when filtered list changes + // biome-ignore lint/correctness/useExhaustiveDependencies: intentional trigger deps — reset selected when filters change + useEffect(() => { + setSelected(0) + }, [ + query, + selectedOrgId, + selectedYearId, + ]) + + if (!open) return null + + return createPortal( +
{ + if (e.target === e.currentTarget) setOpen(false) + }} + > +
+ {/* Search input */} +
+ + setQuery(e.target.value)} + placeholder="Rechercher des pages, organisations…" + className={css({ + flex: 1, + fontSize: "0.9375rem", + border: "none", + outline: "none", + background: "transparent", + color: "neutral/900", + _placeholder: { + color: "neutral/400", + }, + })} + /> +
+ + {/* Group filter bar */} +
+ {( + [ + { + key: null, + label: "Tout", + icon: null, + }, + { + key: "user" as const, + label: "Utilisateur", + icon: , + }, + { + key: "org" as const, + label: selectedOrgId ? "Organisation" : "Organisations", + icon: , + }, + ...(selectedOrgId && selectedYearId + ? [ + { + key: "year" as const, + label: "Exercice", + icon: , + }, + ] + : []), + ] as const + ).map((group) => { + const isActive = groupFilter === group.key + return ( + + ) + })} +
+ + {/* Results */} +
+ {allItems.length === 0 && ( +

+ {query ? `Aucun résultat pour « ${query} »` : "Aucun résultat"} +

+ )} + + {allItems.map((item, i) => ( + setSelected(i)} + onClick={() => activate(item)} + /> + ))} +
+
+
, + document.body, + ) +} + +// ─── Sub-components ─────────────────────────────────────────────────────────── + +function ResultRow(props: { + group: string + label: string + description?: string + active: boolean + onMouseEnter: () => void + onClick: () => void +}) { + return ( + + ) +} diff --git a/packages/website/src/components/layouts/commandPalette/ContextSelect.tsx b/packages/website/src/components/layouts/commandPalette/ContextSelect.tsx new file mode 100644 index 00000000..31b26069 --- /dev/null +++ b/packages/website/src/components/layouts/commandPalette/ContextSelect.tsx @@ -0,0 +1,23 @@ +import { InputCombobox } from "@arrhes/ui" + +export function ContextSelect(props: { + placeholder: string + value: string | null + onChange: (v: string | null) => void + options: { + key: string + label: string + }[] + isLoading?: boolean +}) { + return ( + props.onChange(v ?? null)} + options={props.options} + isLoading={props.isLoading} + allowEmpty={true} + /> + ) +} diff --git a/packages/website/src/components/layouts/commandPalette/searchRoutes.ts b/packages/website/src/components/layouts/commandPalette/searchRoutes.ts new file mode 100644 index 00000000..ec602b3f --- /dev/null +++ b/packages/website/src/components/layouts/commandPalette/searchRoutes.ts @@ -0,0 +1,114 @@ +export type SearchRoute = { + label: string + description?: string + /** Registry key for openTab. */ + tabComponent: string + /** Props to pass alongside the component key. */ + tabProps: Record +} + +// ─── User-level routes (always visible) ────────────────────────────────────── + +export const userSearchRoutes: SearchRoute[] = [ + { + label: "Organisations", + description: "Liste de vos organisations", + tabComponent: "organisations", + tabProps: {}, + }, + { + label: "Profil", + description: "Votre profil utilisateur", + tabComponent: "profil", + tabProps: {}, + }, + { + label: "Support", + description: "Centre d'aide et tickets", + tabComponent: "support", + tabProps: {}, + }, + { + label: "Paramètres", + description: "Paramètres de l'application", + tabComponent: "paramètres", + tabProps: {}, + }, +] + +// ─── Organisation-level routes (require idOrganization) ────────────────────── + +export type OrgSearchRoute = { + label: string + description?: string + /** Registry key — props will be augmented with `{ idOrganization }` at call time. */ + tabComponent: string +} + +export const orgSearchRoutes: OrgSearchRoute[] = [ + { + label: "Assistant IA", + description: "Assistant comptable intelligent", + tabComponent: "agent", + }, + { + label: "Exercices", + description: "Années fiscales", + tabComponent: "exercices", + }, + { + label: "Membres", + description: "Utilisateurs de l'organisation", + tabComponent: "membres", + }, + { + label: "Stockage", + description: "Fichiers et documents de l'organisation", + tabComponent: "organisation-stockage", + }, + { + label: "Facturation", + description: "Abonnements et factures", + tabComponent: "organisation-facturation", + }, + { + label: "Paramètres de l'organisation", + description: "Configuration générale", + tabComponent: "organisation-paramètres", + }, + { + label: "API", + description: "Accès API de l'organisation", + tabComponent: "organisation-api", + }, +] + +// ─── Year-level routes (require idOrganization + idYear) ───────────────────── + +export type YearSearchRoute = { + label: string + description?: string + /** Registry key — props will be augmented with `{ idOrganization, idYear }` at call time. */ + tabComponent: string +} + +export const yearSearchRoutes: YearSearchRoute[] = [ + { + label: "Écritures", + description: "Journal comptable de l'exercice", + tabComponent: "exercice-écritures", + }, + { + label: "Documents", + description: "Rapports, bilans et compte de résultat", + tabComponent: "exercice-documents", + }, + { + label: "Paramètres de l'exercice", + description: "Journaux, catégories, plan comptable", + tabComponent: "exercice-paramètres", + }, +] + +/** @deprecated use userSearchRoutes */ +export const staticSearchRoutes = userSearchRoutes diff --git a/packages/website/src/components/layouts/dataBlock/dataBlockContent.tsx b/packages/website/src/components/layouts/dataBlock/DataBlockContent.tsx similarity index 100% rename from packages/website/src/components/layouts/dataBlock/dataBlockContent.tsx rename to packages/website/src/components/layouts/dataBlock/DataBlockContent.tsx diff --git a/packages/website/src/components/layouts/dataBlock/dataBlockHeader.tsx b/packages/website/src/components/layouts/dataBlock/DataBlockHeader.tsx similarity index 100% rename from packages/website/src/components/layouts/dataBlock/dataBlockHeader.tsx rename to packages/website/src/components/layouts/dataBlock/DataBlockHeader.tsx diff --git a/packages/website/src/components/layouts/dataBlock/dataBlockItem.tsx b/packages/website/src/components/layouts/dataBlock/DataBlockItem.tsx similarity index 100% rename from packages/website/src/components/layouts/dataBlock/dataBlockItem.tsx rename to packages/website/src/components/layouts/dataBlock/DataBlockItem.tsx diff --git a/packages/website/src/components/layouts/dataBlock/dataBlockRoot.tsx b/packages/website/src/components/layouts/dataBlock/DataBlockRoot.tsx similarity index 100% rename from packages/website/src/components/layouts/dataBlock/dataBlockRoot.tsx rename to packages/website/src/components/layouts/dataBlock/DataBlockRoot.tsx diff --git a/packages/website/src/components/layouts/dataBlock/dataBlock.tsx b/packages/website/src/components/layouts/dataBlock/dataBlock.tsx index 1a080372..1e86a9ff 100644 --- a/packages/website/src/components/layouts/dataBlock/dataBlock.tsx +++ b/packages/website/src/components/layouts/dataBlock/dataBlock.tsx @@ -1,7 +1,7 @@ -import { DataBlockContent } from "./dataBlockContent.js" -import { DataBlockHeader } from "./dataBlockHeader.js" -import { DataBlockItem } from "./dataBlockItem.js" -import { DataBlockRoot } from "./dataBlockRoot.js" +import { DataBlockContent } from "./DataBlockContent.js" +import { DataBlockHeader } from "./DataBlockHeader.js" +import { DataBlockItem } from "./DataBlockItem.js" +import { DataBlockRoot } from "./DataBlockRoot.js" export const DataBlock = { Root: DataBlockRoot, diff --git a/packages/website/src/components/layouts/dataTable.tsx b/packages/website/src/components/layouts/dataTable.tsx deleted file mode 100644 index a66a3b96..00000000 --- a/packages/website/src/components/layouts/dataTable.tsx +++ /dev/null @@ -1,786 +0,0 @@ -import { Button, ButtonGhostContent, ButtonOutlineContent, CircularLoader, FormatNull, InputCheckbox } from "@arrhes/ui" -import { css, cx } from "@arrhes/ui/utilities/cn.js" -import { - IconChevronDown, - IconChevronLeft, - IconChevronRight, - IconDatabaseOff, - IconSortAscending, - IconSortDescending, -} from "@tabler/icons-react" -import { - type ColumnDef, - type ColumnFiltersState, - type ColumnSizingState, - flexRender, - getCoreRowModel, - getExpandedRowModel, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - type Row, - type RowData, - type RowSelectionState, - type SortingState, - useReactTable, - type VisibilityState, -} from "@tanstack/react-table" -import { type ComponentProps, Fragment, type ReactElement, useMemo, useRef, useState } from "react" -import { ColumnVisibilityPopover, type VisibilityColumn } from "./columnVisibilityPopover.js" -import { EmptyState } from "./emptyState.js" -import { type FilterColumn, FilterPopover } from "./filterPopover.js" -import { SearchBar } from "./searchBar.js" -import { type SortDirection, SortPopover } from "./sortPopover.js" - -declare module "@tanstack/react-table" { - interface ColumnMeta { - fit?: boolean - } -} - -export function DataTable>(props: { - data: Array - isLoading?: boolean - columns: Array> - pageSize?: number - defaultColumnVisibility?: VisibilityState - onRowClick?: (context: Row) => void - renderSubComponent?: (context: { row: Row }) => ReactElement | null - getRowProps?: (row: Row) => ComponentProps<"tr"> - hideSearchBar?: boolean - enableRowSelection?: boolean | ((row: Row) => boolean) - getRowId?: (row: TData, index: number) => string - selectionActions?: (selectedRows: Array>) => ReactElement | null - resetSelectionTrigger?: unknown - emptyStateProps?: Parameters[0] -}) { - const memoizedData = useMemo( - () => props.data, - [ - props.data, - ], - ) - const [globalFilter, setGlobalFilter] = useState("") - const [sorting, setSorting] = useState([]) - const [columnFilters, setColumnFilters] = useState([]) - const [columnVisibility, setColumnVisibility] = useState(props.defaultColumnVisibility ?? {}) - const [rowSelection, setRowSelection] = useState({}) - const [columnSizingOverrides, setColumnSizingOverrides] = useState({}) - - // Reset selection when the trigger changes (e.g. folder navigation) - const prevResetTriggerRef = useRef(props.resetSelectionTrigger) - if (props.resetSelectionTrigger !== undefined && prevResetTriggerRef.current !== props.resetSelectionTrigger) { - prevResetTriggerRef.current = props.resetSelectionTrigger - if (Object.keys(rowSelection).length > 0) setRowSelection({}) - } - - const selectColumnDef = useMemo>( - () => ({ - id: "__select", - meta: { - fit: true, - }, - enableSorting: false, - enableGlobalFilter: false, - enableHiding: false, - header: ({ table }) => { - const selectedRows = table.getSelectedRowModel().rows - return ( -
- table.toggleAllRowsSelected(checked)} - onClick={(event) => event.stopPropagation()} - /> - {selectedRows.length > 0 && props.selectionActions?.(selectedRows)} -
- ) - }, - cell: ({ row }) => - row.getCanSelect() ? ( - row.toggleSelected(checked)} - onClick={(event) => event.stopPropagation()} - /> - ) : null, - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [ - props.selectionActions, - ], - ) - - const memoizedUserColumns = useMemo( - () => - props.columns.map((column) => ({ - ...column, - enableMultiSort: true, - })), - [ - props.columns, - ], - ) - - const allColumns = useMemo( - () => - props.enableRowSelection !== undefined && props.enableRowSelection !== false - ? [ - selectColumnDef, - ...memoizedUserColumns, - ] - : memoizedUserColumns, - [ - props.enableRowSelection, - selectColumnDef, - memoizedUserColumns, - ], - ) - - const autoColumnSizing = useMemo(() => { - const maxWidth = 200 - const minWidth = 80 - const basePadding = 24 - const pixelsPerCharacter = 8 - const sampledRows = memoizedData.slice(0, 200) - const computedSizing: ColumnSizingState = {} - - for (const column of allColumns) { - const columnWithAccessor = column as typeof column & { - accessorKey?: keyof TData | string - accessorFn?: (row: TData, rowIndex: number) => unknown - } - const columnId = - column.id ?? - (typeof columnWithAccessor.accessorKey === "string" ? columnWithAccessor.accessorKey : undefined) - - if (!columnId || column.meta?.fit === true) { - continue - } - - let longestValueLength = typeof column.header === "string" ? column.header.length : 0 - - for (const [rowIndex, row] of sampledRows.entries()) { - let value: unknown = "" - - if (typeof columnWithAccessor.accessorFn === "function") { - value = columnWithAccessor.accessorFn(row, rowIndex) - } else if (typeof columnWithAccessor.accessorKey === "string") { - value = row[columnWithAccessor.accessorKey as keyof TData] - } - - const valueLength = String(value ?? "").length - if (valueLength > longestValueLength) { - longestValueLength = valueLength - } - } - - computedSizing[columnId] = Math.max( - minWidth, - Math.min(maxWidth, Math.ceil(longestValueLength * pixelsPerCharacter + basePadding)), - ) - } - - return computedSizing - }, [ - allColumns, - memoizedData, - ]) - - const columnSizing = useMemo( - () => ({ - ...autoColumnSizing, - ...columnSizingOverrides, - }), - [ - autoColumnSizing, - columnSizingOverrides, - ], - ) - - const table = useReactTable({ - data: memoizedData, - columns: allColumns, - getRowId: props.getRowId, - enableRowSelection: props.enableRowSelection, - getRowCanExpand: () => !!props.renderSubComponent, - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getExpandedRowModel: getExpandedRowModel(), - getSortedRowModel: getSortedRowModel(), - getPaginationRowModel: getPaginationRowModel(), - onGlobalFilterChange: setGlobalFilter, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - onColumnVisibilityChange: setColumnVisibility, - onRowSelectionChange: setRowSelection, - onColumnSizingChange: setColumnSizingOverrides, - enableMultiSort: true, - enableColumnResizing: true, - columnResizeMode: "onChange", - defaultColumn: { - minSize: 80, - size: 120, - maxSize: 200, - }, - initialState: { - pagination: { - pageSize: props.pageSize ?? 10, - }, - }, - state: { - globalFilter, - sorting, - columnFilters, - columnVisibility, - rowSelection, - columnSizing, - }, - }) - - if (props.isLoading) - return ( - - ) - const columnCount = table.getFlatHeaders().length + (props.renderSubComponent ? 1 : 0) - if (props.data.length === 0) { - return ( -
- } - title={props.emptyStateProps?.title ?? "Aucun résultat"} - subtitle={props.emptyStateProps?.subtitle} - /> -
- ) - } - - return ( -
- {!props.hideSearchBar && ( -
- setGlobalFilter(value)} - /> - {(() => { - const filterableColumns: Array = table - .getAllColumns() - .filter((col) => col.getCanFilter() && col.columnDef.header && col.columnDef.header !== " ") - .map((col) => ({ - id: col.id, - header: col.columnDef.header?.toString() ?? "", - })) - - if (filterableColumns.length === 0) return null - - const filterRecord: Record = {} - for (const col of table.getAllColumns()) { - const val = col.getFilterValue() - if (val !== undefined) filterRecord[col.id] = String(val) - } - - return ( - { - table.getColumn(columnId)?.setFilterValue(value) - }} - onClearAll={() => { - for (const col of table.getAllColumns()) { - col.setFilterValue(undefined) - } - }} - /> - ) - })()} - {(() => { - const sortableColumns = table - .getAllColumns() - .filter((col) => col.getCanSort() && col.columnDef.header && col.columnDef.header !== " ") - .map((col) => ({ - id: col.id, - header: col.columnDef.header?.toString() ?? "", - })) - - if (sortableColumns.length === 0) return null - - const currentSorting = table.getState().sorting - - function getSortDirection(columnId: string): SortDirection { - const existing = currentSorting.find((s) => s.id === columnId) - if (!existing) return false - return existing.desc ? "desc" : "asc" - } - - function toggleSort(columnId: string) { - const existing = currentSorting.find((s) => s.id === columnId) - if (!existing) { - table.setSorting([ - ...currentSorting, - { - id: columnId, - desc: false, - }, - ]) - } else if (!existing.desc) { - table.setSorting( - currentSorting.map((s) => - s.id === columnId - ? { - ...s, - desc: true, - } - : s, - ), - ) - } else { - table.setSorting(currentSorting.filter((s) => s.id !== columnId)) - } - } - - return ( - table.setSorting([])} - activeSortCount={currentSorting.length} - /> - ) - })()} - {(() => { - const visibilityColumns: Array = table - .getAllLeafColumns() - .filter((col) => col.columnDef.header && col.columnDef.header !== " ") - .map((col) => ({ - id: col.id, - header: col.columnDef.header?.toString() ?? "", - isVisible: col.getIsVisible(), - canHide: col.getCanHide(), - })) - - const hasHideableColumns = visibilityColumns.some((column) => column.canHide) - if (!hasHideableColumns) return null - - return ( - { - table.getColumn(columnId)?.toggleVisibility(isVisible) - }} - onShowAll={() => { - for (const col of table.getAllLeafColumns()) { - if (!col.getCanHide()) continue - col.toggleVisibility(true) - } - }} - onDisableAll={() => { - for (const col of table.getAllLeafColumns()) { - if (!col.getCanHide()) continue - col.toggleVisibility(false) - } - }} - /> - ) - })()} -
- )} -
- - - - {props.renderSubComponent && ( - - ) - })} - - - - {table.getRowModel().rows.length > 0 ? null : ( - - - - )} - {table.getRowModel().rows.map((row) => { - const { - className: rowExtraClassName, - onClick: _rowOnClick, - ...rowExtraProps - } = props.getRowProps?.(row) ?? {} - return ( - - { - event.stopPropagation() - if (!props.onRowClick) return - props.onRowClick(row) - }} - className={cx( - css({ - width: "100%", - borderBottom: "1px solid", - borderBottomColor: "neutral/5", - _last: { - borderBottom: "0", - }, - }), - !props.onRowClick - ? undefined - : css({ - cursor: "pointer", - _hover: { - backgroundColor: "neutral/5", - }, - }), - row.getIsExpanded() - ? css({ - borderBottomColor: "neutral/10", - }) - : undefined, - rowExtraClassName, - )} - > - {props.renderSubComponent && ( - - )} - {row.getVisibleCells().map((cell) => { - const isFit = cell.column.columnDef.meta?.fit === true - const boundedCellSize = Math.min(cell.column.getSize(), 200) - return ( - - ) - })} - - {row.getIsExpanded() && props.renderSubComponent && ( - - - - )} - - ) - })} - -
- )} - {table.getFlatHeaders().map((header) => { - const isFit = header.column.columnDef.meta?.fit === true - const boundedHeaderSize = Math.min(header.getSize(), 200) - return ( - -
- {header.column.columnDef.header === undefined ? null : typeof header.column - .columnDef.header === "function" ? ( - flexRender(header.column.columnDef.header, header.getContext()) - ) : ( - - )} -
- {!isFit && ( -
{ - setColumnSizingOverrides((state) => { - const nextState = { - ...state, - } - delete nextState[header.column.id] - return nextState - }) - }} - onMouseDown={header.getResizeHandler()} - onTouchStart={header.getResizeHandler()} - className={css({ - position: "absolute", - top: 0, - right: 0, - width: "0.5rem", - height: "100%", - cursor: "col-resize", - userSelect: "none", - touchAction: "none", - backgroundColor: "transparent", - transition: "background-color 120ms ease", - _hover: { - backgroundColor: "neutral/10", - }, - })} - /> - )} -
- -
-
- -
-
-
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
-
- {props.renderSubComponent({ - row, - })} -
-
- {table.getPageCount() > 1 && ( -
- - {table.getFilteredRowModel().rows.length} résultat - {table.getFilteredRowModel().rows.length > 1 ? "s" : ""} - -
- - - Page {table.getState().pagination.pageIndex + 1} sur {table.getPageCount()} - - -
-
- )} -
- ) -} diff --git a/packages/website/src/components/layouts/listTable/ListTableFilterable.tsx b/packages/website/src/components/layouts/listTable/ListTableFilterable.tsx new file mode 100644 index 00000000..63ca1743 --- /dev/null +++ b/packages/website/src/components/layouts/listTable/ListTableFilterable.tsx @@ -0,0 +1,189 @@ +import { InputDebounced, InputText } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { type ReactElement, useMemo, useState } from "react" +import { type FilterColumn, FilterPopover } from "../FilterPopover.js" +import { type SortDirection, SortPopover } from "../SortPopover.js" + +export type ListTableColumn = FilterColumn & { + id: string + header: string + accessor: (item: TItem) => string | number | undefined | null +} + +export function ListTableFilterable(props: { + items: Array + columns: Array> + children: (items: Array) => ReactElement | Array | null +}) { + const [globalFilter, setGlobalFilter] = useState("") + const [columnFilters, setColumnFilters] = useState>({}) + const [sorting, setSorting] = useState< + Array<{ + id: string + desc: boolean + }> + >([]) + + const filteredAndSorted = useMemo(() => { + let result = props.items + + if (globalFilter) { + const lower = globalFilter.toLowerCase() + result = result.filter((item) => + props.columns.some((col) => { + const value = col.accessor(item) + return value !== undefined && value !== null && String(value).toLowerCase().includes(lower) + }), + ) + } + + for (const [columnId, filterValue] of Object.entries(columnFilters)) { + if (!filterValue) continue + const column = props.columns.find((col) => col.id === columnId) + if (!column) continue + const lower = filterValue.toLowerCase() + result = result.filter((item) => { + const value = column.accessor(item) + if (value === undefined || value === null) { + return false + } + + const normalizedValue = String(value).toLowerCase() + + if (column.filterVariant === "combobox") { + return normalizedValue === lower + } + + return normalizedValue.includes(lower) + }) + } + + if (sorting.length > 0) { + result = [ + ...result, + ].sort((a, b) => { + for (const sort of sorting) { + const column = props.columns.find((col) => col.id === sort.id) + if (!column) continue + const aVal = column.accessor(a) + const bVal = column.accessor(b) + const aStr = aVal !== undefined && aVal !== null ? String(aVal) : "" + const bStr = bVal !== undefined && bVal !== null ? String(bVal) : "" + const comparison = aStr.localeCompare(bStr, undefined, { + numeric: true, + }) + if (comparison !== 0) return sort.desc ? -comparison : comparison + } + return 0 + }) + } + + return result + }, [ + props.items, + props.columns, + globalFilter, + columnFilters, + sorting, + ]) + + function setColumnFilter(columnId: string, value: string | undefined) { + setColumnFilters((prev) => { + const next = { + ...prev, + } + if (value) { + next[columnId] = value + } else { + delete next[columnId] + } + return next + }) + } + + function clearAllFilters() { + setColumnFilters({}) + } + + function toggleSort(columnId: string) { + setSorting((prev) => { + const existing = prev.find((s) => s.id === columnId) + if (!existing) + return [ + ...prev, + { + id: columnId, + desc: false, + }, + ] + if (!existing.desc) + return prev.map((s) => + s.id === columnId + ? { + ...s, + desc: true, + } + : s, + ) + return prev.filter((s) => s.id !== columnId) + }) + } + + function getSortDirection(columnId: string): SortDirection { + const existing = sorting.find((s) => s.id === columnId) + if (!existing) return false + return existing.desc ? "desc" : "asc" + } + + function clearAllSorts() { + setSorting([]) + } + + return ( +
+
+ setGlobalFilter(value)} + > + + + + +
+ {props.children(filteredAndSorted)} +
+ ) +} diff --git a/packages/website/src/components/layouts/listTable/listTableRoot.tsx b/packages/website/src/components/layouts/listTable/ListTableRoot.tsx similarity index 100% rename from packages/website/src/components/layouts/listTable/listTableRoot.tsx rename to packages/website/src/components/layouts/listTable/ListTableRoot.tsx diff --git a/packages/website/src/components/layouts/listTable/listTableRow.tsx b/packages/website/src/components/layouts/listTable/ListTableRow.tsx similarity index 100% rename from packages/website/src/components/layouts/listTable/listTableRow.tsx rename to packages/website/src/components/layouts/listTable/ListTableRow.tsx diff --git a/packages/website/src/components/layouts/listTable/listTable.tsx b/packages/website/src/components/layouts/listTable/listTable.tsx index badfc9f5..bc77d62a 100644 --- a/packages/website/src/components/layouts/listTable/listTable.tsx +++ b/packages/website/src/components/layouts/listTable/listTable.tsx @@ -1,6 +1,6 @@ -import { ListTableFilterable } from "./listTableFilterable.js" -import { ListTableRoot } from "./listTableRoot.js" -import { ListTableRow } from "./listTableRow.js" +import { ListTableFilterable } from "./ListTableFilterable.js" +import { ListTableRoot } from "./ListTableRoot.js" +import { ListTableRow } from "./ListTableRow.js" export const ListTable = { Root: ListTableRoot, @@ -8,4 +8,4 @@ export const ListTable = { Filterable: ListTableFilterable, } -export type { ListTableColumn } from "./listTableFilterable.js" +export type { ListTableColumn } from "./ListTableFilterable.js" diff --git a/packages/website/src/components/layouts/listTable/listTableFilterable.tsx b/packages/website/src/components/layouts/listTable/listTableFilterable.tsx deleted file mode 100644 index 78d42546..00000000 --- a/packages/website/src/components/layouts/listTable/listTableFilterable.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import { InputDebounced, InputText } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { type ReactElement, useMemo, useState } from "react" -import { type FilterColumn, FilterPopover } from "../filterPopover.js" -import { type SortDirection, SortPopover } from "../sortPopover.js" - -export type ListTableColumn = FilterColumn & { - id: string - header: string - accessor: (item: TItem) => string | number | undefined | null -} - -export function ListTableFilterable(props: { - items: Array - columns: Array> - children: (items: Array) => ReactElement | Array | null -}) { - const [globalFilter, setGlobalFilter] = useState("") - const [columnFilters, setColumnFilters] = useState>({}) - const [sorting, setSorting] = useState< - Array<{ - id: string - desc: boolean - }> - >([]) - - const filteredAndSorted = useMemo(() => { - let result = props.items - - if (globalFilter) { - const lower = globalFilter.toLowerCase() - result = result.filter((item) => - props.columns.some((col) => { - const value = col.accessor(item) - return value !== undefined && value !== null && String(value).toLowerCase().includes(lower) - }), - ) - } - - for (const [columnId, filterValue] of Object.entries(columnFilters)) { - if (!filterValue) continue - const column = props.columns.find((col) => col.id === columnId) - if (!column) continue - const lower = filterValue.toLowerCase() - result = result.filter((item) => { - const value = column.accessor(item) - if (value === undefined || value === null) { - return false - } - - const normalizedValue = String(value).toLowerCase() - - if (column.filterVariant === "combobox") { - return normalizedValue === lower - } - - return normalizedValue.includes(lower) - }) - } - - if (sorting.length > 0) { - result = [ - ...result, - ].sort((a, b) => { - for (const sort of sorting) { - const column = props.columns.find((col) => col.id === sort.id) - if (!column) continue - const aVal = column.accessor(a) - const bVal = column.accessor(b) - const aStr = aVal !== undefined && aVal !== null ? String(aVal) : "" - const bStr = bVal !== undefined && bVal !== null ? String(bVal) : "" - const comparison = aStr.localeCompare(bStr, undefined, { - numeric: true, - }) - if (comparison !== 0) return sort.desc ? -comparison : comparison - } - return 0 - }) - } - - return result - }, [ - props.items, - props.columns, - globalFilter, - columnFilters, - sorting, - ]) - - function setColumnFilter(columnId: string, value: string | undefined) { - setColumnFilters((prev) => { - const next = { - ...prev, - } - if (value) { - next[columnId] = value - } else { - delete next[columnId] - } - return next - }) - } - - function clearAllFilters() { - setColumnFilters({}) - } - - function toggleSort(columnId: string) { - setSorting((prev) => { - const existing = prev.find((s) => s.id === columnId) - if (!existing) - return [ - ...prev, - { - id: columnId, - desc: false, - }, - ] - if (!existing.desc) - return prev.map((s) => - s.id === columnId - ? { - ...s, - desc: true, - } - : s, - ) - return prev.filter((s) => s.id !== columnId) - }) - } - - function getSortDirection(columnId: string): SortDirection { - const existing = sorting.find((s) => s.id === columnId) - if (!existing) return false - return existing.desc ? "desc" : "asc" - } - - function clearAllSorts() { - setSorting([]) - } - - return ( -
-
- setGlobalFilter(value)} - > - - - - -
- {props.children(filteredAndSorted)} -
- ) -} diff --git a/packages/website/src/components/layouts/page/PageBanners.tsx b/packages/website/src/components/layouts/page/PageBanners.tsx new file mode 100644 index 00000000..ed01838a --- /dev/null +++ b/packages/website/src/components/layouts/page/PageBanners.tsx @@ -0,0 +1,20 @@ +import { css, cx } from "@arrhes/ui/utilities/cn.js" +import type { ComponentProps, ReactNode } from "react" + +export function PageBanners(props: { children: ReactNode; className?: ComponentProps<"div">["className"] }) { + return ( +
+ {props.children} +
+ ) +} diff --git a/packages/website/src/components/layouts/page/pageContent.tsx b/packages/website/src/components/layouts/page/PageContent.tsx similarity index 100% rename from packages/website/src/components/layouts/page/pageContent.tsx rename to packages/website/src/components/layouts/page/PageContent.tsx diff --git a/packages/website/src/components/layouts/page/pageDescription.tsx b/packages/website/src/components/layouts/page/PageDescription.tsx similarity index 100% rename from packages/website/src/components/layouts/page/pageDescription.tsx rename to packages/website/src/components/layouts/page/PageDescription.tsx diff --git a/packages/website/src/components/layouts/page/pageHeader.tsx b/packages/website/src/components/layouts/page/PageHeader.tsx similarity index 100% rename from packages/website/src/components/layouts/page/pageHeader.tsx rename to packages/website/src/components/layouts/page/PageHeader.tsx diff --git a/packages/website/src/components/layouts/page/pageNavigation.tsx b/packages/website/src/components/layouts/page/PageNavigation.tsx similarity index 100% rename from packages/website/src/components/layouts/page/pageNavigation.tsx rename to packages/website/src/components/layouts/page/PageNavigation.tsx diff --git a/packages/website/src/components/layouts/page/pageRoot.tsx b/packages/website/src/components/layouts/page/PageRoot.tsx similarity index 100% rename from packages/website/src/components/layouts/page/pageRoot.tsx rename to packages/website/src/components/layouts/page/PageRoot.tsx diff --git a/packages/website/src/components/layouts/page/pageSeparator.tsx b/packages/website/src/components/layouts/page/PageSeparator.tsx similarity index 100% rename from packages/website/src/components/layouts/page/pageSeparator.tsx rename to packages/website/src/components/layouts/page/PageSeparator.tsx diff --git a/packages/website/src/components/layouts/page/pageTitle.tsx b/packages/website/src/components/layouts/page/PageTitle.tsx similarity index 100% rename from packages/website/src/components/layouts/page/pageTitle.tsx rename to packages/website/src/components/layouts/page/PageTitle.tsx diff --git a/packages/website/src/components/layouts/page/page.tsx b/packages/website/src/components/layouts/page/page.tsx index cd791793..f36780aa 100644 --- a/packages/website/src/components/layouts/page/page.tsx +++ b/packages/website/src/components/layouts/page/page.tsx @@ -1,13 +1,15 @@ -import { PageContent } from "./pageContent.js" -import { PageDescription } from "./pageDescription.js" -import { PageHeader } from "./pageHeader.js" -import { PageNavigation } from "./pageNavigation.js" -import { PageRoot } from "./pageRoot.js" -import { PageSeparator } from "./pageSeparator.js" -import { PageTitle } from "./pageTitle.js" +import { PageBanners } from "./PageBanners.js" +import { PageContent } from "./PageContent.js" +import { PageDescription } from "./PageDescription.js" +import { PageHeader } from "./PageHeader.js" +import { PageNavigation } from "./PageNavigation.js" +import { PageRoot } from "./PageRoot.js" +import { PageSeparator } from "./PageSeparator.js" +import { PageTitle } from "./PageTitle.js" export const Page = { Root: PageRoot, + Banners: PageBanners, Navigation: PageNavigation, Header: PageHeader, Title: PageTitle, diff --git a/packages/website/src/components/layouts/section/sectionItem.tsx b/packages/website/src/components/layouts/section/SectionItem.tsx similarity index 100% rename from packages/website/src/components/layouts/section/sectionItem.tsx rename to packages/website/src/components/layouts/section/SectionItem.tsx diff --git a/packages/website/src/components/layouts/section/sectionRoot.tsx b/packages/website/src/components/layouts/section/SectionRoot.tsx similarity index 100% rename from packages/website/src/components/layouts/section/sectionRoot.tsx rename to packages/website/src/components/layouts/section/SectionRoot.tsx diff --git a/packages/website/src/components/layouts/section/section.tsx b/packages/website/src/components/layouts/section/section.tsx index 4177c020..c510c236 100644 --- a/packages/website/src/components/layouts/section/section.tsx +++ b/packages/website/src/components/layouts/section/section.tsx @@ -1,5 +1,5 @@ -import { SectionItem } from "./sectionItem.js" -import { SectionRoot } from "./sectionRoot.js" +import { SectionItem } from "./SectionItem.js" +import { SectionRoot } from "./SectionRoot.js" export const Section = { Root: SectionRoot, diff --git a/packages/website/src/components/layouts/settingsSection/settingsSectionHeader.tsx b/packages/website/src/components/layouts/settingsSection/SettingsSectionHeader.tsx similarity index 100% rename from packages/website/src/components/layouts/settingsSection/settingsSectionHeader.tsx rename to packages/website/src/components/layouts/settingsSection/SettingsSectionHeader.tsx diff --git a/packages/website/src/components/layouts/settingsSection/settingsSectionRoot.tsx b/packages/website/src/components/layouts/settingsSection/SettingsSectionRoot.tsx similarity index 100% rename from packages/website/src/components/layouts/settingsSection/settingsSectionRoot.tsx rename to packages/website/src/components/layouts/settingsSection/SettingsSectionRoot.tsx diff --git a/packages/website/src/components/layouts/settingsSection/settingsSectionRow.tsx b/packages/website/src/components/layouts/settingsSection/SettingsSectionRow.tsx similarity index 100% rename from packages/website/src/components/layouts/settingsSection/settingsSectionRow.tsx rename to packages/website/src/components/layouts/settingsSection/SettingsSectionRow.tsx diff --git a/packages/website/src/components/layouts/settingsSection/settingsSection.tsx b/packages/website/src/components/layouts/settingsSection/settingsSection.tsx index 2437db58..9a5462ae 100644 --- a/packages/website/src/components/layouts/settingsSection/settingsSection.tsx +++ b/packages/website/src/components/layouts/settingsSection/settingsSection.tsx @@ -1,6 +1,6 @@ -import { SettingsSectionHeader } from "./settingsSectionHeader.js" -import { SettingsSectionRoot } from "./settingsSectionRoot.js" -import { SettingsSectionRow } from "./settingsSectionRow.js" +import { SettingsSectionHeader } from "./SettingsSectionHeader.js" +import { SettingsSectionRoot } from "./SettingsSectionRoot.js" +import { SettingsSectionRow } from "./SettingsSectionRow.js" export const SettingsSection = { Root: SettingsSectionRoot, diff --git a/packages/website/src/components/layouts/subPageLayout.tsx b/packages/website/src/components/layouts/subPageLayout.tsx deleted file mode 100644 index ebf96b3c..00000000 --- a/packages/website/src/components/layouts/subPageLayout.tsx +++ /dev/null @@ -1,227 +0,0 @@ -import { Button, ButtonGhostContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { type Icon, IconMenu, type IconProps } from "@tabler/icons-react" -import { Outlet, useMatches, useRouterState } from "@tanstack/react-router" -import { cloneElement, type ReactElement, useState } from "react" -import type { ValidParams, ValidRoutes } from "../../routes/applicationRouter.js" -import { LinkButton } from "../linkButton.js" - -export function SubPageLayout(props: { - sections: Record< - string, - { - title?: string - icon?: ReactElement> - items: Array<{ - label: string - icon?: ReactElement> - to: ValidRoutes - params: ValidParams - }> - } - > -}) { - const [isMenuOpen, setIsMenuOpen] = useState(false) - - const routeMatches = useMatches() - const currentPath = useRouterState({ - select: (state) => state.matches.at(-1)?.routeId, - }) - - const asideContent = ( -
- {Object.entries(props.sections).map(([key, section]) => ( -
- {(section.title || section.icon) && ( -
- {section.icon && - cloneElement(section.icon, { - size: 14, - className: css({ - stroke: "neutral/40", - }), - })} - {section.title && ( - - {section.title} - - )} -
- )} -
- {section.items.map((item) => { - const normalizedTo = (item.to ?? "").replace(/\/+$/, "") - const matchRoute = [ - ...routeMatches, - ] - .reverse() - .find((match) => match.fullPath.replace(/\/+$/, "") === normalizedTo) - const isActive = matchRoute === undefined ? false : currentPath === matchRoute.routeId - - return ( - { - setIsMenuOpen(false) - }} - > - - - ) - })} -
-
- ))} -
- ) - - return ( -
-
- {props.sections === undefined ? null : ( - - )} - {/* Main content */} -
-
- - {isMenuOpen === false ? null : asideContent} -
-
- -
-
-
-
- ) -} diff --git a/packages/website/src/components/layouts/tab/TabRoot.tsx b/packages/website/src/components/layouts/tab/TabRoot.tsx new file mode 100644 index 00000000..cd5ad068 --- /dev/null +++ b/packages/website/src/components/layouts/tab/TabRoot.tsx @@ -0,0 +1,61 @@ +import { ButtonGhostContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import type { Icon, IconProps } from "@tabler/icons-react" +import { useMatches, useRouterState } from "@tanstack/react-router" +import type { ReactElement } from "react" +import type { ValidParams, ValidRoutes } from "../../../routes/applicationRouter.js" +import { LinkButton } from "../../LinkButton.js" + +export function TabRoot(props: { + tabs: Array<{ + label: string + icon?: ReactElement> + to: ValidRoutes + params: ValidParams + }> +}) { + const routeMatches = useMatches() + const currentPath = useRouterState({ + select: (state) => state.matches.at(-1)?.routeId, + }) + + return ( +
+ {props.tabs.map((tab) => { + const normalizedTo = (tab.to ?? "").replace(/\/+$/, "") + const matchRoute = [ + ...routeMatches, + ] + .reverse() + .find((match) => match.fullPath.replace(/\/+$/, "") === normalizedTo) + const isActive = matchRoute === undefined ? false : currentPath === matchRoute.routeId + + return ( + + + + ) + })} +
+ ) +} diff --git a/packages/website/src/components/layouts/tab/tab.tsx b/packages/website/src/components/layouts/tab/tab.tsx index ad0c864e..15db2ca9 100644 --- a/packages/website/src/components/layouts/tab/tab.tsx +++ b/packages/website/src/components/layouts/tab/tab.tsx @@ -1,4 +1,4 @@ -import { TabRoot } from "./tabRoot.js" +import { TabRoot } from "./TabRoot.js" export const Tab = { Root: TabRoot, diff --git a/packages/website/src/components/layouts/tab/tabRoot.tsx b/packages/website/src/components/layouts/tab/tabRoot.tsx deleted file mode 100644 index 8f55589a..00000000 --- a/packages/website/src/components/layouts/tab/tabRoot.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { ButtonGhostContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import type { Icon, IconProps } from "@tabler/icons-react" -import { useMatches, useRouterState } from "@tanstack/react-router" -import type { ReactElement } from "react" -import type { ValidParams, ValidRoutes } from "../../../routes/applicationRouter.js" -import { LinkButton } from "../../linkButton.js" - -export function TabRoot(props: { - tabs: Array<{ - label: string - icon?: ReactElement> - to: ValidRoutes - params: ValidParams - }> -}) { - const routeMatches = useMatches() - const currentPath = useRouterState({ - select: (state) => state.matches.at(-1)?.routeId, - }) - - return ( -
- {props.tabs.map((tab) => { - const normalizedTo = (tab.to ?? "").replace(/\/+$/, "") - const matchRoute = [ - ...routeMatches, - ] - .reverse() - .find((match) => match.fullPath.replace(/\/+$/, "") === normalizedTo) - const isActive = matchRoute === undefined ? false : currentPath === matchRoute.routeId - - return ( - - - - ) - })} -
- ) -} diff --git a/packages/website/src/components/layouts/tabBar/SplitTabBar.tsx b/packages/website/src/components/layouts/tabBar/SplitTabBar.tsx new file mode 100644 index 00000000..4602baaf --- /dev/null +++ b/packages/website/src/components/layouts/tabBar/SplitTabBar.tsx @@ -0,0 +1,213 @@ +import { Button, ButtonGhostContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconChevronLeft, IconChevronRight, IconX } from "@tabler/icons-react" +import { useState } from "react" +import { type ComponentTab, currentEntry } from "../../../contexts/tabs/tabsContext.js" +import { useTabs } from "../../../contexts/tabs/useTabs.js" + +type Props = { + tabIds: string[] + activeTabId: string + onActivate: (tabId: string) => void + onRemove: (tabId: string) => void + onReorder: (tabId: string, insertBeforeTabId: string | null) => void + onDropFromLeft: (tabId: string, insertBeforeTabId: string | null) => void +} + +export function SplitTabBar({ tabIds, activeTabId, onActivate, onRemove, onReorder, onDropFromLeft }: Props) { + const { tabs, navigateBack, navigateForward } = useTabs() + const [dragOverTabId, setDragOverTabId] = useState(null) + const panelTabs = tabIds.flatMap((id) => { + const t = tabs.find((x) => x.id === id) + return t + ? [ + t, + ] + : [] + }) + + function handleDrop(e: React.DragEvent, insertBeforeTabId: string | null) { + e.preventDefault() + e.stopPropagation() + setDragOverTabId(null) + const fromLeft = e.dataTransfer.types.includes("application/arrhes-left-tab") + const draggedId = fromLeft + ? e.dataTransfer.getData("application/arrhes-left-tab") + : e.dataTransfer.getData("application/arrhes-right-tab") + if (!draggedId) return + if (fromLeft) onDropFromLeft(draggedId, insertBeforeTabId) + else onReorder(draggedId, insertBeforeTabId) + } + + return ( +
{ + if ( + e.dataTransfer.types.includes("application/arrhes-left-tab") || + e.dataTransfer.types.includes("application/arrhes-right-tab") + ) + e.preventDefault() + }} + onDrop={(e) => handleDrop(e, null)} + > + {panelTabs.map((tab, index) => { + const isActive = tab.id === activeTabId + const entry = tab.type === "component" ? currentEntry(tab as ComponentTab) : null + const title = entry + ? entry.title + : ( + tab as { + title: string + } + ).title + const description = entry?.description ?? (tab.type === "panel" ? tab.description : undefined) + return ( +
{ + e.dataTransfer.setData("application/arrhes-right-tab", tab.id) + e.dataTransfer.effectAllowed = "move" + }} + onDragEnd={() => setDragOverTabId(null)} + onDragOver={(e) => { + if ( + e.dataTransfer.types.includes("application/arrhes-left-tab") || + e.dataTransfer.types.includes("application/arrhes-right-tab") + ) { + e.preventDefault() + e.stopPropagation() + const rect = e.currentTarget.getBoundingClientRect() + const insertBeforeTabId = + e.clientX < rect.left + rect.width / 2 ? tab.id : (panelTabs[index + 1]?.id ?? null) + setDragOverTabId(insertBeforeTabId) + } + }} + onDragLeave={(e) => { + if (!e.currentTarget.contains(e.relatedTarget as Node | null)) setDragOverTabId(null) + }} + onDrop={(e) => { + const rect = e.currentTarget.getBoundingClientRect() + const insertBeforeTabId = + e.clientX < rect.left + rect.width / 2 ? tab.id : (panelTabs[index + 1]?.id ?? null) + handleDrop(e, insertBeforeTabId) + }} + onClick={() => onActivate(tab.id)} + > + {tab.type === "component" && ( + <> + + + + )} + + + {title} + + {description && ( + + {description} + + )} + + +
+ ) + })} +
+ ) +} diff --git a/packages/website/src/components/layouts/tabBar/TabBar.tsx b/packages/website/src/components/layouts/tabBar/TabBar.tsx new file mode 100644 index 00000000..bccf3f2e --- /dev/null +++ b/packages/website/src/components/layouts/tabBar/TabBar.tsx @@ -0,0 +1,371 @@ +import { Button, ButtonGhostContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconChevronLeft, IconChevronRight, IconLayoutColumns, IconPlus, IconX } from "@tabler/icons-react" +import { type ReactNode, useState } from "react" +import { type ComponentTab, currentEntry } from "../../../contexts/tabs/tabsContext.js" +import { useTabs } from "../../../contexts/tabs/useTabs.js" +import { ContextMenu } from "../../overlays/contextMenu/contextMenu.js" + +type RightPanelProps = { + tabIds: string[] + activeTabId: string + onActivate: (tabId: string) => void + onRemove: (tabId: string) => void + onReorder: (tabId: string, insertBeforeTabId: string | null) => void + onDropFromLeft: (tabId: string, insertBeforeTabId: string | null) => void +} + +type TabBarProps = { + excludeTabIds?: string[] + rightSlot?: ReactNode + onDropFromRight?: (tabId: string, insertBeforeTabId: string | null) => void + onMergePanels?: () => void + /** When provided, puts the bar in right-panel mode. */ + panel?: RightPanelProps +} + +export function TabBar({ excludeTabIds, rightSlot, onDropFromRight, onMergePanels, panel }: TabBarProps = {}) { + const { + tabs: allTabs, + activeTabId: contextActiveTabId, + activateTab, + closeTab, + navigateBack, + navigateForward, + reorderTabs, + } = useTabs() + + const isRightPanel = panel !== undefined + const dragDataType = isRightPanel ? "application/arrhes-right-tab" : "application/arrhes-left-tab" + const crossDragDataType = isRightPanel ? "application/arrhes-left-tab" : "application/arrhes-right-tab" + + const tabs = isRightPanel + ? panel.tabIds.flatMap((id) => { + const t = allTabs.find((x) => x.id === id) + return t + ? [ + t, + ] + : [] + }) + : allTabs.filter((t) => !excludeTabIds?.includes(t.id)) + + const activeTabId = isRightPanel ? panel.activeTabId : contextActiveTabId + + function onActivateTab(tabId: string) { + if (isRightPanel) panel.onActivate(tabId) + else activateTab(tabId) + } + + function onCloseTab(tabId: string) { + if (isRightPanel) panel.onRemove(tabId) + else closeTab(tabId) + } + + function onReorderTab(tabId: string, insertBeforeTabId: string | null) { + if (isRightPanel) panel.onReorder(tabId, insertBeforeTabId) + else reorderTabs(tabId, insertBeforeTabId) + } + + function onCrossDropTab(tabId: string, insertBeforeTabId: string | null) { + if (isRightPanel) panel.onDropFromLeft(tabId, insertBeforeTabId) + else onDropFromRight?.(tabId, insertBeforeTabId) + } + + const [dragOverTabId, setDragOverTabId] = useState(null) + const [contextMenuTabId, setContextMenuTabId] = useState(null) + const contextTab = contextMenuTabId !== null ? (tabs.find((t) => t.id === contextMenuTabId) ?? null) : null + const contextTabCanGoBack = contextTab?.type === "component" && (contextTab as ComponentTab).historyIndex > 0 + const contextTabCanGoForward = + contextTab?.type === "component" && + (contextTab as ComponentTab).historyIndex < (contextTab as ComponentTab).history.length - 1 + + return ( + { + if (!open) setContextMenuTabId(null) + }} + > + +
setContextMenuTabId(null)} + onDragOver={(e) => { + if ( + e.dataTransfer.types.includes("application/arrhes-left-tab") || + e.dataTransfer.types.includes("application/arrhes-right-tab") + ) + e.preventDefault() + }} + onDrop={(e) => { + const fromSamePanel = e.dataTransfer.types.includes(dragDataType) + const draggedTabId = fromSamePanel + ? e.dataTransfer.getData(dragDataType) + : e.dataTransfer.getData(crossDragDataType) + if (!draggedTabId) return + setDragOverTabId(null) + if (fromSamePanel) onReorderTab(draggedTabId, null) + else onCrossDropTab(draggedTabId, null) + }} + > + {tabs.map((tab, index) => { + const isActive = tab.id === activeTabId + const entry = tab.type === "component" ? currentEntry(tab as ComponentTab) : null + const title = entry + ? entry.title + : ( + tab as { + title: string + } + ).title + const description = entry?.description ?? (tab.type === "panel" ? tab.description : undefined) + return ( +
{ + e.dataTransfer.setData(dragDataType, tab.id) + e.dataTransfer.effectAllowed = "move" + }} + onDragEnd={(e) => { + setDragOverTabId(null) + if (!isRightPanel && e.dataTransfer.dropEffect === "none") { + window.dispatchEvent( + new CustomEvent("arrhes:split-tab", { + detail: { + tabId: tab.id, + }, + }), + ) + } + }} + onDragOver={(e) => { + if ( + e.dataTransfer.types.includes("application/arrhes-left-tab") || + e.dataTransfer.types.includes("application/arrhes-right-tab") + ) { + e.preventDefault() + e.stopPropagation() + const rect = e.currentTarget.getBoundingClientRect() + const insertBeforeTabId = + e.clientX < rect.left + rect.width / 2 + ? tab.id + : (tabs[index + 1]?.id ?? null) + setDragOverTabId(insertBeforeTabId) + } + }} + onDragLeave={(e) => { + if (!e.currentTarget.contains(e.relatedTarget as Node | null)) + setDragOverTabId(null) + }} + onDrop={(e) => { + e.preventDefault() + e.stopPropagation() + setDragOverTabId(null) + const fromSamePanel = e.dataTransfer.types.includes(dragDataType) + const draggedTabId = fromSamePanel + ? e.dataTransfer.getData(dragDataType) + : e.dataTransfer.getData(crossDragDataType) + if (!draggedTabId) return + const rect = e.currentTarget.getBoundingClientRect() + const insertBeforeTabId = + e.clientX < rect.left + rect.width / 2 ? tab.id : (tabs[index + 1]?.id ?? null) + if (fromSamePanel) onReorderTab(draggedTabId, insertBeforeTabId) + else onCrossDropTab(draggedTabId, insertBeforeTabId) + }} + onClick={() => onActivateTab(tab.id)} + onContextMenu={() => setContextMenuTabId(tab.id)} + > + + + {title} + + {description && ( + + {description} + + )} + + +
+ ) + })} + + {!isRightPanel && ( +
+ +
+ )} + + {rightSlot && ( +
+ {rightSlot} +
+ )} +
+
+ + {contextTab ? ( + <> + } + disabled={!contextTabCanGoBack} + onSelect={() => navigateBack(contextTab.id)} + > + Retour + + } + disabled={!contextTabCanGoForward} + onSelect={() => navigateForward(contextTab.id)} + > + Suivant + + + {!isRightPanel && ( + } + onSelect={() => + window.dispatchEvent( + new CustomEvent("arrhes:split-tab", { + detail: { + tabId: contextTab.id, + }, + }), + ) + } + > + Ouvrir en vue divisée + + )} + } + onSelect={() => onCloseTab(contextTab.id)} + > + Fermer l'onglet + + + ) : ( + <> + {tabs.length > 1 && ( + { + const keepId = activeTabId + for (const t of tabs) { + if (t.id !== keepId) onCloseTab(t.id) + } + }} + > + Fermer les autres onglets + + )} + {onMergePanels && ( + } + onSelect={onMergePanels} + > + Fusionner les panneaux + + )} + + )} + +
+ ) +} diff --git a/packages/website/src/components/layouts/tabBar/TabLink.tsx b/packages/website/src/components/layouts/tabBar/TabLink.tsx new file mode 100644 index 00000000..daa89ded --- /dev/null +++ b/packages/website/src/components/layouts/tabBar/TabLink.tsx @@ -0,0 +1,53 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconExternalLink } from "@tabler/icons-react" +import type { ReactNode } from "react" +import type { OpenTabArgs } from "../../../contexts/tabs/tabDefinitions.js" +import { useTabs } from "../../../contexts/tabs/useTabs.js" +import { ContextMenu } from "../../overlays/contextMenu/contextMenu.js" + +type TabLinkProps = { + args: OpenTabArgs + children: ReactNode +} + +/** + * Wraps any clickable element with tab-navigation behaviour: + * - Left-click → navigate (replace current tab, or new tab if Ctrl/Meta held) + * - Right-click → context menu with "Ouvrir" / "Ouvrir dans un nouvel onglet" + * + * The child element should NOT have its own openTab onClick; TabLink provides it. + */ +export function TabLink({ args, children }: TabLinkProps) { + const { openTab } = useTabs() + + return ( + + openTab(args)} + > + {children} + + + } + onClick={() => openTab(args)} + > + Ouvrir + + } + onClick={() => + openTab(args, { + newTab: true, + }) + } + > + Ouvrir dans un nouvel onglet + + + + ) +} diff --git a/packages/website/src/components/layouts/table/tableRoot.tsx b/packages/website/src/components/layouts/table/TableRoot.tsx similarity index 100% rename from packages/website/src/components/layouts/table/tableRoot.tsx rename to packages/website/src/components/layouts/table/TableRoot.tsx diff --git a/packages/website/src/components/layouts/table/table.tsx b/packages/website/src/components/layouts/table/table.tsx index aab6594b..6e78afce 100644 --- a/packages/website/src/components/layouts/table/table.tsx +++ b/packages/website/src/components/layouts/table/table.tsx @@ -1,12 +1,12 @@ -import { FilterPopover } from "../filterPopover.js" -import { SortPopover } from "../sortPopover.js" -import { TableBodyCell } from "./tableBody/tableBodyCell.js" +import { FilterPopover } from "../FilterPopover.js" +import { SortPopover } from "../SortPopover.js" +import { TableRoot } from "./TableRoot.js" +import { TableBodyCell } from "./tableBody/TableBodyCell.js" +import { TableBodyRow } from "./tableBody/TableBodyRow.js" import { TableBodyRoot } from "./tableBody/tableBodyRoot.js" -import { TableBodyRow } from "./tableBody/tableBodyRow.js" -import { TableHeaderCell } from "./tableHeader/tableHeaderCell.js" -import { TableHeaderRoot } from "./tableHeader/tableHeaderRoot.js" -import { TableHeaderRow } from "./tableHeader/tableHeaderRow.js" -import { TableRoot } from "./tableRoot.js" +import { TableHeaderCell } from "./tableHeader/TableHeaderCell.js" +import { TableHeaderRoot } from "./tableHeader/TableHeaderRoot.js" +import { TableHeaderRow } from "./tableHeader/TableHeaderRow.js" export const Table = { Root: TableRoot, diff --git a/packages/website/src/components/layouts/table/tableBody/tableBodyCell.tsx b/packages/website/src/components/layouts/table/tableBody/TableBodyCell.tsx similarity index 100% rename from packages/website/src/components/layouts/table/tableBody/tableBodyCell.tsx rename to packages/website/src/components/layouts/table/tableBody/TableBodyCell.tsx diff --git a/packages/website/src/components/layouts/table/tableBody/tableBodyRow.tsx b/packages/website/src/components/layouts/table/tableBody/TableBodyRow.tsx similarity index 100% rename from packages/website/src/components/layouts/table/tableBody/tableBodyRow.tsx rename to packages/website/src/components/layouts/table/tableBody/TableBodyRow.tsx diff --git a/packages/website/src/components/layouts/table/tableHeader/tableHeaderCell.tsx b/packages/website/src/components/layouts/table/tableHeader/TableHeaderCell.tsx similarity index 100% rename from packages/website/src/components/layouts/table/tableHeader/tableHeaderCell.tsx rename to packages/website/src/components/layouts/table/tableHeader/TableHeaderCell.tsx diff --git a/packages/website/src/components/layouts/table/tableHeader/tableHeaderRoot.tsx b/packages/website/src/components/layouts/table/tableHeader/TableHeaderRoot.tsx similarity index 100% rename from packages/website/src/components/layouts/table/tableHeader/tableHeaderRoot.tsx rename to packages/website/src/components/layouts/table/tableHeader/TableHeaderRoot.tsx diff --git a/packages/website/src/components/layouts/table/tableHeader/tableHeaderRow.tsx b/packages/website/src/components/layouts/table/tableHeader/TableHeaderRow.tsx similarity index 100% rename from packages/website/src/components/layouts/table/tableHeader/tableHeaderRow.tsx rename to packages/website/src/components/layouts/table/tableHeader/TableHeaderRow.tsx diff --git a/packages/website/src/components/overlays/contextMenu/ContextMenuContent.tsx b/packages/website/src/components/overlays/contextMenu/ContextMenuContent.tsx new file mode 100644 index 00000000..6817485b --- /dev/null +++ b/packages/website/src/components/overlays/contextMenu/ContextMenuContent.tsx @@ -0,0 +1,65 @@ +import { css, cx } from "@arrhes/ui/utilities/cn.js" +import { type ComponentPropsWithRef, useEffect, useRef } from "react" +import { createPortal } from "react-dom" +import { useContextMenu } from "./ContextMenuRoot.js" + +export function ContextMenuContent({ children, className, ...props }: ComponentPropsWithRef<"div">) { + const ctx = useContextMenu() + const ref = useRef(null) + + useEffect(() => { + if (!ctx?.open) return + function handleClickOutside(e: MouseEvent) { + if (ref.current && !ref.current.contains(e.target as Node)) { + ctx?.closeMenu() + } + } + function handleKeyDown(e: KeyboardEvent) { + if (e.key === "Escape") ctx?.closeMenu() + } + document.addEventListener("mousedown", handleClickOutside) + document.addEventListener("keydown", handleKeyDown) + return () => { + document.removeEventListener("mousedown", handleClickOutside) + document.removeEventListener("keydown", handleKeyDown) + } + }, [ + ctx?.open, + ctx?.closeMenu, + ]) + + if (!ctx?.open) return null + + return createPortal( +
+ {children} +
, + document.body, + ) +} diff --git a/packages/website/src/components/overlays/contextMenu/ContextMenuItem.tsx b/packages/website/src/components/overlays/contextMenu/ContextMenuItem.tsx new file mode 100644 index 00000000..944bc184 --- /dev/null +++ b/packages/website/src/components/overlays/contextMenu/ContextMenuItem.tsx @@ -0,0 +1,78 @@ +import { css, cx } from "@arrhes/ui/utilities/cn.js" +import type { ComponentPropsWithRef, ReactElement } from "react" +import { useContextMenu } from "./ContextMenuRoot.js" + +type ContextMenuItemProps = ComponentPropsWithRef<"button"> & { + leftIcon?: ReactElement + color?: "default" | "danger" + onSelect?: () => void +} + +export function ContextMenuItem({ + leftIcon, + color = "default", + onSelect, + onClick, + children, + className, + ...props +}: ContextMenuItemProps) { + const ctx = useContextMenu() + return ( + + ) +} diff --git a/packages/website/src/components/overlays/contextMenu/ContextMenuRoot.tsx b/packages/website/src/components/overlays/contextMenu/ContextMenuRoot.tsx new file mode 100644 index 00000000..e1318490 --- /dev/null +++ b/packages/website/src/components/overlays/contextMenu/ContextMenuRoot.tsx @@ -0,0 +1,57 @@ +import { createContext, type ReactNode, useContext, useState } from "react" + +type ContextMenuContextValue = { + open: boolean + position: { + x: number + y: number + } + openMenu: (x: number, y: number) => void + closeMenu: () => void +} + +export const ContextMenuContext = createContext(null) + +export function useContextMenu() { + return useContext(ContextMenuContext) +} + +type ContextMenuRootProps = { + children: ReactNode + onOpenChange?: (open: boolean) => void +} + +export function ContextMenuRoot({ children, onOpenChange }: ContextMenuRootProps) { + const [open, setOpen] = useState(false) + const [position, setPosition] = useState({ + x: 0, + y: 0, + }) + + function openMenu(x: number, y: number) { + setPosition({ + x, + y, + }) + setOpen(true) + onOpenChange?.(true) + } + + function closeMenu() { + setOpen(false) + onOpenChange?.(false) + } + + return ( + + {children} + + ) +} diff --git a/packages/website/src/components/overlays/contextMenu/ContextMenuSeparator.tsx b/packages/website/src/components/overlays/contextMenu/ContextMenuSeparator.tsx new file mode 100644 index 00000000..7b11b9ac --- /dev/null +++ b/packages/website/src/components/overlays/contextMenu/ContextMenuSeparator.tsx @@ -0,0 +1,18 @@ +import { css, cx } from "@arrhes/ui/utilities/cn.js" +import type { ComponentPropsWithRef } from "react" + +export function ContextMenuSeparator({ className, ...props }: ComponentPropsWithRef<"div">) { + return ( +
+ ) +} diff --git a/packages/website/src/components/overlays/contextMenu/ContextMenuTrigger.tsx b/packages/website/src/components/overlays/contextMenu/ContextMenuTrigger.tsx new file mode 100644 index 00000000..99a5a2ec --- /dev/null +++ b/packages/website/src/components/overlays/contextMenu/ContextMenuTrigger.tsx @@ -0,0 +1,35 @@ +import { type ComponentPropsWithRef, cloneElement, isValidElement, type ReactElement } from "react" +import { useContextMenu } from "./ContextMenuRoot.js" + +type ContextMenuTriggerProps = ComponentPropsWithRef<"div"> & { + asChild?: boolean +} + +export function ContextMenuTrigger({ children, asChild, ...props }: ContextMenuTriggerProps) { + const ctx = useContextMenu() + + function handleContextMenu(e: React.MouseEvent) { + e.preventDefault() + ctx?.openMenu(e.clientX, e.clientY) + } + + if (asChild && isValidElement(children)) { + const child = children as ReactElement> + return cloneElement(child, { + ...child.props, + onContextMenu: (e: React.MouseEvent) => { + handleContextMenu(e) + ;(child.props.onContextMenu as ((e: React.MouseEvent) => void) | undefined)?.(e) + }, + }) + } + + return ( +
+ {children} +
+ ) +} diff --git a/packages/website/src/components/overlays/contextMenu/contextMenu.tsx b/packages/website/src/components/overlays/contextMenu/contextMenu.tsx index 5ed73f8d..34c7ee0f 100644 --- a/packages/website/src/components/overlays/contextMenu/contextMenu.tsx +++ b/packages/website/src/components/overlays/contextMenu/contextMenu.tsx @@ -1,8 +1,8 @@ -import { ContextMenuContent } from "./contextMenuContent.js" -import { ContextMenuItem } from "./contextMenuItem.js" -import { ContextMenuRoot } from "./contextMenuRoot.js" -import { ContextMenuSeparator } from "./contextMenuSeparator.js" -import { ContextMenuTrigger } from "./contextMenuTrigger.js" +import { ContextMenuContent } from "./ContextMenuContent.js" +import { ContextMenuItem } from "./ContextMenuItem.js" +import { ContextMenuRoot } from "./ContextMenuRoot.js" +import { ContextMenuSeparator } from "./ContextMenuSeparator.js" +import { ContextMenuTrigger } from "./ContextMenuTrigger.js" export const ContextMenu = { Root: ContextMenuRoot, diff --git a/packages/website/src/components/overlays/contextMenu/contextMenuContent.tsx b/packages/website/src/components/overlays/contextMenu/contextMenuContent.tsx deleted file mode 100644 index 92f7d4ae..00000000 --- a/packages/website/src/components/overlays/contextMenu/contextMenuContent.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { css, cx } from "@arrhes/ui/utilities/cn.js" -import { ContextMenuPortal, ContextMenuContent as RadixContextMenuContent } from "@radix-ui/react-context-menu" -import type { ComponentPropsWithRef } from "react" - -export function ContextMenuContent(props: ComponentPropsWithRef) { - return ( - - - {props.children} - - - ) -} diff --git a/packages/website/src/components/overlays/contextMenu/contextMenuItem.tsx b/packages/website/src/components/overlays/contextMenu/contextMenuItem.tsx deleted file mode 100644 index 841da4ac..00000000 --- a/packages/website/src/components/overlays/contextMenu/contextMenuItem.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { css, cx } from "@arrhes/ui/utilities/cn.js" -import { ContextMenuItem as RadixContextMenuItem } from "@radix-ui/react-context-menu" -import type { ComponentPropsWithRef, ReactElement } from "react" - -export function ContextMenuItem( - props: ComponentPropsWithRef & { - leftIcon?: ReactElement - color?: "default" | "danger" - }, -) { - const { leftIcon, color = "default", ...rest } = props - return ( - - {leftIcon && ( - - {leftIcon} - - )} - {props.children} - - ) -} diff --git a/packages/website/src/components/overlays/contextMenu/contextMenuRoot.tsx b/packages/website/src/components/overlays/contextMenu/contextMenuRoot.tsx deleted file mode 100644 index 26cfc1bc..00000000 --- a/packages/website/src/components/overlays/contextMenu/contextMenuRoot.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { ContextMenu } from "@radix-ui/react-context-menu" -import type { ComponentPropsWithRef } from "react" - -export function ContextMenuRoot(props: ComponentPropsWithRef) { - return ( - - {props.children} - - ) -} diff --git a/packages/website/src/components/overlays/contextMenu/contextMenuSeparator.tsx b/packages/website/src/components/overlays/contextMenu/contextMenuSeparator.tsx deleted file mode 100644 index 8206fa85..00000000 --- a/packages/website/src/components/overlays/contextMenu/contextMenuSeparator.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { css, cx } from "@arrhes/ui/utilities/cn.js" -import { ContextMenuSeparator as RadixContextMenuSeparator } from "@radix-ui/react-context-menu" -import type { ComponentPropsWithRef } from "react" - -export function ContextMenuSeparator(props: ComponentPropsWithRef) { - return ( - - ) -} diff --git a/packages/website/src/components/overlays/contextMenu/contextMenuTrigger.tsx b/packages/website/src/components/overlays/contextMenu/contextMenuTrigger.tsx deleted file mode 100644 index f6a8660c..00000000 --- a/packages/website/src/components/overlays/contextMenu/contextMenuTrigger.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { ContextMenuTrigger as RadixContextMenuTrigger } from "@radix-ui/react-context-menu" -import type { ComponentPropsWithRef } from "react" - -export function ContextMenuTrigger(props: ComponentPropsWithRef) { - return {props.children} -} diff --git a/packages/website/src/components/overlays/dialog/confirmationModal.tsx b/packages/website/src/components/overlays/dialog/confirmationModal.tsx deleted file mode 100644 index 323dd07d..00000000 --- a/packages/website/src/components/overlays/dialog/confirmationModal.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { Button, type ButtonContentProps, ButtonOutlineContent, ButtonPlainContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconAlertTriangle } from "@tabler/icons-react" -import { type ComponentPropsWithRef, type JSX, type ReactElement, useState } from "react" -import { Dialog } from "./dialog.js" - -export function ConfirmationModal(props: { - title: string - description: string | JSX.Element - submitButtonProps: ButtonContentProps - onSubmit: () => Promise - children?: ReactElement> - open?: boolean - onOpenChange?: (open: boolean) => void -}) { - const [internalOpen, setInternalOpen] = useState(false) - const isControlled = props.open !== undefined - const open = isControlled ? props.open : internalOpen - const setOpen = isControlled ? (props.onOpenChange ?? (() => {})) : setInternalOpen - - async function handleSubmit() { - await props.onSubmit() - setOpen(false) - } - - return ( - setOpen(value)} - > - {props.children && ( - { - setOpen(true) - event.preventDefault() - }} - > - {props.children} - - )} - {open === false ? null : ( - - -
- -
- {props.title} -
- - {props.description} - - - - - -
- )} -
- ) -} diff --git a/packages/website/src/components/overlays/dialog/dialog.tsx b/packages/website/src/components/overlays/dialog/dialog.tsx index 7ee93196..b60072bf 100644 --- a/packages/website/src/components/overlays/dialog/dialog.tsx +++ b/packages/website/src/components/overlays/dialog/dialog.tsx @@ -1,23 +1 @@ -import { DialogBody } from "./dialogBody.js" -import { DialogContent } from "./dialogContent.js" -import { DialogDescription } from "./dialogDescription.js" -import { DialogFooter } from "./dialogFooter.js" -import { DialogHeader } from "./dialogHeader.js" -import { DialogOverlay } from "./dialogOverlay.js" -import { DialogPortal } from "./dialogPortal.js" -import { DialogRoot } from "./dialogRoot.js" -import { DialogTitle } from "./dialogTitle.js" -import { DialogTrigger } from "./dialogTrigger.js" - -export const Dialog = { - Content: DialogContent, - Body: DialogBody, - Description: DialogDescription, - Footer: DialogFooter, - Header: DialogHeader, - Overlay: DialogOverlay, - Portal: DialogPortal, - Root: DialogRoot, - Title: DialogTitle, - Trigger: DialogTrigger, -} +export { Dialog } from "@arrhes/ui" diff --git a/packages/website/src/components/overlays/dialog/dialogBody.tsx b/packages/website/src/components/overlays/dialog/dialogBody.tsx deleted file mode 100644 index 9aec886c..00000000 --- a/packages/website/src/components/overlays/dialog/dialogBody.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { css, cx } from "@arrhes/ui/utilities/cn.js" -import type { HTMLAttributes } from "react" - -export function DialogBody(props: HTMLAttributes) { - return ( -
- {props.children} -
- ) -} diff --git a/packages/website/src/components/overlays/dialog/dialogContent.tsx b/packages/website/src/components/overlays/dialog/dialogContent.tsx deleted file mode 100644 index 341d8113..00000000 --- a/packages/website/src/components/overlays/dialog/dialogContent.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { css, cx } from "@arrhes/ui/utilities/cn.js" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import type { ComponentPropsWithRef } from "react" -import { DialogOverlay } from "./dialogOverlay.js" -import { DialogPortal } from "./dialogPortal.js" - -export function DialogContent(props: ComponentPropsWithRef) { - return ( - - - - {props.children} - - - - ) -} diff --git a/packages/website/src/components/overlays/dialog/dialogDescription.tsx b/packages/website/src/components/overlays/dialog/dialogDescription.tsx deleted file mode 100644 index f256a431..00000000 --- a/packages/website/src/components/overlays/dialog/dialogDescription.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { css, cx } from "@arrhes/ui/utilities/cn.js" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import type { ComponentPropsWithRef } from "react" - -export function DialogDescription(props: ComponentPropsWithRef) { - return ( - - ) -} diff --git a/packages/website/src/components/overlays/dialog/dialogFooter.tsx b/packages/website/src/components/overlays/dialog/dialogFooter.tsx deleted file mode 100644 index f6a4ae2b..00000000 --- a/packages/website/src/components/overlays/dialog/dialogFooter.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { css, cx } from "@arrhes/ui/utilities/cn.js" -import type { HTMLAttributes } from "react" - -export function DialogFooter(props: HTMLAttributes) { - return ( -
- ) -} diff --git a/packages/website/src/components/overlays/dialog/dialogHeader.tsx b/packages/website/src/components/overlays/dialog/dialogHeader.tsx deleted file mode 100644 index d8faa516..00000000 --- a/packages/website/src/components/overlays/dialog/dialogHeader.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { ButtonGhostContent } from "@arrhes/ui" -import { css, cx } from "@arrhes/ui/utilities/cn.js" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import { IconX } from "@tabler/icons-react" -import type { HTMLAttributes } from "react" - -export function DialogHeader(props: HTMLAttributes) { - return ( -
-
- {props.children} -
- - - -
- ) -} diff --git a/packages/website/src/components/overlays/dialog/dialogOverlay.tsx b/packages/website/src/components/overlays/dialog/dialogOverlay.tsx deleted file mode 100644 index e8ddde47..00000000 --- a/packages/website/src/components/overlays/dialog/dialogOverlay.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { css, cx } from "@arrhes/ui/utilities/cn.js" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import type { ComponentPropsWithRef } from "react" - -export function DialogOverlay(props: ComponentPropsWithRef) { - return ( - - ) -} diff --git a/packages/website/src/components/overlays/dialog/dialogPortal.tsx b/packages/website/src/components/overlays/dialog/dialogPortal.tsx deleted file mode 100644 index 1023b949..00000000 --- a/packages/website/src/components/overlays/dialog/dialogPortal.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import * as DialogPrimitive from "@radix-ui/react-dialog" - -export const DialogPortal = DialogPrimitive.Portal diff --git a/packages/website/src/components/overlays/dialog/dialogRoot.tsx b/packages/website/src/components/overlays/dialog/dialogRoot.tsx deleted file mode 100644 index 367a7f14..00000000 --- a/packages/website/src/components/overlays/dialog/dialogRoot.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import * as DialogPrimitive from "@radix-ui/react-dialog" -import type { ComponentPropsWithRef } from "react" - -export function DialogRoot(props: ComponentPropsWithRef) { - return ( - - {props.children} - - ) -} diff --git a/packages/website/src/components/overlays/dialog/dialogTitle.tsx b/packages/website/src/components/overlays/dialog/dialogTitle.tsx deleted file mode 100644 index 70c123e9..00000000 --- a/packages/website/src/components/overlays/dialog/dialogTitle.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { css, cx } from "@arrhes/ui/utilities/cn.js" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import type { ComponentPropsWithRef } from "react" - -export function DialogTitle(props: ComponentPropsWithRef) { - return ( - - ) -} diff --git a/packages/website/src/components/overlays/dialog/dialogTrigger.tsx b/packages/website/src/components/overlays/dialog/dialogTrigger.tsx deleted file mode 100644 index 85040ceb..00000000 --- a/packages/website/src/components/overlays/dialog/dialogTrigger.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import * as DialogPrimitive from "@radix-ui/react-dialog" - -export const DialogTrigger = DialogPrimitive.Trigger diff --git a/packages/website/src/components/overlays/drawer/drawer.tsx b/packages/website/src/components/overlays/drawer/drawer.tsx deleted file mode 100644 index 34b9d18a..00000000 --- a/packages/website/src/components/overlays/drawer/drawer.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { DrawerBody } from "./drawerBody.js" -import { DrawerContent } from "./drawerContent.js" -import { DrawerHeader } from "./drawerHeader.js" -import { DrawerRoot } from "./drawerRoot.js" -import { DrawerTrigger } from "./drawerTrigger.js" - -export const Drawer = { - Root: DrawerRoot, - Trigger: DrawerTrigger, - Content: DrawerContent, - Header: DrawerHeader, - Body: DrawerBody, -} diff --git a/packages/website/src/components/overlays/drawer/drawerBody.tsx b/packages/website/src/components/overlays/drawer/drawerBody.tsx deleted file mode 100644 index 80ff64f3..00000000 --- a/packages/website/src/components/overlays/drawer/drawerBody.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" -import type { JSX } from "react" - -export function DrawerBody(props: { children: JSX.Element }) { - return ( -
- {props.children} -
- ) -} diff --git a/packages/website/src/components/overlays/drawer/drawerContent.tsx b/packages/website/src/components/overlays/drawer/drawerContent.tsx deleted file mode 100644 index 7d476441..00000000 --- a/packages/website/src/components/overlays/drawer/drawerContent.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { css, cx } from "@arrhes/ui/utilities/cn.js" -import { type JSX, useCallback, useEffect, useRef, useState } from "react" -import { createPortal } from "react-dom" -import { useDrawerContext } from "./drawerRoot.js" - -export function DrawerContent(props: { children: JSX.Element | JSX.Element[]; className?: string }) { - const { open, setOpen } = useDrawerContext() - const [mounted, setMounted] = useState(false) - const [visible, setVisible] = useState(false) - const contentRef = useRef(null) - - // Mount when open becomes true, then trigger visible for animations - useEffect(() => { - if (open) { - setMounted(true) - // Trigger animation on next frame after mount - requestAnimationFrame(() => { - requestAnimationFrame(() => { - setVisible(true) - }) - }) - } else { - setVisible(false) - const timer = setTimeout(() => { - setMounted(false) - }, 200) - return () => clearTimeout(timer) - } - }, [ - open, - ]) - - // Handle Escape key - const handleKeyDown = useCallback( - (e: KeyboardEvent) => { - if (e.key === "Escape") { - setOpen(false) - } - }, - [ - setOpen, - ], - ) - - useEffect(() => { - if (mounted) { - document.addEventListener("keydown", handleKeyDown) - // Prevent body scroll when drawer is open - const originalOverflow = document.body.style.overflow - document.body.style.overflow = "hidden" - return () => { - document.removeEventListener("keydown", handleKeyDown) - document.body.style.overflow = originalOverflow - } - } - }, [ - mounted, - handleKeyDown, - ]) - - if (!mounted) return null - - return createPortal( - <> - {/* Overlay */} -
setOpen(false)} - aria-hidden="true" - /> - {/* Content */} -
- {props.children} -
- , - document.body, - ) -} diff --git a/packages/website/src/components/overlays/drawer/drawerHeader.tsx b/packages/website/src/components/overlays/drawer/drawerHeader.tsx deleted file mode 100644 index 196dd060..00000000 --- a/packages/website/src/components/overlays/drawer/drawerHeader.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Button, ButtonGhostContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconX } from "@tabler/icons-react" -import { useDrawerContext } from "./drawerRoot.js" - -export function DrawerHeader(props: { title: string | undefined }) { - const { setOpen } = useDrawerContext() - - return ( -
- - {props.title === undefined ? null : ( - - {props.title} - - )} - - -
- ) -} diff --git a/packages/website/src/components/overlays/drawer/drawerRoot.tsx b/packages/website/src/components/overlays/drawer/drawerRoot.tsx deleted file mode 100644 index 041867ed..00000000 --- a/packages/website/src/components/overlays/drawer/drawerRoot.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { createContext, type JSX, useContext } from "react" - -type DrawerContextType = { - open: boolean - setOpen: (open: boolean) => void -} - -const DrawerContext = createContext(null) - -export function useDrawerContext() { - const context = useContext(DrawerContext) - if (context === null) { - throw new Error("useDrawerContext must be used within a DrawerRoot") - } - return context -} - -export function DrawerRoot(props: { - open: boolean - onOpenChange: (open: boolean) => void - children: JSX.Element | JSX.Element[] -}) { - return ( - - {props.children} - - ) -} diff --git a/packages/website/src/components/overlays/drawer/drawerTrigger.tsx b/packages/website/src/components/overlays/drawer/drawerTrigger.tsx deleted file mode 100644 index 1dd70762..00000000 --- a/packages/website/src/components/overlays/drawer/drawerTrigger.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Button } from "@arrhes/ui" -import { css, cx } from "@arrhes/ui/utilities/cn.js" -import type { JSX } from "react" -import { useDrawerContext } from "./drawerRoot.js" - -export function DrawerTrigger(props: { children: JSX.Element; className?: string }) { - const { setOpen } = useDrawerContext() - - return ( - - ) -} diff --git a/packages/website/src/components/overlays/popover/PopoverContent.tsx b/packages/website/src/components/overlays/popover/PopoverContent.tsx new file mode 100644 index 00000000..ee23a039 --- /dev/null +++ b/packages/website/src/components/overlays/popover/PopoverContent.tsx @@ -0,0 +1 @@ +export { PopoverContent } from "@arrhes/ui/components/overlays/popover/PopoverContent.js" diff --git a/packages/website/src/components/overlays/popover/popover.tsx b/packages/website/src/components/overlays/popover/popover.tsx index 7135b96e..2da98283 100644 --- a/packages/website/src/components/overlays/popover/popover.tsx +++ b/packages/website/src/components/overlays/popover/popover.tsx @@ -1,5 +1,5 @@ +import { PopoverContent } from "./PopoverContent.js" import { PopoverClose } from "./popoverClose.js" -import { PopoverContent } from "./popoverContent.js" import { PopoverRoot } from "./popoverRoot.js" import { PopoverTrigger } from "./popoverTrigger.js" diff --git a/packages/website/src/components/overlays/popover/popoverClose.tsx b/packages/website/src/components/overlays/popover/popoverClose.tsx index a945fec4..6a88d6e5 100644 --- a/packages/website/src/components/overlays/popover/popoverClose.tsx +++ b/packages/website/src/components/overlays/popover/popoverClose.tsx @@ -1,3 +1 @@ -import * as PopoverPrimitive from "@radix-ui/react-popover" - -export const PopoverClose = PopoverPrimitive.Close +export { PopoverClose } from "@arrhes/ui/components/overlays/popover/popoverClose.js" diff --git a/packages/website/src/components/overlays/popover/popoverContent.tsx b/packages/website/src/components/overlays/popover/popoverContent.tsx deleted file mode 100644 index 9bc4e71a..00000000 --- a/packages/website/src/components/overlays/popover/popoverContent.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { css, cx } from "@arrhes/ui/utilities/cn.js" -import * as PopoverPrimitive from "@radix-ui/react-popover" -import type { ComponentProps } from "react" - -export function PopoverContent(props: ComponentProps) { - return ( - - - - ) -} diff --git a/packages/website/src/components/overlays/popover/popoverRoot.tsx b/packages/website/src/components/overlays/popover/popoverRoot.tsx index 1320adea..917b027a 100644 --- a/packages/website/src/components/overlays/popover/popoverRoot.tsx +++ b/packages/website/src/components/overlays/popover/popoverRoot.tsx @@ -1,3 +1 @@ -import * as PopoverPrimitive from "@radix-ui/react-popover" - -export const PopoverRoot = PopoverPrimitive.Root +export { PopoverRoot } from "@arrhes/ui/components/overlays/popover/popoverRoot.js" diff --git a/packages/website/src/components/overlays/popover/popoverTrigger.tsx b/packages/website/src/components/overlays/popover/popoverTrigger.tsx index d421191f..4d607233 100644 --- a/packages/website/src/components/overlays/popover/popoverTrigger.tsx +++ b/packages/website/src/components/overlays/popover/popoverTrigger.tsx @@ -1,3 +1 @@ -import * as PopoverPrimitive from "@radix-ui/react-popover" - -export const PopoverTrigger = PopoverPrimitive.Trigger +export { PopoverTrigger } from "@arrhes/ui/components/overlays/popover/popoverTrigger.js" diff --git a/packages/website/src/components/overlays/tooltip/TooltipContent.tsx b/packages/website/src/components/overlays/tooltip/TooltipContent.tsx new file mode 100644 index 00000000..724b634b --- /dev/null +++ b/packages/website/src/components/overlays/tooltip/TooltipContent.tsx @@ -0,0 +1,82 @@ +import { css, cx } from "@arrhes/ui/utilities/cn.js" +import { type ComponentPropsWithRef, useLayoutEffect, useRef, useState } from "react" +import { createPortal } from "react-dom" +import { useTooltipRoot } from "./tooltipRoot.js" + +type TooltipContentProps = ComponentPropsWithRef<"div"> & { + sideOffset?: number +} + +export function TooltipContent({ sideOffset = 4, children, className, style, ...props }: TooltipContentProps) { + const ctx = useTooltipRoot() + const ref = useRef(null) + const [pos, setPos] = useState<{ + top: number + left: number + ready: boolean + }>({ + top: 0, + left: 0, + ready: false, + }) + + useLayoutEffect(() => { + if (!ctx?.open) { + setPos((p) => ({ + ...p, + ready: false, + })) + return + } + if (!ref.current || !ctx.triggerRef.current) return + const tr = ctx.triggerRef.current.getBoundingClientRect() + const cr = ref.current.getBoundingClientRect() + setPos({ + top: tr.top - cr.height - sideOffset, + left: Math.max(8, tr.left + tr.width / 2 - cr.width / 2), + ready: true, + }) + }, [ + ctx?.open, + sideOffset, + ctx?.triggerRef?.current?.getBoundingClientRect, + ctx?.triggerRef?.current, + ]) + + if (!ctx?.open) return null + + return createPortal( +
+ + {children} + +
, + document.body, + ) +} diff --git a/packages/website/src/components/overlays/tooltip/tooltip.tsx b/packages/website/src/components/overlays/tooltip/tooltip.tsx index 42f8b55e..3e5a2af9 100644 --- a/packages/website/src/components/overlays/tooltip/tooltip.tsx +++ b/packages/website/src/components/overlays/tooltip/tooltip.tsx @@ -1,4 +1,4 @@ -import { TooltipContent } from "./tooltipContent.js" +import { TooltipContent } from "./TooltipContent.js" import { TooltipPortal } from "./tooltipPortal.js" import { TooltipProvider } from "./tooltipProvider.js" import { TooltipRoot } from "./tooltipRoot.js" diff --git a/packages/website/src/components/overlays/tooltip/tooltipContent.tsx b/packages/website/src/components/overlays/tooltip/tooltipContent.tsx deleted file mode 100644 index 8e946a65..00000000 --- a/packages/website/src/components/overlays/tooltip/tooltipContent.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { css, cx } from "@arrhes/ui/utilities/cn.js" -import * as TooltipPrimitive from "@radix-ui/react-tooltip" -import type { ComponentProps } from "react" - -export function TooltipContent(props: ComponentProps) { - return ( - - - {props.children} - - - ) -} diff --git a/packages/website/src/components/overlays/tooltip/tooltipPortal.tsx b/packages/website/src/components/overlays/tooltip/tooltipPortal.tsx index 75269276..2c8911af 100644 --- a/packages/website/src/components/overlays/tooltip/tooltipPortal.tsx +++ b/packages/website/src/components/overlays/tooltip/tooltipPortal.tsx @@ -1,3 +1,6 @@ -import * as TooltipPrimitive from "@radix-ui/react-tooltip" +import type { ReactNode } from "react" -export const TooltipPortal = TooltipPrimitive.Portal +// Portal is a no-op; TooltipContent handles its own portal rendering +export function TooltipPortal({ children }: { children: ReactNode; container?: Element }) { + return <>{children} +} diff --git a/packages/website/src/components/overlays/tooltip/tooltipProvider.tsx b/packages/website/src/components/overlays/tooltip/tooltipProvider.tsx index d9e93e49..ec85a40e 100644 --- a/packages/website/src/components/overlays/tooltip/tooltipProvider.tsx +++ b/packages/website/src/components/overlays/tooltip/tooltipProvider.tsx @@ -1,3 +1,6 @@ -import * as TooltipPrimitive from "@radix-ui/react-tooltip" +import type { ReactNode } from "react" -export const TooltipProvider = TooltipPrimitive.Provider +// Provider is a no-op wrapper kept for API compatibility +export function TooltipProvider({ children }: { children: ReactNode; delayDuration?: number }) { + return <>{children} +} diff --git a/packages/website/src/components/overlays/tooltip/tooltipRoot.tsx b/packages/website/src/components/overlays/tooltip/tooltipRoot.tsx index 75d0c7e1..a8847fc9 100644 --- a/packages/website/src/components/overlays/tooltip/tooltipRoot.tsx +++ b/packages/website/src/components/overlays/tooltip/tooltipRoot.tsx @@ -1,3 +1,47 @@ -import * as TooltipPrimitive from "@radix-ui/react-tooltip" +import { createContext, type MutableRefObject, type ReactNode, useContext, useRef, useState } from "react" -export const TooltipRoot = TooltipPrimitive.Root +type TooltipRootContextValue = { + open: boolean + openTooltip: () => void + closeTooltip: () => void + triggerRef: MutableRefObject +} + +export const TooltipRootContext = createContext(null) + +export function useTooltipRoot() { + return useContext(TooltipRootContext) +} + +export function TooltipRoot({ children, delayDuration = 700 }: { children: ReactNode; delayDuration?: number }) { + const [open, setOpen] = useState(false) + const triggerRef = useRef(null) + const timerRef = useRef | undefined>(undefined) + + function openTooltip() { + clearTimeout(timerRef.current) + if (delayDuration === 0) { + setOpen(true) + } else { + timerRef.current = setTimeout(() => setOpen(true), delayDuration) + } + } + + function closeTooltip() { + clearTimeout(timerRef.current) + setOpen(false) + } + + return ( + + {children} + + ) +} diff --git a/packages/website/src/components/overlays/tooltip/tooltipTrigger.tsx b/packages/website/src/components/overlays/tooltip/tooltipTrigger.tsx index 7b76ab16..2d5ba272 100644 --- a/packages/website/src/components/overlays/tooltip/tooltipTrigger.tsx +++ b/packages/website/src/components/overlays/tooltip/tooltipTrigger.tsx @@ -1,3 +1,35 @@ -import * as TooltipPrimitive from "@radix-ui/react-tooltip" +import type { ComponentPropsWithRef } from "react" +import { useTooltipRoot } from "./tooltipRoot.js" -export const TooltipTrigger = TooltipPrimitive.Trigger +export function TooltipTrigger({ children, ...props }: ComponentPropsWithRef<"button">) { + const ctx = useTooltipRoot() + return ( + + ) +} diff --git a/packages/website/src/contexts/RootProvider.tsx b/packages/website/src/contexts/RootProvider.tsx new file mode 100644 index 00000000..762fc938 --- /dev/null +++ b/packages/website/src/contexts/RootProvider.tsx @@ -0,0 +1,21 @@ +import { CircularLoader, ModalProvider, PopoverProvider, ToasterProvider } from "@arrhes/ui" +import { Fragment, Suspense } from "react" +import { DataProvider } from "./data/DataProvider.js" +import { RouterProvider } from "./router/RouterProvider.js" + +export function RootProvider() { + return ( + + + + + + }> + + + + + + + ) +} diff --git a/packages/website/src/contexts/dashboard/DashboardContextProvider.tsx b/packages/website/src/contexts/dashboard/DashboardContextProvider.tsx new file mode 100644 index 00000000..e35e61e5 --- /dev/null +++ b/packages/website/src/contexts/dashboard/DashboardContextProvider.tsx @@ -0,0 +1,59 @@ +import type { ReactNode } from "react" +import { useCallback, useState } from "react" +import { setCookie } from "../../utilities/cookies/setCookie.js" +import { cookiePrefix } from "../../utilities/variables.js" +import { DashboardContext } from "./dashboardContext.js" + +const SELECTED_ORG_KEY = "arrhes:context-org" +const SELECTED_YEAR_KEY = "arrhes:context-year" + +function readStorage(key: string): string | null { + try { + return localStorage.getItem(key) + } catch { + return null + } +} + +function writeStorage(key: string, value: string | null): void { + try { + if (value !== null) localStorage.setItem(key, value) + else localStorage.removeItem(key) + } catch {} +} + +export function DashboardContextProvider(props: { children: ReactNode }) { + const [selectedOrgId, setSelectedOrgId] = useState(() => { + const id = readStorage(SELECTED_ORG_KEY) + if (id !== null) setCookie(`${cookiePrefix}_id_organization`, id) + return id + }) + + const [selectedYearId, setSelectedYearId] = useState(() => readStorage(SELECTED_YEAR_KEY)) + + const setOrg = useCallback((id: string | null) => { + setSelectedOrgId(id) + writeStorage(SELECTED_ORG_KEY, id) + if (id !== null) setCookie(`${cookiePrefix}_id_organization`, id) + setSelectedYearId(null) + writeStorage(SELECTED_YEAR_KEY, null) + }, []) + + const setYear = useCallback((id: string | null) => { + setSelectedYearId(id) + writeStorage(SELECTED_YEAR_KEY, id) + }, []) + + return ( + + {props.children} + + ) +} diff --git a/packages/website/src/contexts/dashboard/dashboardContext.tsx b/packages/website/src/contexts/dashboard/dashboardContext.tsx new file mode 100644 index 00000000..fda650c0 --- /dev/null +++ b/packages/website/src/contexts/dashboard/dashboardContext.tsx @@ -0,0 +1,16 @@ +import { createContext, useContext } from "react" + +export type DashboardContextValue = { + selectedOrgId: string | null + selectedYearId: string | null + setOrg: (id: string | null) => void + setYear: (id: string | null) => void +} + +export const DashboardContext = createContext(null) + +export function useDashboardContext(): DashboardContextValue { + const ctx = useContext(DashboardContext) + if (ctx === null) throw new Error("useDashboardContext must be used inside DashboardContextProvider") + return ctx +} diff --git a/packages/website/src/contexts/data/dataProvider.tsx b/packages/website/src/contexts/data/DataProvider.tsx similarity index 100% rename from packages/website/src/contexts/data/dataProvider.tsx rename to packages/website/src/contexts/data/DataProvider.tsx diff --git a/packages/website/src/contexts/rootProvider.tsx b/packages/website/src/contexts/rootProvider.tsx deleted file mode 100644 index 3b0371b3..00000000 --- a/packages/website/src/contexts/rootProvider.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { CircularLoader, ToasterProvider } from "@arrhes/ui" -import { Fragment, Suspense } from "react" -import { DataProvider } from "./data/dataProvider.js" -import { RouterProvider } from "./router/routerProvider.js" - -export function RootProvider() { - return ( - - - - }> - - - - - ) -} diff --git a/packages/website/src/contexts/router/routerProvider.tsx b/packages/website/src/contexts/router/RouterProvider.tsx similarity index 100% rename from packages/website/src/contexts/router/routerProvider.tsx rename to packages/website/src/contexts/router/RouterProvider.tsx diff --git a/packages/website/src/contexts/tabs/TabContentArea.tsx b/packages/website/src/contexts/tabs/TabContentArea.tsx new file mode 100644 index 00000000..4ddc5f27 --- /dev/null +++ b/packages/website/src/contexts/tabs/TabContentArea.tsx @@ -0,0 +1,56 @@ +import type { Tab } from "./tabsContext.js" + +type TabContentAreaProps = { + activeTabId: string | null + tabs: Tab[] +} + +export function TabContentArea({ activeTabId, tabs }: TabContentAreaProps) { + const visibleStyle: React.CSSProperties = { + flex: 1, + minHeight: 0, + display: "flex", + flexDirection: "column", + } + const hiddenStyle: React.CSSProperties = { + display: "none", + } + + return ( + <> + {tabs.map((tab) => { + const isActiveTab = tab.id === activeTabId + + if (tab.type === "panel") { + return ( +
+ {tab.component} +
+ ) + } + + // Component tab: only render if alive. + if (!tab.isAlive) return null + + return ( +
+ {tab.history.map((entry, idx) => ( +
+ {entry.component} +
+ ))} +
+ ) + })} + + ) +} diff --git a/packages/website/src/contexts/tabs/tabDefinitions.ts b/packages/website/src/contexts/tabs/tabDefinitions.ts new file mode 100644 index 00000000..0d88f164 --- /dev/null +++ b/packages/website/src/contexts/tabs/tabDefinitions.ts @@ -0,0 +1,290 @@ +import { createElement } from "react" +import { AgentTabContent } from "../../features/dashboard/$idOrganization/agent/AgentTabContent.js" +import { OrganizationTabContent } from "../../features/dashboard/$idOrganization/OrganizationTabContent.js" +import { OrganizationApiTabContent } from "../../features/dashboard/$idOrganization/organizationApi/OrganizationApiTabContent.js" +import { OrganizationBillingTabContent } from "../../features/dashboard/$idOrganization/organizationBilling/OrganizationBillingTabContent.js" +import { UpdateLicencePage } from "../../features/dashboard/$idOrganization/organizationBilling/UpdateLicencePage.js" +import { UpdateOcrPage } from "../../features/dashboard/$idOrganization/organizationBilling/UpdateOcrPage.js" +import { UpdateStoragePage } from "../../features/dashboard/$idOrganization/organizationBilling/UpdateStoragePage.js" +import { UpdateTokensPage } from "../../features/dashboard/$idOrganization/organizationBilling/UpdateTokensPage.js" +import { WalletTopUpPage } from "../../features/dashboard/$idOrganization/organizationBilling/wallet/WalletTopUpPage.js" +import { WalletWithdrawalPage } from "../../features/dashboard/$idOrganization/organizationBilling/wallet/WalletWithdrawalPage.js" +import { OrganizationSettingsTabContent } from "../../features/dashboard/$idOrganization/organizationSettings/OrganizationSettingsTabContent.js" +import { OrganizationUsersPage } from "../../features/dashboard/$idOrganization/organizationUsers/OrganizationUsersPage.js" +import { YearsPage } from "../../features/dashboard/$idOrganization/years/YearsPage.js" +import { EntryTabContent } from "../../features/dashboard/$idYear/entries/$idEntry/EntryTabContent.js" +import { EntriesPage } from "../../features/dashboard/$idYear/entries/EntriesPage.js" +import { FileTabContent } from "../../features/dashboard/$idYear/files/$idFile/FileTabContent.js" +import { FilesPage } from "../../features/dashboard/$idYear/files/FilesPage.js" +import { ReportsTabContent } from "../../features/dashboard/$idYear/reports/ReportsTabContent.js" +import { YearSettingsTabContent } from "../../features/dashboard/$idYear/yearSettings/YearSettingsTabContent.js" +import { AdminTicketTabContent } from "../../features/dashboard/admin/tickets/$idTicket/AdminTicketTabContent.js" +import { TicketsPage } from "../../features/dashboard/admin/tickets/TicketsPage.js" +import { OrganizationsPage } from "../../features/dashboard/organizations/OrganizationsPage.js" +import { UserProfilePage } from "../../features/dashboard/profile/UserProfilePage.js" +import { SettingsPage } from "../../features/dashboard/settings/SettingsPage.js" +import { TicketTabContent } from "../../features/dashboard/support/$idTicket/TicketTabContent.js" +import { SupportPage } from "../../features/dashboard/support/SupportPage.js" + +// ─── Types ─────────────────────────────────────────────────────────────────── + +export type TabDefinition = { + id: string + title: string + description?: string + component: React.ReactNode +} + +// ─── Registry ──────────────────────────────────────────────────────────────── + +export const TAB_REGISTRY = { + // ─── No-prop tabs ────────────────────────────────────────────────────── + organisations: (_props: Record): TabDefinition => ({ + id: "organisations", + title: "Organisations", + description: "Liste de vos organisations", + component: createElement(OrganizationsPage), + }), + + profil: (_props: Record): TabDefinition => ({ + id: "profil", + title: "Profil", + description: "Votre profil utilisateur", + component: createElement(UserProfilePage), + }), + + support: (_props: Record): TabDefinition => ({ + id: "support", + title: "Support", + description: "Centre d'aide et tickets", + component: createElement(SupportPage), + }), + + "admin-tickets": (_props: Record): TabDefinition => ({ + id: "admin-tickets", + title: "Administration", + description: "Tickets d'administration", + component: createElement(TicketsPage), + }), + + paramètres: (_props: Record): TabDefinition => ({ + id: "paramètres", + title: "Paramètres", + description: "Paramètres de l'application", + component: createElement(SettingsPage), + }), + + // ─── Org-level tabs ──────────────────────────────────────────────────── + + organisation: (props: { idOrganization: string }): TabDefinition => ({ + id: `organisation-${props.idOrganization}`, + title: "Organisation", + description: "Vue d'ensemble de l'organisation", + component: createElement(OrganizationTabContent, { + idOrganization: props.idOrganization, + }), + }), + + exercices: (props: { idOrganization: string }): TabDefinition => ({ + id: `exercices-${props.idOrganization}`, + title: "Exercices", + description: "Années fiscales", + component: createElement(YearsPage, { + idOrganization: props.idOrganization, + }), + }), + + agent: (props: { idOrganization: string }): TabDefinition => ({ + id: `agent-${props.idOrganization}`, + title: "Assistant IA", + description: "Assistant comptable intelligent", + component: createElement(AgentTabContent, { + idOrganization: props.idOrganization, + }), + }), + + membres: (props: { idOrganization: string }): TabDefinition => ({ + id: `membres-${props.idOrganization}`, + title: "Membres", + description: "Utilisateurs de l'organisation", + component: createElement(OrganizationUsersPage, { + idOrganization: props.idOrganization, + }), + }), + + "organisation-stockage": (props: { idOrganization: string }): TabDefinition => ({ + id: `organisation-stockage-${props.idOrganization}`, + title: "Stockage", + description: "Fichiers et documents de l'organisation", + component: createElement(FilesPage), + }), + + "organisation-facturation": (props: { idOrganization: string }): TabDefinition => ({ + id: `organisation-facturation-${props.idOrganization}`, + title: "Facturation", + description: "Abonnements et factures", + component: createElement(OrganizationBillingTabContent, { + idOrganization: props.idOrganization, + }), + }), + + "organisation-paramètres": (props: { idOrganization: string }): TabDefinition => ({ + id: `organisation-paramètres-${props.idOrganization}`, + title: "Paramètres de l'organisation", + description: "Configuration générale", + component: createElement(OrganizationSettingsTabContent, { + idOrganization: props.idOrganization, + }), + }), + + "organisation-api": (props: { idOrganization: string }): TabDefinition => ({ + id: `organisation-api-${props.idOrganization}`, + title: "API", + description: "Accès API de l'organisation", + component: createElement(OrganizationApiTabContent, { + idOrganization: props.idOrganization, + }), + }), + + // ─── Year-level tabs ─────────────────────────────────────────────────── + + "exercice-écritures": (props: { idOrganization: string; idYear: string }): TabDefinition => ({ + id: `exercice-écritures-${props.idOrganization}-${props.idYear}`, + title: "Écritures", + description: "Journal comptable de l'exercice", + component: createElement(EntriesPage, { + idOrganization: props.idOrganization, + idYear: props.idYear, + }), + }), + + "exercice-documents": (props: { idOrganization: string; idYear: string }): TabDefinition => ({ + id: `exercice-documents-${props.idOrganization}-${props.idYear}`, + title: "Documents", + description: "Rapports, bilans et compte de résultat", + component: createElement(ReportsTabContent, { + idOrganization: props.idOrganization, + idYear: props.idYear, + }), + }), + + "exercice-stockage": (props: { idOrganization: string; idYear: string }): TabDefinition => ({ + id: `exercice-stockage-${props.idOrganization}-${props.idYear}`, + title: "Stockage", + description: "Fichiers attachés à l'exercice", + component: createElement(FilesPage), + }), + + "exercice-paramètres": (props: { idOrganization: string; idYear: string }): TabDefinition => ({ + id: `exercice-paramètres-${props.idOrganization}-${props.idYear}`, + title: "Paramètres de l'exercice", + description: "Journaux, catégories, plan comptable", + component: createElement(YearSettingsTabContent, { + idOrganization: props.idOrganization, + idYear: props.idYear, + }), + }), + + // ─── Billing form tabs ───────────────────────────────────────────────── + + "facturation-licence": (props: { idOrganization: string }): TabDefinition => ({ + id: `facturation-licence-${props.idOrganization}`, + title: "Modifier la licence", + description: "Modifier le montant de la licence mensuelle", + component: createElement(UpdateLicencePage, { + idOrganization: props.idOrganization, + }), + }), + + "facturation-stockage": (props: { idOrganization: string }): TabDefinition => ({ + id: `facturation-stockage-${props.idOrganization}`, + title: "Modifier le stockage", + description: "Ajuster la capacité de stockage", + component: createElement(UpdateStoragePage, { + idOrganization: props.idOrganization, + }), + }), + + "facturation-tokens": (props: { idOrganization: string }): TabDefinition => ({ + id: `facturation-tokens-${props.idOrganization}`, + title: "Modifier les tokens IA", + description: "Acheter des tokens pour l'assistant IA", + component: createElement(UpdateTokensPage, { + idOrganization: props.idOrganization, + }), + }), + + "facturation-ocr": (props: { idOrganization: string }): TabDefinition => ({ + id: `facturation-ocr-${props.idOrganization}`, + title: "Ajouter des pages OCR", + description: "Acheter des pages OCR supplémentaires", + component: createElement(UpdateOcrPage, { + idOrganization: props.idOrganization, + }), + }), + + "facturation-recharge": (props: { idOrganization: string }): TabDefinition => ({ + id: `facturation-recharge-${props.idOrganization}`, + title: "Recharger le portefeuille", + description: "Ajouter des fonds au portefeuille", + component: createElement(WalletTopUpPage, { + idOrganization: props.idOrganization, + }), + }), + + "facturation-retrait": (props: { idOrganization: string }): TabDefinition => ({ + id: `facturation-retrait-${props.idOrganization}`, + title: "Retirer du portefeuille", + description: "Retirer des fonds du portefeuille", + component: createElement(WalletWithdrawalPage, { + idOrganization: props.idOrganization, + }), + }), + + // ─── Detail tabs ─────────────────────────────────────────────────────── + + écriture: (props: { idOrganization: string; idYear: string; idEntry: string; label?: string }): TabDefinition => ({ + id: `écriture-${props.idOrganization}-${props.idYear}-${props.idEntry}`, + title: "Écriture", + description: props.label, + component: createElement(EntryTabContent, { + idOrganization: props.idOrganization, + idYear: props.idYear, + idEntry: props.idEntry, + }), + }), + + ticket: (props: { idTicket: string }): TabDefinition => ({ + id: `ticket-${props.idTicket}`, + title: "Ticket", + component: createElement(TicketTabContent, { + idTicket: props.idTicket, + }), + }), + + "admin-ticket": (props: { idTicket: string }): TabDefinition => ({ + id: `admin-ticket-${props.idTicket}`, + title: "Ticket", + component: createElement(AdminTicketTabContent, { + idTicket: props.idTicket, + }), + }), + + fichier: (props: { idOrganization: string; idFile: string }): TabDefinition => ({ + id: `fichier-${props.idOrganization}-${props.idFile}`, + title: "Fichier", + component: createElement(FileTabContent, { + idOrganization: props.idOrganization, + idFile: props.idFile, + }), + }), +} + +// ─── Open tab args (discriminated union derived from registry) ──────────────── + +export type OpenTabArgs = { + [K in keyof typeof TAB_REGISTRY]: { + component: K + props: Parameters<(typeof TAB_REGISTRY)[K]>[0] + } +}[keyof typeof TAB_REGISTRY] diff --git a/packages/website/src/contexts/tabs/tabsContext.tsx b/packages/website/src/contexts/tabs/tabsContext.tsx new file mode 100644 index 00000000..b9ca007a --- /dev/null +++ b/packages/website/src/contexts/tabs/tabsContext.tsx @@ -0,0 +1,82 @@ +import { createContext } from "react" +import type { OpenTabArgs } from "./tabDefinitions.js" + +// ─── History ───────────────────────────────────────────────────────────────── + +/** + * One entry in a tab's back/forward history stack. + * `component` is null when the entry has never been visited (or was evicted) + * and will be (re)created from the registry on first access. + */ +export type HistoryEntry = { + /** Stable random ID for this entry, used to build the browser URL. */ + id: string + definitionKey: string + definitionProps: Record + title: string + description?: string + /** The rendered React node. null = not yet mounted or evicted. */ + component: React.ReactNode +} + +// ─── Tab Types ─────────────────────────────────────────────────────────────── + +export type ComponentTab = { + id: string + type: "component" + /** All visited (and future-accessible) entries for this tab. */ + history: HistoryEntry[] + /** Index into `history` that is currently visible. */ + historyIndex: number + /** + * Whether this tab's current history entry is mounted in the DOM. + * When false all `entry.component` values are null (LRU-evicted). + */ + isAlive: boolean +} + +export type PanelTab = { + id: string + type: "panel" + title: string + description?: string + icon?: string + component: React.ReactNode // never serialized +} + +export type Tab = ComponentTab | PanelTab + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +/** Returns the currently-active history entry of a ComponentTab. */ +export function currentEntry(tab: ComponentTab): HistoryEntry { + return tab.history[tab.historyIndex] +} + +// ─── Options ───────────────────────────────────────────────────────────────── + +export type OpenTabOptions = { + /** + * When true, always create (or activate an existing) separate tab + * instead of replacing the current tab's history. + * Also forced when the user holds Ctrl / Meta during the click. + */ + newTab?: boolean +} + +// ─── Actions ───────────────────────────────────────────────────────────────── + +export type TabsContextValue = { + tabs: Tab[] + activeTabId: string | null + openTab: (args: OpenTabArgs, options?: OpenTabOptions) => void + closeTab: (id: string) => void + activateTab: (id: string) => void + navigateBack: (tabId: string) => void + navigateForward: (tabId: string) => void + openPanelTab: (title: string, component: React.ReactNode, description?: string, icon?: string) => string + updateTabTitle: (id: string, title: string) => void + reorderTabs: (tabId: string, beforeTabId: string | null) => void +} + +export const TabsContext = createContext(null) diff --git a/packages/website/src/contexts/tabs/tabsProvider.tsx b/packages/website/src/contexts/tabs/tabsProvider.tsx new file mode 100644 index 00000000..57f69e85 --- /dev/null +++ b/packages/website/src/contexts/tabs/tabsProvider.tsx @@ -0,0 +1,754 @@ +import { generateId } from "@arrhes/application-metadata" +import { useCallback, useEffect, useMemo, useRef, useState } from "react" +import { applicationRouter } from "../../routes/applicationRouter.js" +import { setCookie } from "../../utilities/cookies/setCookie.js" +import { cookiePrefix } from "../../utilities/variables.js" +import { type OpenTabArgs, TAB_REGISTRY, type TabDefinition } from "./tabDefinitions.js" +import { + type ComponentTab, + currentEntry, + type HistoryEntry, + type OpenTabOptions, + type PanelTab, + type Tab, + TabsContext, + type TabsContextValue, +} from "./tabsContext.js" +import { loadPersistedTabs, normalisePersisted, type PersistedHistoryEntry, savePersistedTabs } from "./tabsStorage.js" +import { OuterRouterProvider } from "./useOuterRouter.js" + +// Maximum number of component tabs kept alive in React memory at any time. +const MAX_ALIVE_TABS = 5 + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +function buildEntry(definitionKey: string, definitionProps: Record, id?: string): HistoryEntry { + const registry = TAB_REGISTRY as Record TabDefinition> + const def = registry[definitionKey]?.(definitionProps) + return { + id: id ?? generateId(), + definitionKey, + definitionProps, + title: def?.title ?? "", + description: def?.description, + component: def?.component ?? null, + } +} + +/** + * Returns a new tabs array where at most MAX_ALIVE_TABS component tabs have + * isAlive = true. The `newlyActivatedId` is guaranteed to stay alive; + * older tabs beyond the limit are evicted (isAlive = false, all history + * component nodes cleared). + */ +function applyLruEviction(tabs: Tab[], newlyActivatedId: string): Tab[] { + const aliveComponentTabs = tabs.filter((t): t is ComponentTab => t.type === "component" && t.isAlive) + + if (aliveComponentTabs.length <= MAX_ALIVE_TABS) return tabs + + const toEvict = aliveComponentTabs + .filter((t) => t.id !== newlyActivatedId) + .slice(0, aliveComponentTabs.length - MAX_ALIVE_TABS) + + const evictIds = new Set(toEvict.map((t) => t.id)) + + return tabs.map((t) => { + if (t.type === "component" && evictIds.has(t.id)) { + return { + ...t, + isAlive: false, + // Free all component nodes across history. + history: t.history.map((e) => ({ + ...e, + component: null, + })), + } + } + return t + }) +} + +// ─── Provider ──────────────────────────────────────────────────────────────── + +type Props = { + children: React.ReactNode +} + +export function TabsProvider({ children }: Props) { + const [tabs, setTabs] = useState(() => { + const raw = loadPersistedTabs() + const persisted = raw ? normalisePersisted(raw) : null + if (persisted && persisted.tabs.length > 0) { + return persisted.tabs.flatMap((t) => { + if (!t.history || t.history.length === 0) return [] + + const history: HistoryEntry[] = t.history.map((e, idx) => { + // Only build the component for the currently-visible entry. + if (idx === (t.historyIndex ?? 0)) { + return buildEntry(e.definitionKey, e.definitionProps ?? {}, e.id) + } + return { + id: e.id, + definitionKey: e.definitionKey, + definitionProps: e.definitionProps ?? {}, + title: e.title, + description: e.description, + component: null, + } + }) + + const tab: ComponentTab = { + id: t.id, + type: "component", + history, + historyIndex: t.historyIndex ?? 0, + isAlive: true, + } + return [ + tab, + ] + }) + } + return [] + }) + + const [activeTabId, setActiveTabId] = useState(() => { + const persisted = loadPersistedTabs() + if (persisted?.activeTabId) return persisted.activeTabId + return null + }) + + // Ordered list of recently focused tab IDs (most recent last). + // Used to determine which tab to activate after closing the active one. + const focusHistoryRef = useRef([]) + + // Track whether Ctrl / Meta is currently held so openTab can decide + // whether to replace-in-place or create a new tab — without requiring + // every call site to pass a flag. + const ctrlKeyRef = useRef(false) + useEffect(() => { + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === "Control" || e.key === "Meta") ctrlKeyRef.current = true + } + const onKeyUp = (e: KeyboardEvent) => { + if (e.key === "Control" || e.key === "Meta") ctrlKeyRef.current = false + } + window.addEventListener("keydown", onKeyDown) + window.addEventListener("keyup", onKeyUp) + // Safety: reset if window loses focus. + window.addEventListener("blur", () => { + ctrlKeyRef.current = false + }) + return () => { + window.removeEventListener("keydown", onKeyDown) + window.removeEventListener("keyup", onKeyUp) + } + }, []) + + // After every state change, sync to sessionStorage. + useEffect(() => { + const componentTabs = tabs.filter((t): t is ComponentTab => t.type === "component") + savePersistedTabs({ + activeTabId, + tabs: componentTabs.map((t) => ({ + id: t.id, + type: "component" as const, + history: t.history.map( + (e): PersistedHistoryEntry => ({ + id: e.id, + definitionKey: e.definitionKey, + definitionProps: e.definitionProps, + title: e.title, + description: e.description, + }), + ), + historyIndex: t.historyIndex, + })), + }) + }, [ + tabs, + activeTabId, + ]) + + // Keep a synchronous ref so openTab can read current tabs without a stale closure. + const tabsRef = useRef(tabs) + tabsRef.current = tabs + + // Keep a synchronous ref for activeTabId too. + const activeTabIdRef = useRef(activeTabId) + activeTabIdRef.current = activeTabId + + // Whenever the active tab or its current history entry changes, sync the + // org cookie so that all API calls include the correct X-Organization-Id header. + // This mirrors what organizationPathRoute.beforeLoad did in the TanStack Router flow. + useEffect(() => { + if (activeTabId === null) return + const activeTab = tabs.find((t): t is ComponentTab => t.type === "component" && t.id === activeTabId) + if (!activeTab) return + const idOrganization = currentEntry(activeTab).definitionProps.idOrganization + if (typeof idOrganization === "string" && idOrganization) { + setCookie(`${cookiePrefix}_id_organization`, idOrganization) + } + }, [ + activeTabId, + tabs, + ]) + + // On mount: initialise the browser URL from persisted state so the correct + // /dashboard/:tabId/:historyIndex is shown immediately. + useEffect(() => { + if (activeTabId === null) { + window.history.replaceState({}, "", "/dashboard") + } else { + const tab = tabsRef.current.find((t): t is ComponentTab => t.type === "component" && t.id === activeTabId) + const entryId = tab ? currentEntry(tab).id : "0" + window.history.replaceState( + { + tabId: activeTabId, + entryId, + }, + "", + `/dashboard/${activeTabId}/${entryId}`, + ) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + activeTabId, + ]) + + // React to browser back/forward (mouse buttons, keyboard, browser UI). + // Parse the destination URL/state and update tab context accordingly. + useEffect(() => { + const onPopState = (e: PopStateEvent) => { + const state = e.state as { + tabId?: string + entryId?: string + panelTabId?: string + } | null + + // Panel-tab back/forward: re-activate the panel if it still exists, + // otherwise fall through to URL-based component-tab restoration. + if (state?.panelTabId !== undefined) { + const stillOpen = tabsRef.current.some((t) => t.id === state.panelTabId) + if (stillOpen) { + setActiveTabId(state.panelTabId) + return + } + // Panel was closed — fall through to URL restoration below. + } + + let tabId: string | null = null + let entryId: string | null = null + if (state?.tabId !== undefined && state.entryId !== undefined) { + tabId = state.tabId + entryId = state.entryId + } else { + const m = window.location.pathname.match(/^\/dashboard\/([^/]+)\/([^/]+)$/) + if (m) { + tabId = m[1] + entryId = m[2] + } else { + const s = window.location.pathname.match(/^\/dashboard\/([^/]+)$/) + if (s) tabId = s[1] + } + } + if (tabId === null) return + setActiveTabId(tabId) + setTabs((prev) => + applyLruEviction( + prev.map((t) => { + if (t.type !== "component" || t.id !== tabId) return t + const historyIndex = entryId ? t.history.findIndex((e) => e.id === entryId) : t.historyIndex + if (historyIndex < 0) return t + const history = t.history.map((e, i) => { + if (i !== historyIndex || e.component !== null) return e + return buildEntry(e.definitionKey, e.definitionProps, e.id) + }) + return { + ...t, + history, + historyIndex, + } + }), + tabId, + ), + ) + } + window.addEventListener("popstate", onPopState) + return () => window.removeEventListener("popstate", onPopState) + }, []) + + // ─── Actions ───────────────────────────────────────────────────────────── + + const openTab = useCallback((args: OpenTabArgs, options?: OpenTabOptions) => { + window.dispatchEvent(new CustomEvent("arrhes:tab-opened")) + + const forceNew = options?.newTab === true || ctrlKeyRef.current + + const registry = TAB_REGISTRY as Record TabDefinition> + const def = registry[args.component as string]?.(args.props as Record) + if (!def) return + + const definitionKey = args.component as string + const definitionProps = args.props as Record + + const current = tabsRef.current + const currentActiveId = activeTabIdRef.current + + if (!forceNew) { + // ── Replace mode: push into the current tab's history ─────────── + const activeTab = current.find((t): t is ComponentTab => t.type === "component" && t.id === currentActiveId) + + if (activeTab) { + // Check if the current history entry is already this exact definition + const ce = currentEntry(activeTab) + if ( + ce.definitionKey === definitionKey && + JSON.stringify(ce.definitionProps) === JSON.stringify(definitionProps) + ) { + // Already showing this — no-op. + return + } + + const entry = buildEntry(definitionKey, definitionProps) + const newHistory = [ + // Truncate any forward history. + ...activeTab.history.slice(0, activeTab.historyIndex + 1), + entry, + ] + const newIndex = newHistory.length - 1 + + setTabs((prev) => + applyLruEviction( + prev.map((t) => + t.id === activeTab.id + ? { + ...t, + history: newHistory, + historyIndex: newIndex, + } + : t, + ), + activeTab.id, + ), + ) + window.history.pushState( + { + tabId: activeTab.id, + entryId: entry.id, + }, + "", + `/dashboard/${activeTab.id}/${entry.id}`, + ) + // Active tab ID stays the same. + return + } + // No active tab — fall through to create a new one. + } + + // ── New tab mode ───────────────────────────────────────────────────── + // Dedup: find a tab whose current history entry matches. + const existing = current.find((t): t is ComponentTab => { + if (t.type !== "component") return false + const ce = currentEntry(t) + return ( + ce.definitionKey === definitionKey && + JSON.stringify(ce.definitionProps) === JSON.stringify(definitionProps) + ) + }) + + if (existing) { + // Revive if evicted. + setTabs((prev) => + applyLruEviction( + prev.map((t) => { + if (t.type === "component" && t.id === existing.id && !t.isAlive) { + const history = t.history.map((e, i) => + i === t.historyIndex ? buildEntry(e.definitionKey, e.definitionProps) : e, + ) + return { + ...t, + isAlive: true, + history, + } + } + return t + }), + existing.id, + ), + ) + setActiveTabId(existing.id) + // Push so the previous tab's position is preserved in browser history. + if (currentActiveId !== existing.id) { + window.history.pushState( + { + tabId: existing.id, + entryId: currentEntry(existing).id, + }, + "", + `/dashboard/${existing.id}/${currentEntry(existing).id}`, + ) + } else { + window.history.replaceState( + { + tabId: existing.id, + entryId: currentEntry(existing).id, + }, + "", + `/dashboard/${existing.id}/${currentEntry(existing).id}`, + ) + } + return + } + + const entry = buildEntry(definitionKey, definitionProps) + const newTab: ComponentTab = { + id: generateId(), + type: "component", + history: [ + entry, + ], + historyIndex: 0, + isAlive: true, + } + setActiveTabId(newTab.id) + setTabs((prev) => + applyLruEviction( + [ + ...prev, + newTab, + ], + newTab.id, + ), + ) + // Push so the previous tab's position is preserved in browser history. + if (currentActiveId !== null) { + window.history.pushState( + { + tabId: newTab.id, + entryId: entry.id, + }, + "", + `/dashboard/${newTab.id}/${entry.id}`, + ) + } else { + window.history.replaceState( + { + tabId: newTab.id, + entryId: entry.id, + }, + "", + `/dashboard/${newTab.id}/${entry.id}`, + ) + } + }, []) + + const closeTab = useCallback((id: string) => { + const current = tabsRef.current + const idx = current.findIndex((t) => t.id === id) + if (idx === -1) return + const next = current.filter((t) => t.id !== id) + const currentActiveId = activeTabIdRef.current + + // Remove closed tab from focus history. + focusHistoryRef.current = focusHistoryRef.current.filter((fid) => fid !== id) + + let newActiveId: string | null + if (next.length === 0) { + newActiveId = null + } else if (currentActiveId !== id) { + newActiveId = currentActiveId + } else { + // Find the most recently focused tab that still exists. + const existingIds = new Set(next.map((t) => t.id)) + const lastFocused = [ + ...focusHistoryRef.current, + ] + .reverse() + .find((fid) => existingIds.has(fid)) + newActiveId = lastFocused ?? next[Math.min(idx, next.length - 1)].id + } + + setTabs((prev) => { + const i = prev.findIndex((t) => t.id === id) + if (i === -1) return prev + const n = prev.filter((t) => t.id !== id) + if (n.length === 0) { + setActiveTabId(null) + return [] + } + setActiveTabId((currentActive) => { + if (currentActive !== id) return currentActive + const existingIds = new Set(n.map((t) => t.id)) + const lastFocused = [ + ...focusHistoryRef.current, + ] + .reverse() + .find((fid) => existingIds.has(fid)) + return lastFocused ?? n[Math.min(i, n.length - 1)].id + }) + return n + }) + + if (newActiveId === null) { + window.history.replaceState({}, "", "/dashboard") + } else { + const newActiveTab = next.find((t): t is ComponentTab => t.type === "component" && t.id === newActiveId) + const entryId = newActiveTab ? currentEntry(newActiveTab).id : "0" + window.history.replaceState( + { + tabId: newActiveId, + entryId, + }, + "", + `/dashboard/${newActiveId}/${entryId}`, + ) + } + }, []) + + const activateTab = useCallback((id: string) => { + // Record in focus history (append, deduplicate from earlier positions). + focusHistoryRef.current = [ + ...focusHistoryRef.current.filter((fid) => fid !== id), + id, + ] + + setTabs((prev) => { + const revived = prev.map((t) => { + if (t.type === "component" && t.id === id && !t.isAlive) { + const history = t.history.map((e, i) => + i === t.historyIndex ? buildEntry(e.definitionKey, e.definitionProps) : e, + ) + return { + ...t, + isAlive: true, + history, + } + } + return t + }) + return applyLruEviction(revived, id) + }) + setActiveTabId(id) + const tab = tabsRef.current.find((t): t is ComponentTab => t.type === "component" && t.id === id) + const entryId = tab ? currentEntry(tab).id : "0" + // Push a new browser history entry when switching to a different tab so + // the back button returns to the previous tab at its exact position. + // Replace (no-op) when re-activating the already-active tab. + if (activeTabIdRef.current !== id) { + window.history.pushState( + { + tabId: id, + entryId, + }, + "", + `/dashboard/${id}/${entryId}`, + ) + } else { + window.history.replaceState( + { + tabId: id, + entryId, + }, + "", + `/dashboard/${id}/${entryId}`, + ) + } + }, []) + + // Navigate within the tab's own history stack (does not add browser history entries). + const navigateBack = useCallback((tabId: string) => { + const tab = tabsRef.current.find((t): t is ComponentTab => t.type === "component" && t.id === tabId) + if (!tab || tab.historyIndex <= 0) return + const newIndex = tab.historyIndex - 1 + const targetEntry = tab.history[newIndex] + setTabs((prev) => { + const t = prev.find((pt): pt is ComponentTab => pt.type === "component" && pt.id === tabId) + if (!t || t.historyIndex <= 0) return prev + const idx = t.historyIndex - 1 + const history = t.history.map((e, i) => { + if (i !== idx || e.component !== null) return e + return buildEntry(e.definitionKey, e.definitionProps, e.id) + }) + return applyLruEviction( + prev.map((pt) => + pt.id === tabId + ? { + ...t, + history, + historyIndex: idx, + } + : pt, + ), + tabId, + ) + }) + window.history.replaceState( + { + tabId, + entryId: targetEntry.id, + }, + "", + `/dashboard/${tabId}/${targetEntry.id}`, + ) + }, []) + + const navigateForward = useCallback((tabId: string) => { + const tab = tabsRef.current.find((t): t is ComponentTab => t.type === "component" && t.id === tabId) + if (!tab || tab.historyIndex >= tab.history.length - 1) return + const newIndex = tab.historyIndex + 1 + const targetEntry = tab.history[newIndex] + setTabs((prev) => { + const t = prev.find((pt): pt is ComponentTab => pt.type === "component" && pt.id === tabId) + if (!t || t.historyIndex >= t.history.length - 1) return prev + const idx = t.historyIndex + 1 + const history = t.history.map((e, i) => { + if (i !== idx || e.component !== null) return e + return buildEntry(e.definitionKey, e.definitionProps, e.id) + }) + return applyLruEviction( + prev.map((pt) => + pt.id === tabId + ? { + ...t, + history, + historyIndex: idx, + } + : pt, + ), + tabId, + ) + }) + window.history.replaceState( + { + tabId, + entryId: targetEntry.id, + }, + "", + `/dashboard/${tabId}/${targetEntry.id}`, + ) + }, []) + + const openPanelTab = useCallback( + (title: string, component: React.ReactNode, description?: string, icon?: string): string => { + // Dedup: if a panel tab with the same title already exists, focus it. + const existing = tabsRef.current.find((t): t is PanelTab => t.type === "panel" && t.title === title) + if (existing) { + setActiveTabId(existing.id) + window.history.pushState( + { + panelTabId: existing.id, + }, + "", + window.location.href, + ) + return existing.id + } + + const id = generateId() + const newTab: PanelTab = { + id, + type: "panel", + title, + description, + icon, + component, + } + setTabs((prev) => [ + ...prev, + newTab, + ]) + setActiveTabId(id) + // Push a browser history entry so that pressing Back from a panel tab + // returns to the previous tab/panel rather than skipping it. + // Store the panel tab ID so popstate can re-activate it on forward. + window.history.pushState( + { + panelTabId: id, + }, + "", + window.location.href, + ) + return id + }, + [], + ) + + const updateTabTitle = useCallback((id: string, title: string) => { + setTabs((prev) => + prev.map((t) => { + if (t.id !== id || t.type !== "component") return t + // Update the title of the current history entry. + const history = t.history.map((e, i) => + i === t.historyIndex + ? { + ...e, + title, + } + : e, + ) + return { + ...t, + history, + } + }), + ) + }, []) + + const reorderTabs = useCallback((tabId: string, beforeTabId: string | null) => { + setTabs((prev) => { + const tab = prev.find((t) => t.id === tabId) + if (!tab) return prev + const without = prev.filter((t) => t.id !== tabId) + if (beforeTabId === null) + return [ + ...without, + tab, + ] + const idx = without.findIndex((t) => t.id === beforeTabId) + if (idx === -1) + return [ + ...without, + tab, + ] + return [ + ...without.slice(0, idx), + tab, + ...without.slice(idx), + ] + }) + }, []) + + const value = useMemo( + () => ({ + tabs, + activeTabId, + openTab, + closeTab, + activateTab, + navigateBack, + navigateForward, + openPanelTab, + updateTabTitle, + reorderTabs, + }), + [ + tabs, + activeTabId, + openTab, + closeTab, + activateTab, + navigateBack, + navigateForward, + openPanelTab, + updateTabTitle, + reorderTabs, + ], + ) + + return ( + + {children} + + ) +} diff --git a/packages/website/src/contexts/tabs/tabsStorage.ts b/packages/website/src/contexts/tabs/tabsStorage.ts new file mode 100644 index 00000000..4491a406 --- /dev/null +++ b/packages/website/src/contexts/tabs/tabsStorage.ts @@ -0,0 +1,117 @@ +import { generateId } from "@arrhes/application-metadata" + +// ─── sessionStorage persistence ────────────────────────────────────────────── +// Only component tabs are persisted (panel tabs hold a ReactNode which can't be serialized). + +const STORAGE_KEY = "arrhes_tabs" + +export type PersistedHistoryEntry = { + id: string + definitionKey: string + definitionProps: Record + title: string + description?: string +} + +export type PersistedComponentTab = { + id: string + type: "component" + /** Full navigation history for this tab. */ + history: PersistedHistoryEntry[] + /** Currently-visible index into `history`. */ + historyIndex: number +} + +export type PersistedState = { + activeTabId: string | null + tabs: PersistedComponentTab[] +} + +// ─── Legacy shape (before history was introduced) ──────────────────────────── + +type LegacyPersistedTab = { + id: string + type: "component" + title: string + description?: string + icon?: string + definitionKey: string + definitionProps: Record + // no `history` field +} + +type LegacyPersistedState = { + activeTabId: string | null + tabs: LegacyPersistedTab[] +} + +function isLegacy(raw: unknown): raw is LegacyPersistedState { + if (typeof raw !== "object" || raw === null) return false + const obj = raw as Record + if (!Array.isArray(obj.tabs)) return false + const first = obj.tabs[0] + if (!first) return true // empty — either format + return !("history" in first) +} + +// ─── I/O ───────────────────────────────────────────────────────────────────── + +export function loadPersistedTabs(): PersistedState | null { + try { + const raw = sessionStorage.getItem(STORAGE_KEY) + if (raw === null) return null + const parsed: unknown = JSON.parse(raw) + + // Migrate legacy format that had a flat definitionKey/definitionProps/title. + if (isLegacy(parsed)) { + return { + activeTabId: parsed.activeTabId, + tabs: parsed.tabs.map((t) => ({ + id: t.id, + type: "component" as const, + history: [ + { + id: generateId(), + definitionKey: t.definitionKey, + definitionProps: t.definitionProps ?? {}, + title: t.title, + description: t.description, + }, + ], + historyIndex: 0, + })), + } + } + + return parsed as PersistedState + } catch { + return null + } +} + +/** Normalise a loaded state: assign IDs to any entries that are missing one + * (forward-compat for sessions saved before this field was added). */ +export function normalisePersisted(state: PersistedState): PersistedState { + return { + ...state, + tabs: state.tabs.map((t) => ({ + ...t, + history: t.history.map((e) => + e.id + ? e + : { + ...e, + id: generateId(), + }, + ), + })), + } +} + +export function savePersistedTabs(state: PersistedState): void { + try { + sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state)) + } catch { + // sessionStorage unavailable (private mode etc.) — silently ignore + } +} diff --git a/packages/website/src/contexts/tabs/useOuterRouter.tsx b/packages/website/src/contexts/tabs/useOuterRouter.tsx new file mode 100644 index 00000000..dfdb3299 --- /dev/null +++ b/packages/website/src/contexts/tabs/useOuterRouter.tsx @@ -0,0 +1,16 @@ +import { createContext, useContext } from "react" +import type { applicationRouter } from "../../routes/applicationRouter.js" + +type OuterRouter = typeof applicationRouter + +const OuterRouterContext = createContext(null) + +export const OuterRouterProvider = OuterRouterContext.Provider + +export function useOuterRouter(): OuterRouter { + const router = useContext(OuterRouterContext) + if (router === null) { + throw new Error("useOuterRouter must be used within an OuterRouterProvider") + } + return router +} diff --git a/packages/website/src/contexts/tabs/useTabs.tsx b/packages/website/src/contexts/tabs/useTabs.tsx new file mode 100644 index 00000000..91823fd9 --- /dev/null +++ b/packages/website/src/contexts/tabs/useTabs.tsx @@ -0,0 +1,10 @@ +import { useContext } from "react" +import { TabsContext, type TabsContextValue } from "./tabsContext.js" + +export function useTabs(): TabsContextValue { + const ctx = useContext(TabsContext) + if (ctx === null) { + throw new Error("useTabs must be used within a TabsProvider") + } + return ctx +} diff --git a/packages/website/src/features/Breadcrumbs.tsx b/packages/website/src/features/Breadcrumbs.tsx new file mode 100644 index 00000000..11bb9d68 --- /dev/null +++ b/packages/website/src/features/Breadcrumbs.tsx @@ -0,0 +1,90 @@ +import { readOneOrganizationRouteDefinition, readOneYearRouteDefinition } from "@arrhes/application-metadata/routes" +import { ButtonGhostContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { useRouterState } from "@tanstack/react-router" +import { Fragment } from "react/jsx-runtime" +import { LinkButton } from "../components/LinkButton.js" +import { useDataFromAPI } from "../utilities/useHTTPData.js" + +export function Breadcrumbs() { + const routerState = useRouterState() + const currentMatch = routerState.matches.at(-1) + const params = currentMatch?.params as Record | undefined + + const idOrganization = params?.idOrganization ?? null + const idYear = params?.idYear ?? null + + const organizationQuery = useDataFromAPI({ + routeDefinition: readOneOrganizationRouteDefinition, + body: { + idOrganization: idOrganization ?? "", + }, + enabled: idOrganization !== null, + }) + + const yearQuery = useDataFromAPI({ + routeDefinition: readOneYearRouteDefinition, + body: { + idYear: idYear ?? "", + }, + enabled: idOrganization !== null && idYear !== null, + }) + + const organizationName = organizationQuery.data?.name ?? idOrganization + const yearName = yearQuery.data?.label ?? idYear + + if (idOrganization === null) { + return null + } + + return ( +
+ + + + {idYear !== null && ( + + + / + + {/* */} + + + + + )} +
+ ) +} diff --git a/packages/website/src/features/rootLayout.tsx b/packages/website/src/features/RootLayout.tsx similarity index 100% rename from packages/website/src/features/rootLayout.tsx rename to packages/website/src/features/RootLayout.tsx diff --git a/packages/website/src/features/breadcrumbs.tsx b/packages/website/src/features/breadcrumbs.tsx deleted file mode 100644 index 63106f66..00000000 --- a/packages/website/src/features/breadcrumbs.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { readOneOrganizationRouteDefinition, readOneYearRouteDefinition } from "@arrhes/application-metadata/routes" -import { ButtonGhostContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { useRouterState } from "@tanstack/react-router" -import { Fragment } from "react/jsx-runtime" -import { LinkButton } from "../components/linkButton.js" -import { useDataFromAPI } from "../utilities/useHTTPData.js" - -export function Breadcrumbs() { - const routerState = useRouterState() - const currentMatch = routerState.matches.at(-1) - const params = currentMatch?.params as Record | undefined - - const idOrganization = params?.idOrganization ?? null - const idYear = params?.idYear ?? null - - const organizationQuery = useDataFromAPI({ - routeDefinition: readOneOrganizationRouteDefinition, - body: { - idOrganization: idOrganization ?? "", - }, - enabled: idOrganization !== null, - }) - - const yearQuery = useDataFromAPI({ - routeDefinition: readOneYearRouteDefinition, - body: { - idYear: idYear ?? "", - }, - enabled: idOrganization !== null && idYear !== null, - }) - - const organizationName = organizationQuery.data?.name ?? idOrganization - const yearName = yearQuery.data?.label ?? idYear - - if (idOrganization === null) { - return null - } - - return ( -
- - - - {idYear !== null && ( - - - / - - {/* */} - - - - - )} -
- ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/OrganizationLayout.tsx b/packages/website/src/features/dashboard/$idOrganization/OrganizationLayout.tsx new file mode 100644 index 00000000..4dca574f --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/OrganizationLayout.tsx @@ -0,0 +1,5 @@ +import { Outlet } from "@tanstack/react-router" + +export function OrganizationLayout() { + return +} diff --git a/packages/website/src/features/dashboard/$idOrganization/OrganizationTabContent.tsx b/packages/website/src/features/dashboard/$idOrganization/OrganizationTabContent.tsx new file mode 100644 index 00000000..897b131c --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/OrganizationTabContent.tsx @@ -0,0 +1,206 @@ +import { Button, ButtonGhostContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { + IconApi, + IconCalendar, + IconChevronRight, + IconCreditCard, + IconRobot, + IconSettings, + IconUsers, +} from "@tabler/icons-react" +import type { ReactNode } from "react" +import { TabLink } from "../../../components/layouts/tabBar/TabLink.js" +import type { OpenTabArgs } from "../../../contexts/tabs/tabDefinitions.js" + +type NavItem = { + label: string + description: string + icon: ReactNode + args: OpenTabArgs +} + +function NavCard({ item }: { item: NavItem }) { + return ( + + + + ) +} + +export function OrganizationTabContent(props: { idOrganization: string }) { + const items: NavItem[] = [ + { + label: "Exercices", + description: "Ann\u00e9es fiscales et \u00e9critures comptables", + icon: , + args: { + component: "exercices", + props: { + idOrganization: props.idOrganization, + }, + }, + }, + { + label: "Assistant", + description: "Assistant IA comptable", + icon: , + args: { + component: "agent", + props: { + idOrganization: props.idOrganization, + }, + }, + }, + { + label: "Membres", + description: "Utilisateurs et droits d\u2019acc\u00e8s", + icon: , + args: { + component: "membres", + props: { + idOrganization: props.idOrganization, + }, + }, + }, + { + label: "Facturation", + description: "Abonnements, services et paiements", + icon: , + args: { + component: "organisation-facturation", + props: { + idOrganization: props.idOrganization, + }, + }, + }, + { + label: "Param\u00e8tres", + description: "Configuration g\u00e9n\u00e9rale et s\u00e9curit\u00e9", + icon: , + args: { + component: "organisation-param\u00e8tres", + props: { + idOrganization: props.idOrganization, + }, + }, + }, + { + label: "API", + description: "Cl\u00e9s d\u2019acc\u00e8s et documentation", + icon: , + args: { + component: "organisation-api", + props: { + idOrganization: props.idOrganization, + }, + }, + }, + ] + + return ( +
+ {items.map((item) => ( +
+ +
+ ))} +
+ ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/agent/AgentLayout.tsx b/packages/website/src/features/dashboard/$idOrganization/agent/AgentLayout.tsx new file mode 100644 index 00000000..b942c36f --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/agent/AgentLayout.tsx @@ -0,0 +1,316 @@ +import { + readAllAgentSessionsRouteDefinition, + readOrganizationBillingRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { Button, ButtonGhostContent, ButtonOutlineContent, formatDateTime, LinkButton } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconMenu, IconMessage, IconPlus } from "@tabler/icons-react" +import { Outlet, useNavigate, useParams } from "@tanstack/react-router" +import { useState } from "react" +import { Banner } from "../../../../components/layouts/Banner.tsx" +import { EmptyState } from "../../../../components/layouts/EmptyState.tsx" +import { Page } from "../../../../components/layouts/page/page.tsx" +import { SearchBar } from "../../../../components/layouts/SearchBar.tsx" + +import { useDataFromAPI } from "../../../../utilities/useHTTPData.ts" +import { extractSnippet } from "./extractSnippet.ts" + +export function AgentLayout() { + const params = useParams({ + strict: false, + }) as { + idOrganization: string + idAgentSession: string + } + const [activeSessionId, setActiveSessionId] = useState(undefined) + const [isMenuOpen, setIsMenuOpen] = useState(false) + const navigate = useNavigate() + const [search, setSearch] = useState("") + + const subscription = useDataFromAPI({ + routeDefinition: readOrganizationBillingRouteDefinition, + body: {}, + }) + + const tokensTotalAvailable = subscription.data?.tokensTotalAvailable ?? 0 + const currentSessionId = activeSessionId ?? params.idAgentSession + const searchTrimmed = search.trim() + + const { data: sessions } = useDataFromAPI({ + routeDefinition: readAllAgentSessionsRouteDefinition, + body: searchTrimmed + ? { + idOrganization: params.idOrganization, + search: searchTrimmed, + } + : { + idOrganization: params.idOrganization, + }, + }) + + const displaySessions = sessions ?? [] + + const sidebarContent = ( +
+
+ + +
+
+
+ {displaySessions.length === 0 ? ( + } + title="Aucune session" + subtitle={undefined} + /> + ) : null} + {displaySessions.map((session) => { + const snippet = + searchTrimmed && session.matchedContent + ? extractSnippet(session.matchedContent, searchTrimmed) + : undefined + + return ( + +
+ + {session.title ?? formatDateTime(session.createdAt)} + + {session.title && ( + + {formatDateTime(session.createdAt)} + + )} + {snippet && ( + + {snippet} + + )} +
+
+ ) + })} +
+
+
+ ) + + if (subscription.isPending) { + return ( + + +
+ Chargement... +
+
+
+ ) + } + + return ( +
+ {sidebarContent} +
+
+ + {isMenuOpen && ( +
setIsMenuOpen(false)} + > + {sidebarContent} +
+ )} +
+
+ 0 ? "information" : "warning"} + title="Assistant IA" + > + {tokensTotalAvailable > 0 + ? `Tokens disponibles: ${tokensTotalAvailable.toLocaleString("fr-FR")}` + : "Aucun token disponible. Rechargez votre organisation pour continuer."} + +
+
+ +
+
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/agent/AgentMessage.tsx b/packages/website/src/features/dashboard/$idOrganization/agent/AgentMessage.tsx new file mode 100644 index 00000000..344caf24 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/agent/AgentMessage.tsx @@ -0,0 +1,170 @@ +import type { readAllAgentMessagesRouteDefinition } from "@arrhes/application-metadata" +import { formatDateTime } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconCalculator, IconFile, IconFileText, IconNotebook, IconPaperclip, IconTag } from "@tabler/icons-react" +import type { InferOutput } from "valibot" +import { AgentMessagePart } from "./AgentMessagePart.tsx" +import { getAgentMessageParts } from "./getAgentMessageParts.ts" + +const referenceTypeIcons: Record = { + account: IconCalculator, + entry: IconFileText, + journal: IconNotebook, + tag: IconTag, + file: IconFile, +} + +/** + * Format a Date to "HH:MM" string. + */ +function _formatTime(date: Date | undefined): string | undefined { + if (!date || Number.isNaN(date.getTime())) return undefined + const h = String(date.getHours()).padStart(2, "0") + const m = String(date.getMinutes()).padStart(2, "0") + return `${h}:${m}` +} + +export function AgentMessage(props: { + agentMessage: InferOutput[number] +}) { + const parts = getAgentMessageParts(props.agentMessage) + + return ( +
+ {/* User question */} +
+ + {props.agentMessage.userMessage} + + + {formatDateTime(new Date(props.agentMessage.createdAt))} + +
+ + {Array.isArray(props.agentMessage.attachedFiles) && props.agentMessage.attachedFiles.length > 0 && ( +
+ {( + props.agentMessage.attachedFiles as Array<{ + idFile: string + name: string + }> + ).map((file) => ( + + + {file.name} + + ))} +
+ )} + + {Array.isArray(props.agentMessage.references) && props.agentMessage.references.length > 0 && ( +
+ {( + props.agentMessage.references as Array<{ + id: string + type: string + label: string + }> + ).map((ref) => { + const Icon = referenceTypeIcons[ref.type] ?? IconFile + return ( + + + {ref.label} + + ) + })} +
+ )} + +
+ {parts.map((part, index) => ( + + ))} +
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/agent/agentMessagePart.tsx b/packages/website/src/features/dashboard/$idOrganization/agent/AgentMessagePart.tsx similarity index 100% rename from packages/website/src/features/dashboard/$idOrganization/agent/agentMessagePart.tsx rename to packages/website/src/features/dashboard/$idOrganization/agent/AgentMessagePart.tsx diff --git a/packages/website/src/features/dashboard/$idOrganization/agent/AgentPage.tsx b/packages/website/src/features/dashboard/$idOrganization/agent/AgentPage.tsx new file mode 100644 index 00000000..1c85eca3 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/agent/AgentPage.tsx @@ -0,0 +1,640 @@ +import { + createOneAgentFileRouteDefinition, + createOneAgentMessageRouteDefinition, + createOneAgentSessionRouteDefinition, + readAllAgentSessionsRouteDefinition, + readAllYearsRouteDefinition, + updateOneAgentSessionRouteDefinition, +} from "@arrhes/application-metadata" +import { Button, ButtonOutlineContent, ButtonPlainContent, InputSelect, InputTextArea, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconNotebook, IconPaperclip, IconSend, IconX } from "@tabler/icons-react" +import { Link, useNavigate, useParams } from "@tanstack/react-router" +import type { KeyboardEvent } from "react" +import { useEffect, useRef, useState } from "react" +import { Popover } from "../../../../components/overlays/popover/popover.tsx" +import { dataClient } from "../../../../contexts/data/queryClient.js" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" +import { useDataFromAPI } from "../../../../utilities/useHTTPData.js" + +const suggestionChips = [ + "Montre-moi mes écritures récentes", + "Ajoute une écriture d'achat en utilisant cette facture en pièce jointe", + "Quels sont mes exercices ouverts ?", +] + +interface PendingFileItem { + id: string + file: File + name: string +} + +export function AgentPage({ + idOrganization: idOrganizationProp, + onSessionCreated, +}: { + idOrganization?: string + onSessionCreated?: (id: string) => void +} = {}) { + const params = useParams({ + strict: false, + }) as { + idOrganization?: string + } + const idOrganization = idOrganizationProp ?? params.idOrganization ?? "" + const [input, setInput] = useState("") + const [isLoading, setIsLoading] = useState(false) + const navigate = useNavigate() + + // Fetch available years for the context bar + const { data: yearsData } = useDataFromAPI({ + routeDefinition: readAllYearsRouteDefinition, + body: {}, + }) + + // Auto-select if only one year exists + const [selectedYearId, setSelectedYearId] = useState(undefined) + const [customInstructions, setCustomInstructions] = useState("") + const [pendingFiles, setPendingFiles] = useState([]) + const fileInputRef = useRef(null) + const autoSelectedRef = useRef(false) + useEffect(() => { + if (autoSelectedRef.current || !yearsData) return + if (yearsData.length === 1 && yearsData[0]) { + autoSelectedRef.current = true + setSelectedYearId(yearsData[0].id) + } + }, [ + yearsData, + ]) + + async function createNewSession(text: string) { + if (text.trim() === "") { + toast({ + title: "Veuillez saisir une requête pour démarrer une session", + variant: "warning", + }) + return + } + if (isLoading) { + toast({ + title: "Une session est déjà en cours de création", + variant: "warning", + }) + return + } + setIsLoading(true) + + try { + const agentSessionResponse = await getResponseBodyFromAPI({ + routeDefinition: createOneAgentSessionRouteDefinition, + body: { + idOrganization: idOrganization, + message: text.trim(), + idYear: selectedYearId || null, + customInstructions: customInstructions.trim() || null, + }, + }) + + if (agentSessionResponse.ok === false) { + toast({ + title: "Impossible de créer la session", + variant: "error", + }) + return + } + + // Upload files if any + const fileIds: string[] = [] + for (const pendingFile of pendingFiles) { + const file = pendingFile.file + const hashBuffer = await crypto.subtle.digest("SHA-256", await file.arrayBuffer()) + const fileHash = Array.from(new Uint8Array(hashBuffer)) + .map((b) => b.toString(16).padStart(2, "0")) + .join("") + + const createFileResponse = await getResponseBodyFromAPI({ + routeDefinition: createOneAgentFileRouteDefinition, + body: { + idOrganization: idOrganization, + idAgentSession: agentSessionResponse.data.id, + fileName: pendingFile.name, + fileType: file.type || "application/octet-stream", + fileSize: file.size, + fileHash, + }, + }) + + if (createFileResponse.ok === false) { + toast({ + title: `Impossible d'importer ${pendingFile.name}`, + variant: "error", + }) + continue + } + + // url is null when a duplicate was found — no upload needed + if (createFileResponse.data.url) { + const uploadResponse = await fetch(createFileResponse.data.url, { + method: "PUT", + headers: { + "Content-Type": file.type || "application/octet-stream", + }, + body: file, + }) + + if (!uploadResponse.ok) { + toast({ + title: `Échec de l'envoi de ${pendingFile.name}`, + variant: "error", + }) + continue + } + } + + fileIds.push(createFileResponse.data.file.id) + } + + // Attach files to session if any were uploaded + if (fileIds.length > 0) { + await getResponseBodyFromAPI({ + routeDefinition: updateOneAgentSessionRouteDefinition, + body: { + idAgentSession: agentSessionResponse.data.id, + fileIds, + }, + }) + } + + const agentMessageResponse = await getResponseBodyFromAPI({ + routeDefinition: createOneAgentMessageRouteDefinition, + body: { + idOrganization: idOrganization, + idAgentSession: agentSessionResponse.data.id, + message: text.trim(), + }, + }) + + if (agentMessageResponse.ok === false) { + toast({ + title: "Impossible de créer le message", + variant: "error", + }) + return + } + + // Invalidate session list so the new session appears in the sidebar + dataClient.invalidateQueries({ + queryKey: [ + readAllAgentSessionsRouteDefinition.path, + ], + exact: false, + }) + + if (onSessionCreated) { + onSessionCreated(agentSessionResponse.data.id) + } else { + navigate({ + to: "/dashboard/organisations/$idOrganization/agent/sessions/$idAgentSession", + params: { + idOrganization: idOrganization, + idAgentSession: agentSessionResponse.data.id, + }, + }) + } + } catch (error) { + console.error("[createNewSession]", error) + toast({ + title: "Une erreur est survenue lors de la création de la session", + variant: "error", + }) + } finally { + setIsLoading(false) + setPendingFiles([]) + } + } + + return ( +
+
+ {/* Greeting */} +
+

+ Comment puis-je vous aider ? +

+

+ Posez une question sur votre comptabilité, demandez une action ou explorez vos données. +

+
+ + {/* Input area */} +
{ + event.preventDefault() + }} + className={css({ + width: "100%", + display: "flex", + flexDirection: "column", + gap: "0.75rem", + })} + > +
+ setInput(value ?? "")} + onKeyDown={(event: KeyboardEvent) => { + if (event.key === "Enter" && !event.shiftKey && !event.ctrlKey) { + event.preventDefault() + createNewSession(input) + } + }} + placeholder="Votre message..." + disabled={isLoading} + className={css({ + flex: 1, + })} + /> + { + const files = event.target.files + if (files && files.length > 0) { + const localFiles = Array.from(files).map((file) => ({ + id: `${file.name}-${file.size}-${file.lastModified}-${crypto.randomUUID()}`, + file, + name: file.name, + })) + setPendingFiles((prev) => [ + ...prev, + ...localFiles, + ]) + } + event.target.value = "" + }} + /> + {pendingFiles.length > 0 && ( +
+ + Fichiers ajoutés: {pendingFiles.length} + +
+ {pendingFiles.map((pendingFile) => ( + + + + {pendingFile.name} + + + + ))} +
+
+ )} +
+ + + + + +
+ + Contexte de la session + + + Ce contexte guide les réponses de l'assistant pour la session. + +
+ +
+
+ + Exercice + + setSelectedYearId(value ?? undefined)} + allowEmpty={true} + placeholder="Sélectionner un exercice" + options={ + yearsData === undefined + ? [] + : yearsData.map((year) => ({ + key: year.id, + label: year.label, + })) + } + /> +
+ +
+ + Instructions + + setCustomInstructions(value ?? "")} + placeholder="Ex: Réponds de manière détaillée, utilise le compte 411 pour les clients..." + /> +
+
+
+
+ + +
+
+
+ + {/* Suggestion chips */} +
+ {suggestionChips.map((chipText) => ( + + ))} +
+ + {/* Disclaimer */} +

+ L'assistant peut faire des erreurs. Vérifiez les informations importantes.{" "} + + En savoir plus + +

+
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/agent/AgentSessionContent.tsx b/packages/website/src/features/dashboard/$idOrganization/agent/AgentSessionContent.tsx new file mode 100644 index 00000000..3b99f5de --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/agent/AgentSessionContent.tsx @@ -0,0 +1,1314 @@ +import { + createOneAgentFileRouteDefinition, + createOneAgentMessageRouteDefinition, + deleteOneAgentSessionRouteDefinition, + getStreamForAgentMessageRouteDefinition, + readAllAgentMessagesRouteDefinition, + readAllAgentSessionsRouteDefinition, + readAllYearsRouteDefinition, + readOneAgentSessionRouteDefinition, + updateOneAgentSessionRouteDefinition, +} from "@arrhes/application-metadata" +import { + Button, + ButtonGhostContent, + ButtonOutlineContent, + ButtonPlainContent, + CircularLoader, + Dialog, + InputSelect, + InputTextArea, + toast, + useModalStore, +} from "@arrhes/ui" +import { css } from "@arrhes/ui/css" +import { + IconChevronRight, + IconDotsVertical, + IconNotebook, + IconPaperclip, + IconSend, + IconTrash, + IconX, +} from "@tabler/icons-react" +import { useNavigate, useParams } from "@tanstack/react-router" +import { useCallback, useEffect, useId, useRef, useState } from "react" +import { DataWrapper } from "../../../../components/layouts/DataWrapper.tsx" +import { Popover } from "../../../../components/overlays/popover/popover.tsx" +import { dataClient } from "../../../../contexts/data/queryClient.ts" +import { getCookie } from "../../../../utilities/cookies/getCookie.js" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../utilities/invalidateData.ts" +import { resolveApiBaseUrl } from "../../../../utilities/resolveApiBaseUrl.js" +import { useDataFromAPI } from "../../../../utilities/useHTTPData.ts" +import { cookiePrefix } from "../../../../utilities/variables.js" +import { AgentMessage } from "./AgentMessage.tsx" +import { isHealthyStreamResponse } from "./isStreamResponseUnavailable.ts" +import { MentionInput, type MentionReference } from "./MentionInput.tsx" + +const subagentLabels: Record = { + data_analyst: "Analyste de données", + entry_creator: "Créateur d'écritures", + document_processor: "Traitement de documents", + auditor: "Auditeur", +} + +function SubagentIndicator(props: { + subagents: Array<{ + role: string + depth: number + content: string + }> +}) { + const [expanded, setExpanded] = useState>({}) + + return ( +
+ {props.subagents.map((subagent, index) => { + const isExpanded = expanded[index] ?? false + const label = subagentLabels[subagent.role] ?? subagent.role + + return ( +
+ + {isExpanded && subagent.content && ( +
+ {subagent.content} +
+ )} +
+ ) + })} +
+ ) +} + +function ContextInitialiser(props: { + agentSession: { + idYear: string | null + customInstructions: string | null + } + contextInitialisedRef: React.MutableRefObject + setEditYear: (v: string | null | undefined) => void + setEditInstructions: (v: string | null | undefined) => void + children: React.ReactNode +}) { + useEffect(() => { + if (!props.contextInitialisedRef.current) { + props.contextInitialisedRef.current = true + props.setEditYear(props.agentSession.idYear ?? null) + props.setEditInstructions(props.agentSession.customInstructions ?? "") + } + }, [ + props.agentSession, + props.contextInitialisedRef, + props.setEditInstructions, + props.setEditYear, + ]) + + return <>{props.children} +} + +export function AgentSessionContent({ + idOrganization: idOrganizationProp, + idAgentSession: idAgentSessionProp, + onDeleteSession, +}: { + idOrganization?: string + idAgentSession?: string + onDeleteSession?: () => void +} = {}) { + const params = useParams({ + strict: false, + }) as { + idOrganization?: string + idAgentSession?: string + } + const idOrganization = idOrganizationProp ?? params.idOrganization ?? "" + const idAgentSession = idAgentSessionProp ?? params.idAgentSession ?? "" + + const [input, setInput] = useState(undefined) + const [draftReferences, setDraftReferences] = useState([]) + const messagesEndRef = useRef(null) + const _scrollContainerRef = useRef(null) + const navigate = useNavigate() + const [isDeleting, setIsDeleting] = useState(false) + const deleteModalId = useId() + const { open: openModal, close: closeModal } = useModalStore() + const [editYear, setEditYear] = useState(undefined) + const [editInstructions, setEditInstructions] = useState(undefined) + const [isSavingContext, setIsSavingContext] = useState(false) + const contextInitialisedRef = useRef(false) + const [pendingFiles, setPendingFiles] = useState([]) + const fileInputRef = useRef(null) + + const { data: yearsData } = useDataFromAPI({ + routeDefinition: readAllYearsRouteDefinition, + body: {}, + }) + + const handleSaveContext = useCallback(async () => { + setIsSavingContext(true) + try { + const result = await getResponseBodyFromAPI({ + routeDefinition: updateOneAgentSessionRouteDefinition, + body: { + idAgentSession: idAgentSession, + idYear: editYear || null, + customInstructions: editInstructions?.trim() || null, + }, + }) + if (result.ok) { + toast({ + title: "Contexte mis à jour", + variant: "success", + }) + await invalidateData({ + routeDefinition: readOneAgentSessionRouteDefinition, + body: { + idAgentSession: idAgentSession, + }, + }) + } else { + toast({ + title: "Erreur lors de la mise à jour", + variant: "error", + }) + } + } finally { + setIsSavingContext(false) + } + }, [ + idAgentSession, + editYear, + editInstructions, + ]) + + const handleDeleteSession = useCallback(async () => { + const sessionId = idAgentSession + setIsDeleting(true) + try { + await getResponseBodyFromAPI({ + routeDefinition: deleteOneAgentSessionRouteDefinition, + body: { + idAgentSession: sessionId, + }, + }) + await dataClient.invalidateQueries({ + queryKey: [ + readAllAgentSessionsRouteDefinition.path, + ], + exact: false, + }) + if (onDeleteSession) { + onDeleteSession() + } else { + navigate({ + to: "/dashboard/organisations/$idOrganization/agent", + params: { + idOrganization: idOrganization, + }, + }) + } + } finally { + setIsDeleting(false) + } + }, [ + navigate, + idAgentSession, + idOrganization, + onDeleteSession, + ]) + + // ── Streaming state ───────────────────────────────────────────────────────── + // ID of the assistant message currently being streamed from the worker + const [streamMessageId, setStreamMessageId] = useState(undefined) + const [isSending, setIsSending] = useState(false) + // Accumulates the text content of the in-progress assistant message + const [streamingContent, setStreamingContent] = useState("") + const [streamingToolCalls, setStreamingToolCalls] = useState([]) + const [isStreaming, setIsStreaming] = useState(false) + const [activeSubagents, setActiveSubagents] = useState< + Array<{ + role: string + depth: number + content: string + }> + >([]) + const abortStreamRef = useRef(null) + + // ── Auto-scroll to bottom on new content ──────────────────────────────── + const scrollToBottom = useCallback(() => { + messagesEndRef.current?.scrollIntoView({ + behavior: "smooth", + }) + }, []) + + // Scroll when streaming content or tool calls update + useEffect(() => { + if (isStreaming) scrollToBottom() + }, [ + isStreaming, + scrollToBottom, + ]) + + // ── Auto-detect streaming messages on page load / navigation ──────────── + // If the user opens a session that already has a message in "streaming" + // state (e.g. page refresh while the worker is running), start the SSE + // connection for it automatically. + const { data: messagesData } = useDataFromAPI({ + routeDefinition: readAllAgentMessagesRouteDefinition, + body: { + idAgentSession: idAgentSession, + }, + }) + + // Scroll when messages list changes (new message added, stream completed) + useEffect(() => { + if (messagesData) scrollToBottom() + }, [ + messagesData, + scrollToBottom, + ]) + + useEffect(() => { + if (streamMessageId) return // already streaming something + if (!messagesData) return + + const streamingMsg = messagesData.find((m) => m.state === "streaming") + if (streamingMsg) { + setStreamMessageId(streamingMsg.id) + } + }, [ + messagesData, + streamMessageId, + ]) + + // Single effect: open SSE stream, poll as fallback, self-cleanup + useEffect(() => { + if (!streamMessageId) return + + // Abort any previous stream + abortStreamRef.current?.abort() + const controller = new AbortController() + abortStreamRef.current = controller + + setIsStreaming(true) + setStreamingContent("") + setStreamingToolCalls([]) + setActiveSubagents([]) + + let accumulated = "" + const accumulatedToolCalls: unknown[] = [] + let lastBoundaryLen = 0 + const seenEnds = new Set() + let finished = false + const subagentStack: Array<{ + role: string + depth: number + content: string + }> = [] + + const finish = async () => { + if (finished) return + finished = true + await invalidateData({ + routeDefinition: readAllAgentMessagesRouteDefinition, + body: { + idAgentSession: idAgentSession, + }, + }) + setStreamingContent("") + setStreamingToolCalls([]) + setIsStreaming(false) + setStreamMessageId(undefined) + } + + // SSE stream + ;(async () => { + let streamCompleted = false + try { + const headers: Record = { + "Content-Type": "application/json", + } + const orgCookie = getCookie(`${cookiePrefix}_id_organization`) + if (orgCookie) { + headers["X-Organization-Id"] = orgCookie + } + + const apiBaseUrl = resolveApiBaseUrl(import.meta.env.VITE_API_BASE_URL) + if (!apiBaseUrl) { + throw new Error("VITE_API_BASE_URL is not configured") + } + + const response = await fetch(new URL(`${apiBaseUrl}${getStreamForAgentMessageRouteDefinition.path}`), { + method: "POST", + credentials: "include", + signal: controller.signal, + headers, + body: JSON.stringify({ + idOrganization: idOrganization, + idAgentMessage: streamMessageId, + }), + }) + + if (!isHealthyStreamResponse(response)) { + toast({ + title: "Le flux de reponse est indisponible", + description: "Veuillez renvoyer votre message.", + variant: "error", + }) + if (!controller.signal.aborted) { + await finish() + } + return + } + + const reader = response.body.getReader() + const decoder = new TextDecoder() + let buffer = "" + + while (true) { + const { done, value } = await reader.read() + if (done) break + + buffer += decoder.decode(value, { + stream: true, + }) + + const parts = buffer.split("\n\n") + buffer = parts.pop() ?? "" + + for (const part of parts) { + for (const line of part.split("\n")) { + if (!line.startsWith("data: ")) continue + const jsonStr = line.slice(6).trim() + if (!jsonStr) continue + + try { + const chunk = JSON.parse(jsonStr) + if (chunk.type === "TEXT_MESSAGE_CONTENT" && typeof chunk.delta === "string") { + if (chunk.subagentSkills && subagentStack.length > 0) { + // Accumulate subagent content + const current = subagentStack[subagentStack.length - 1] + if (current) { + current.content += chunk.delta + setActiveSubagents([ + ...subagentStack, + ]) + } + } else { + accumulated += chunk.delta + setStreamingContent(accumulated) + } + } + if (chunk.type === "TOOL_CALL_START") { + // Emit text boundary if text accumulated since last boundary + if (accumulated.length > lastBoundaryLen) { + accumulatedToolCalls.push({ + type: "TEXT_BOUNDARY", + contentLength: accumulated.length, + }) + lastBoundaryLen = accumulated.length + } + accumulatedToolCalls.push(chunk) + setStreamingToolCalls([ + ...accumulatedToolCalls, + ]) + } + if (chunk.type === "CONTEXT_LIMIT_WARNING") { + toast({ + title: "La conversation approche de sa limite de contexte", + description: `${chunk.usage}% de la capacité utilisée. Envisagez de créer une nouvelle session.`, + variant: "warning", + }) + } + if (chunk.type === "TOOL_CALL_END") { + // Deduplicate: framework re-emits TOOL_CALL_END after RUN_FINISHED + const tcId = chunk.toolCallId as string | undefined + if (tcId && seenEnds.has(tcId)) continue + if (tcId) seenEnds.add(tcId) + accumulatedToolCalls.push(chunk) + setStreamingToolCalls([ + ...accumulatedToolCalls, + ]) + } + if (chunk.type === "SUBAGENT_RUN_START") { + subagentStack.push({ + role: (chunk.skills as string) ?? "subagent", + depth: chunk.depth as number, + content: "", + }) + setActiveSubagents([ + ...subagentStack, + ]) + } + if (chunk.type === "SUBAGENT_RUN_END") { + subagentStack.pop() + setActiveSubagents([ + ...subagentStack, + ]) + } + } catch { + // ignore malformed chunks + } + } + } + } + + streamCompleted = true + } catch (err: unknown) { + if (err instanceof Error && err.name === "AbortError") return + console.error("[stream] SSE error — falling back to polling", err) + } finally { + // Only finish when the stream completed naturally. + // If the SSE failed (network/auth error), let the polling + // fallback continue running so it can detect completion. + if (streamCompleted && !controller.signal.aborted) { + finish() + } + } + })() + + // Polling failsafe: if SSE fails or is slow, periodically + // check the DB for the completed message + const poll = setInterval(async () => { + try { + const result = await getResponseBodyFromAPI({ + routeDefinition: readAllAgentMessagesRouteDefinition, + body: { + idAgentSession: idAgentSession, + }, + signal: controller.signal, + }) + if (!result.ok || !result.data) return + + const msg = result.data.find((m) => m.id === streamMessageId) + if (msg && msg.state !== "streaming") { + controller.abort() + finish() + } + } catch { + // ignore polling errors + } + }, 3000) + + return () => { + clearInterval(poll) + controller.abort() + } + }, [ + streamMessageId, + idAgentSession, + idOrganization, + ]) + + // ── Send message ──────────────────────────────────────────────────────────── + const sendMessageToWorker = useCallback( + async ( + text: string, + references?: MentionReference[], + options?: { + idYear?: string | null + attachedFiles?: Array<{ + idFile: string + name: string + }> | null + }, + ) => { + if (!text.trim() || isSending) return + setIsSending(true) + + try { + if (pendingFiles.length > 0) { + if (!options?.idYear) { + toast({ + title: "Veuillez sélectionner un exercice pour importer des fichiers", + variant: "warning", + }) + return + } + + const existingIds = (options.attachedFiles ?? []).map((file) => file.idFile) + const newIds: string[] = [] + const failedFiles: File[] = [] + + for (const file of pendingFiles) { + const hashBuffer = await crypto.subtle.digest("SHA-256", await file.arrayBuffer()) + const fileHash = Array.from(new Uint8Array(hashBuffer)) + .map((b) => b.toString(16).padStart(2, "0")) + .join("") + + const createFileResponse = await getResponseBodyFromAPI({ + routeDefinition: createOneAgentFileRouteDefinition, + body: { + idOrganization: idOrganization, + idAgentSession: idAgentSession, + fileName: file.name, + fileType: file.type || "application/octet-stream", + fileSize: file.size, + fileHash, + }, + }) + + if (createFileResponse.ok === false) { + toast({ + title: `Impossible d'importer ${file.name}`, + variant: "error", + }) + failedFiles.push(file) + continue + } + + if (createFileResponse.data.url) { + const uploadResponse = await fetch(createFileResponse.data.url, { + method: "PUT", + headers: { + "Content-Type": file.type || "application/octet-stream", + }, + body: file, + }) + if (!uploadResponse.ok) { + toast({ + title: `Échec de l'envoi de ${file.name}`, + variant: "error", + }) + failedFiles.push(file) + continue + } + } + + newIds.push(createFileResponse.data.file.id) + } + + if (newIds.length > 0) { + const attachResult = await getResponseBodyFromAPI({ + routeDefinition: updateOneAgentSessionRouteDefinition, + body: { + idAgentSession: idAgentSession, + fileIds: [ + ...existingIds, + ...newIds, + ], + }, + }) + + if (!attachResult.ok) { + toast({ + title: "Impossible d'attacher les fichiers a la session", + variant: "error", + }) + return + } + + await invalidateData({ + routeDefinition: readOneAgentSessionRouteDefinition, + body: { + idAgentSession: idAgentSession, + }, + }) + } + + setPendingFiles(failedFiles) + } + + const result = await getResponseBodyFromAPI({ + routeDefinition: createOneAgentMessageRouteDefinition, + body: { + idOrganization: idOrganization, + idAgentSession: idAgentSession, + message: text.trim(), + references: references && references.length > 0 ? references : null, + }, + }) + + if (!result.ok || !result.data) { + toast({ + title: "Impossible de créer le message", + variant: "error", + }) + return + } + + const { id: idAgentMessage } = result.data + + setStreamMessageId(idAgentMessage) + + await invalidateData({ + routeDefinition: readAllAgentMessagesRouteDefinition, + body: { + idAgentSession: idAgentSession, + }, + }) + scrollToBottom() + return true + } catch (error) { + console.error("[sendMessageToWorker]", error) + toast({ + title: "Une erreur est survenue lors de l'envoi du message", + variant: "error", + }) + return false + } finally { + setIsSending(false) + } + }, + [ + isSending, + idOrganization, + idAgentSession, + pendingFiles, + scrollToBottom, + ], + ) + + const [_historyIndex, setHistoryIndex] = useState(-1) + + const isSubmitting = isSending || isStreaming + + return ( + + {(agentSession) => { + const _hasContext = Boolean(agentSession.idYear || agentSession.customInstructions?.trim()) + + return ( + +
+ {/* Messages area */} + + {(agentMessages) => { + const sortedAgentMessages = agentMessages.sort( + (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(), + ) + + return ( +
+ {sortedAgentMessages.map((agentMessage) => { + // If this is the message being streamed, show live content + if (agentMessage.id === streamMessageId && isStreaming) { + return ( +
+ 0 + ? streamingToolCalls + : agentMessage.toolCalls, + }} + /> + {activeSubagents.length > 0 && ( + + )} +
+ +
+
+ ) + } + return ( + + ) + })} + + {/* Thinking indicator — waiting for first token */} + {isSending && !isStreaming && ( +
+ +
+ )} + +
+
+ ) + }} + + + {/* Input area */} +
+ { + void sendMessageToWorker(text, references, { + idYear: agentSession.idYear, + attachedFiles: (agentSession.attachedFiles ?? []) as Array<{ + idFile: string + name: string + }>, + }) + }} + onValueChange={(text, references) => { + setInput(text) + setDraftReferences(references) + }} + disabled={isSubmitting} + idOrganization={idOrganization} + idYear={agentSession.idYear} + /> + { + const files = event.target.files + if (!files || files.length === 0) return + const newFiles = Array.from(files) + event.target.value = "" + + setPendingFiles((prev) => [ + ...prev, + ...newFiles, + ]) + }} + /> + {/* File chips */} + {(() => { + const sessionFiles = (agentSession.attachedFiles ?? []) as Array<{ + idFile: string + name: string + }> + const hasFiles = sessionFiles.length > 0 || pendingFiles.length > 0 + if (!hasFiles) return null + return ( +
+ {sessionFiles.length > 0 && ( +
+ {sessionFiles.map((file) => ( + + + + {file.name} + + + + ))} +
+ )} + + {pendingFiles.length > 0 && ( +
+ + Fichiers ajoutés: {pendingFiles.length} + +
+ {pendingFiles.map((file, index) => ( + + + + {file.name} + + + + ))} +
+
+ )} +
+ ) + })()} +
+ + + + + + + + + + , + ) + } + > + } + text={isDeleting ? "Suppression..." : "Supprimer la session"} + color="danger" + className={css({ + width: "100%", + justifyContent: "start", + })} + /> + + + + + + + + + +
+ + Contexte de la session + + + Ce contexte guide les réponses de l'assistant pour cette session. + +
+ +
+
+ + Exercice + + setEditYear(value)} + allowEmpty={true} + placeholder="Sélectionner un exercice" + options={ + yearsData === undefined + ? [] + : yearsData.map((year) => ({ + key: year.id, + label: year.label, + })) + } + /> +
+ +
+ + Instructions + + setEditInstructions(value)} + placeholder="Ex: Réponds de manière détaillée, utilise le compte 411 pour les clients..." + /> +
+
+ + +
+
+ + +
+
+
+ + ) + }} +
+ ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/agent/AgentSessionPage.tsx b/packages/website/src/features/dashboard/$idOrganization/agent/AgentSessionPage.tsx new file mode 100644 index 00000000..f94342fa --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/agent/AgentSessionPage.tsx @@ -0,0 +1,17 @@ +import { css } from "@arrhes/ui/css" +import { AgentSessionContent } from "./AgentSessionContent.tsx" + +export function AgentSessionPage() { + return ( +
+ +
+ ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/agent/AgentTabContent.tsx b/packages/website/src/features/dashboard/$idOrganization/agent/AgentTabContent.tsx new file mode 100644 index 00000000..96f45e9a --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/agent/AgentTabContent.tsx @@ -0,0 +1,329 @@ +import { + readAllAgentSessionsRouteDefinition, + readOrganizationBillingRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { Button, ButtonGhostContent, ButtonOutlineContent, formatDateTime } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconMenu, IconMessage, IconPlus } from "@tabler/icons-react" +import { lazy, Suspense, useState } from "react" +import { Banner } from "../../../../components/layouts/Banner.tsx" +import { EmptyState } from "../../../../components/layouts/EmptyState.tsx" +import { Page } from "../../../../components/layouts/page/page.tsx" +import { SearchBar } from "../../../../components/layouts/SearchBar.tsx" +import { useDataFromAPI } from "../../../../utilities/useHTTPData.ts" +import { extractSnippet } from "./extractSnippet.ts" + +const AgentPage = lazy(() => + import("./AgentPage.js").then((m) => ({ + default: m.AgentPage, + })), +) +const AgentSessionContent = lazy(() => + import("./AgentSessionContent.js").then((m) => ({ + default: m.AgentSessionContent, + })), +) + +export function AgentTabContent({ idOrganization }: { idOrganization: string }) { + const [selectedSessionId, setSelectedSessionId] = useState(null) + const [isMenuOpen, setIsMenuOpen] = useState(false) + const [search, setSearch] = useState("") + + const subscription = useDataFromAPI({ + routeDefinition: readOrganizationBillingRouteDefinition, + body: {}, + }) + + const tokensTotalAvailable = subscription.data?.tokensTotalAvailable ?? 0 + const searchTrimmed = search.trim() + + const { data: sessions } = useDataFromAPI({ + routeDefinition: readAllAgentSessionsRouteDefinition, + body: searchTrimmed + ? { + idOrganization, + search: searchTrimmed, + } + : { + idOrganization, + }, + }) + + const displaySessions = sessions ?? [] + + const sidebarContent = ( +
+
+ + +
+
+
+ {displaySessions.length === 0 ? ( + } + title="Aucune session" + subtitle={undefined} + /> + ) : null} + {displaySessions.map((session) => { + const snippet = + searchTrimmed && session.matchedContent + ? extractSnippet(session.matchedContent, searchTrimmed) + : undefined + + return ( + + ) + })} +
+
+
+ ) + + if (subscription.isPending) { + return ( + + +
+ Chargement... +
+
+
+ ) + } + + return ( +
+ {sidebarContent} +
+
+ + {isMenuOpen && ( +
setIsMenuOpen(false)} + > + {sidebarContent} +
+ )} +
+
+ 0 ? "information" : "warning"} + title="Assistant IA" + > + {tokensTotalAvailable > 0 + ? `Tokens disponibles: ${tokensTotalAvailable.toLocaleString("fr-FR")}` + : "Aucun token disponible. Rechargez votre organisation pour continuer."} + +
+
+ + {selectedSessionId === null ? ( + setSelectedSessionId(id)} + /> + ) : ( + // key forces remount when switching sessions + setSelectedSessionId(null)} + /> + )} + +
+
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/agent/mentionInput.tsx b/packages/website/src/features/dashboard/$idOrganization/agent/MentionInput.tsx similarity index 100% rename from packages/website/src/features/dashboard/$idOrganization/agent/mentionInput.tsx rename to packages/website/src/features/dashboard/$idOrganization/agent/MentionInput.tsx diff --git a/packages/website/src/features/dashboard/$idOrganization/agent/agentLayout.tsx b/packages/website/src/features/dashboard/$idOrganization/agent/agentLayout.tsx deleted file mode 100644 index ceb1b958..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/agent/agentLayout.tsx +++ /dev/null @@ -1,313 +0,0 @@ -import { - readAllAgentSessionsRouteDefinition, - readOrganizationBillingRouteDefinition, -} from "@arrhes/application-metadata/routes" -import { Button, ButtonGhostContent, ButtonOutlineContent, formatDateTime, LinkButton } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconMenu, IconMessage, IconPlus } from "@tabler/icons-react" -import { Outlet, useNavigate, useParams } from "@tanstack/react-router" -import { useState } from "react" -import { Banner } from "../../../../components/layouts/banner.tsx" -import { EmptyState } from "../../../../components/layouts/emptyState.tsx" -import { Page } from "../../../../components/layouts/page/page.tsx" -import { SearchBar } from "../../../../components/layouts/searchBar.tsx" -import { organizationPathRoute } from "../../../../routes/root/dashboard/organizations/$idOrganization/organizationPathRoute.tsx" -import { useDataFromAPI } from "../../../../utilities/useHTTPData.ts" -import { extractSnippet } from "./extractSnippet.ts" - -export function AgentLayout() { - const params = useParams({ - from: organizationPathRoute.id, - }) - const [activeSessionId, setActiveSessionId] = useState(undefined) - const [isMenuOpen, setIsMenuOpen] = useState(false) - const navigate = useNavigate() - const [search, setSearch] = useState("") - - const subscription = useDataFromAPI({ - routeDefinition: readOrganizationBillingRouteDefinition, - body: {}, - }) - - const tokensTotalAvailable = subscription.data?.tokensTotalAvailable ?? 0 - const currentSessionId = activeSessionId ?? params.idAgentSession - const searchTrimmed = search.trim() - - const { data: sessions } = useDataFromAPI({ - routeDefinition: readAllAgentSessionsRouteDefinition, - body: searchTrimmed - ? { - idOrganization: params.idOrganization, - search: searchTrimmed, - } - : { - idOrganization: params.idOrganization, - }, - }) - - const displaySessions = sessions ?? [] - - const sidebarContent = ( -
-
- - -
-
-
- {displaySessions.length === 0 ? ( - } - title="Aucune session" - subtitle={undefined} - /> - ) : null} - {displaySessions.map((session) => { - const snippet = - searchTrimmed && session.matchedContent - ? extractSnippet(session.matchedContent, searchTrimmed) - : undefined - - return ( - -
- - {session.title ?? formatDateTime(session.createdAt)} - - {session.title && ( - - {formatDateTime(session.createdAt)} - - )} - {snippet && ( - - {snippet} - - )} -
-
- ) - })} -
-
-
- ) - - if (subscription.isPending) { - return ( - - -
- Chargement... -
-
-
- ) - } - - return ( -
- {sidebarContent} -
-
- - {isMenuOpen && ( -
setIsMenuOpen(false)} - > - {sidebarContent} -
- )} -
-
- 0 ? "information" : "warning"} - title="Assistant IA" - > - {tokensTotalAvailable > 0 - ? `Tokens disponibles: ${tokensTotalAvailable.toLocaleString("fr-FR")}` - : "Aucun token disponible. Rechargez votre organisation pour continuer."} - -
-
- -
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/agent/agentMessage.tsx b/packages/website/src/features/dashboard/$idOrganization/agent/agentMessage.tsx deleted file mode 100644 index 457c5036..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/agent/agentMessage.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import type { readAllAgentMessagesRouteDefinition } from "@arrhes/application-metadata" -import { formatDateTime } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconCalculator, IconFile, IconFileText, IconNotebook, IconPaperclip, IconTag } from "@tabler/icons-react" -import type { InferOutput } from "valibot" -import { AgentMessagePart } from "./agentMessagePart.tsx" -import { getAgentMessageParts } from "./getAgentMessageParts.ts" - -const referenceTypeIcons: Record = { - account: IconCalculator, - entry: IconFileText, - journal: IconNotebook, - tag: IconTag, - file: IconFile, -} - -/** - * Format a Date to "HH:MM" string. - */ -function _formatTime(date: Date | undefined): string | undefined { - if (!date || Number.isNaN(date.getTime())) return undefined - const h = String(date.getHours()).padStart(2, "0") - const m = String(date.getMinutes()).padStart(2, "0") - return `${h}:${m}` -} - -export function AgentMessage(props: { - agentMessage: InferOutput[number] -}) { - const parts = getAgentMessageParts(props.agentMessage) - - return ( -
- {/* User question */} -
- - {props.agentMessage.userMessage} - - - {formatDateTime(new Date(props.agentMessage.createdAt))} - -
- - {Array.isArray(props.agentMessage.attachedFiles) && props.agentMessage.attachedFiles.length > 0 && ( -
- {( - props.agentMessage.attachedFiles as Array<{ - idFile: string - name: string - }> - ).map((file) => ( - - - {file.name} - - ))} -
- )} - - {Array.isArray(props.agentMessage.references) && props.agentMessage.references.length > 0 && ( -
- {( - props.agentMessage.references as Array<{ - id: string - type: string - label: string - }> - ).map((ref) => { - const Icon = referenceTypeIcons[ref.type] ?? IconFile - return ( - - - {ref.label} - - ) - })} -
- )} - -
- {parts.map((part, index) => ( - - ))} -
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/agent/agentPage.tsx b/packages/website/src/features/dashboard/$idOrganization/agent/agentPage.tsx deleted file mode 100644 index e0d8dd13..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/agent/agentPage.tsx +++ /dev/null @@ -1,628 +0,0 @@ -import { - createOneAgentFileRouteDefinition, - createOneAgentMessageRouteDefinition, - createOneAgentSessionRouteDefinition, - readAllAgentSessionsRouteDefinition, - readAllYearsRouteDefinition, - updateOneAgentSessionRouteDefinition, -} from "@arrhes/application-metadata" -import { Button, ButtonOutlineContent, ButtonPlainContent, InputSelect, InputTextArea, toast } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconNotebook, IconPaperclip, IconSend, IconX } from "@tabler/icons-react" -import { Link, useNavigate, useParams } from "@tanstack/react-router" -import type { KeyboardEvent } from "react" -import { useEffect, useRef, useState } from "react" -import { Popover } from "../../../../components/overlays/popover/popover.tsx" -import { dataClient } from "../../../../contexts/data/queryClient.js" -import { organizationPathRoute } from "../../../../routes/root/dashboard/organizations/$idOrganization/organizationPathRoute.js" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" -import { useDataFromAPI } from "../../../../utilities/useHTTPData.js" - -const suggestionChips = [ - "Montre-moi mes écritures récentes", - "Ajoute une écriture d'achat en utilisant cette facture en pièce jointe", - "Quels sont mes exercices ouverts ?", -] - -interface PendingFileItem { - id: string - file: File - name: string -} - -export function AgentPage() { - const params = useParams({ - from: organizationPathRoute.id, - }) - const [input, setInput] = useState("") - const [isLoading, setIsLoading] = useState(false) - const navigate = useNavigate() - - // Fetch available years for the context bar - const { data: yearsData } = useDataFromAPI({ - routeDefinition: readAllYearsRouteDefinition, - body: {}, - }) - - // Auto-select if only one year exists - const [selectedYearId, setSelectedYearId] = useState(undefined) - const [customInstructions, setCustomInstructions] = useState("") - const [pendingFiles, setPendingFiles] = useState([]) - const fileInputRef = useRef(null) - const autoSelectedRef = useRef(false) - useEffect(() => { - if (autoSelectedRef.current || !yearsData) return - if (yearsData.length === 1 && yearsData[0]) { - autoSelectedRef.current = true - setSelectedYearId(yearsData[0].id) - } - }, [ - yearsData, - ]) - - async function createNewSession(text: string) { - if (text.trim() === "") { - toast({ - title: "Veuillez saisir une requête pour démarrer une session", - variant: "warning", - }) - return - } - if (isLoading) { - toast({ - title: "Une session est déjà en cours de création", - variant: "warning", - }) - return - } - setIsLoading(true) - - try { - const agentSessionResponse = await getResponseBodyFromAPI({ - routeDefinition: createOneAgentSessionRouteDefinition, - body: { - idOrganization: params.idOrganization, - message: text.trim(), - idYear: selectedYearId || null, - customInstructions: customInstructions.trim() || null, - }, - }) - - if (agentSessionResponse.ok === false) { - toast({ - title: "Impossible de créer la session", - variant: "error", - }) - return - } - - // Upload files if any - const fileIds: string[] = [] - for (const pendingFile of pendingFiles) { - const file = pendingFile.file - const hashBuffer = await crypto.subtle.digest("SHA-256", await file.arrayBuffer()) - const fileHash = Array.from(new Uint8Array(hashBuffer)) - .map((b) => b.toString(16).padStart(2, "0")) - .join("") - - const createFileResponse = await getResponseBodyFromAPI({ - routeDefinition: createOneAgentFileRouteDefinition, - body: { - idOrganization: params.idOrganization, - idAgentSession: agentSessionResponse.data.id, - fileName: pendingFile.name, - fileType: file.type || "application/octet-stream", - fileSize: file.size, - fileHash, - }, - }) - - if (createFileResponse.ok === false) { - toast({ - title: `Impossible d'importer ${pendingFile.name}`, - variant: "error", - }) - continue - } - - // url is null when a duplicate was found — no upload needed - if (createFileResponse.data.url) { - const uploadResponse = await fetch(createFileResponse.data.url, { - method: "PUT", - headers: { - "Content-Type": file.type || "application/octet-stream", - }, - body: file, - }) - - if (!uploadResponse.ok) { - toast({ - title: `Échec de l'envoi de ${pendingFile.name}`, - variant: "error", - }) - continue - } - } - - fileIds.push(createFileResponse.data.file.id) - } - - // Attach files to session if any were uploaded - if (fileIds.length > 0) { - await getResponseBodyFromAPI({ - routeDefinition: updateOneAgentSessionRouteDefinition, - body: { - idAgentSession: agentSessionResponse.data.id, - fileIds, - }, - }) - } - - const agentMessageResponse = await getResponseBodyFromAPI({ - routeDefinition: createOneAgentMessageRouteDefinition, - body: { - idOrganization: params.idOrganization, - idAgentSession: agentSessionResponse.data.id, - message: text.trim(), - }, - }) - - if (agentMessageResponse.ok === false) { - toast({ - title: "Impossible de créer le message", - variant: "error", - }) - return - } - - // Invalidate session list so the new session appears in the sidebar - dataClient.invalidateQueries({ - queryKey: [ - readAllAgentSessionsRouteDefinition.path, - ], - exact: false, - }) - - navigate({ - to: "/dashboard/organisations/$idOrganization/agent/sessions/$idAgentSession", - params: { - idOrganization: params.idOrganization, - idAgentSession: agentSessionResponse.data.id, - }, - }) - } catch (error) { - console.error("[createNewSession]", error) - toast({ - title: "Une erreur est survenue lors de la création de la session", - variant: "error", - }) - } finally { - setIsLoading(false) - setPendingFiles([]) - } - } - - return ( -
-
- {/* Greeting */} -
-

- Comment puis-je vous aider ? -

-

- Posez une question sur votre comptabilité, demandez une action ou explorez vos données. -

-
- - {/* Input area */} -
{ - event.preventDefault() - }} - className={css({ - width: "100%", - display: "flex", - flexDirection: "column", - gap: "0.75rem", - })} - > -
- setInput(value ?? "")} - onKeyDown={(event: KeyboardEvent) => { - if (event.key === "Enter" && !event.shiftKey && !event.ctrlKey) { - event.preventDefault() - createNewSession(input) - } - }} - placeholder="Votre message..." - disabled={isLoading} - className={css({ - flex: 1, - })} - /> - { - const files = event.target.files - if (files && files.length > 0) { - const localFiles = Array.from(files).map((file) => ({ - id: `${file.name}-${file.size}-${file.lastModified}-${crypto.randomUUID()}`, - file, - name: file.name, - })) - setPendingFiles((prev) => [ - ...prev, - ...localFiles, - ]) - } - event.target.value = "" - }} - /> - {pendingFiles.length > 0 && ( -
- - Fichiers ajoutés: {pendingFiles.length} - -
- {pendingFiles.map((pendingFile) => ( - - - - {pendingFile.name} - - - - ))} -
-
- )} -
- - - - - -
- - Contexte de la session - - - Ce contexte guide les réponses de l'assistant pour la session. - -
- -
-
- - Exercice - - setSelectedYearId(value ?? undefined)} - allowEmpty={true} - placeholder="Sélectionner un exercice" - options={ - yearsData === undefined - ? [] - : yearsData.map((year) => ({ - key: year.id, - label: year.label, - })) - } - /> -
- -
- - Instructions - - setCustomInstructions(value ?? "")} - placeholder="Ex: Réponds de manière détaillée, utilise le compte 411 pour les clients..." - /> -
-
-
-
- - -
-
-
- - {/* Suggestion chips */} -
- {suggestionChips.map((chipText) => ( - - ))} -
- - {/* Disclaimer */} -

- L'assistant peut faire des erreurs. Vérifiez les informations importantes.{" "} - - En savoir plus - -

-
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/agent/agentSessionContent.tsx b/packages/website/src/features/dashboard/$idOrganization/agent/agentSessionContent.tsx deleted file mode 100644 index 967e2911..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/agent/agentSessionContent.tsx +++ /dev/null @@ -1,1271 +0,0 @@ -import { - createOneAgentFileRouteDefinition, - createOneAgentMessageRouteDefinition, - deleteOneAgentSessionRouteDefinition, - getStreamForAgentMessageRouteDefinition, - readAllAgentMessagesRouteDefinition, - readAllAgentSessionsRouteDefinition, - readAllYearsRouteDefinition, - readOneAgentSessionRouteDefinition, - updateOneAgentSessionRouteDefinition, -} from "@arrhes/application-metadata" -import { - Button, - ButtonGhostContent, - ButtonOutlineContent, - ButtonPlainContent, - CircularLoader, - InputSelect, - InputTextArea, - toast, -} from "@arrhes/ui" -import { css } from "@arrhes/ui/css" -import { - IconChevronRight, - IconDotsVertical, - IconNotebook, - IconPaperclip, - IconSend, - IconTrash, - IconX, -} from "@tabler/icons-react" -import { useNavigate, useParams } from "@tanstack/react-router" -import { useCallback, useEffect, useRef, useState } from "react" -import { DataWrapper } from "../../../../components/layouts/dataWrapper.tsx" -import { ConfirmationModal } from "../../../../components/overlays/dialog/confirmationModal.tsx" -import { Popover } from "../../../../components/overlays/popover/popover.tsx" -import { dataClient } from "../../../../contexts/data/queryClient.ts" -import { agentSessionRoute } from "../../../../routes/root/dashboard/agent/agentSessionRoute.tsx" -import { getCookie } from "../../../../utilities/cookies/getCookie.js" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../utilities/invalidateData.ts" -import { resolveApiBaseUrl } from "../../../../utilities/resolveApiBaseUrl.js" -import { useDataFromAPI } from "../../../../utilities/useHTTPData.ts" -import { cookiePrefix } from "../../../../utilities/variables.js" -import { AgentMessage } from "./agentMessage.tsx" -import { isHealthyStreamResponse } from "./isStreamResponseUnavailable.ts" -import { MentionInput, type MentionReference } from "./mentionInput.tsx" - -const subagentLabels: Record = { - data_analyst: "Analyste de données", - entry_creator: "Créateur d'écritures", - document_processor: "Traitement de documents", - auditor: "Auditeur", -} - -function SubagentIndicator(props: { - subagents: Array<{ - role: string - depth: number - content: string - }> -}) { - const [expanded, setExpanded] = useState>({}) - - return ( -
- {props.subagents.map((subagent, index) => { - const isExpanded = expanded[index] ?? false - const label = subagentLabels[subagent.role] ?? subagent.role - - return ( -
- - {isExpanded && subagent.content && ( -
- {subagent.content} -
- )} -
- ) - })} -
- ) -} - -function ContextInitialiser(props: { - agentSession: { - idYear: string | null - customInstructions: string | null - } - contextInitialisedRef: React.MutableRefObject - setEditYear: (v: string | null | undefined) => void - setEditInstructions: (v: string | null | undefined) => void - children: React.ReactNode -}) { - useEffect(() => { - if (!props.contextInitialisedRef.current) { - props.contextInitialisedRef.current = true - props.setEditYear(props.agentSession.idYear ?? null) - props.setEditInstructions(props.agentSession.customInstructions ?? "") - } - }, [ - props.agentSession, - props.contextInitialisedRef, - props.setEditInstructions, - props.setEditYear, - ]) - - return <>{props.children} -} - -export function AgentSessionContent() { - const params = useParams({ - from: agentSessionRoute.id, - }) - - const [input, setInput] = useState(undefined) - const [draftReferences, setDraftReferences] = useState([]) - const messagesEndRef = useRef(null) - const _scrollContainerRef = useRef(null) - const navigate = useNavigate() - const [isDeleting, setIsDeleting] = useState(false) - const [deleteOpen, setDeleteOpen] = useState(false) - const [editYear, setEditYear] = useState(undefined) - const [editInstructions, setEditInstructions] = useState(undefined) - const [isSavingContext, setIsSavingContext] = useState(false) - const contextInitialisedRef = useRef(false) - const [pendingFiles, setPendingFiles] = useState([]) - const fileInputRef = useRef(null) - - const { data: yearsData } = useDataFromAPI({ - routeDefinition: readAllYearsRouteDefinition, - body: {}, - }) - - const handleSaveContext = useCallback(async () => { - setIsSavingContext(true) - try { - const result = await getResponseBodyFromAPI({ - routeDefinition: updateOneAgentSessionRouteDefinition, - body: { - idAgentSession: params.idAgentSession, - idYear: editYear || null, - customInstructions: editInstructions?.trim() || null, - }, - }) - if (result.ok) { - toast({ - title: "Contexte mis à jour", - variant: "success", - }) - await invalidateData({ - routeDefinition: readOneAgentSessionRouteDefinition, - body: { - idAgentSession: params.idAgentSession, - }, - }) - } else { - toast({ - title: "Erreur lors de la mise à jour", - variant: "error", - }) - } - } finally { - setIsSavingContext(false) - } - }, [ - params.idAgentSession, - editYear, - editInstructions, - ]) - - const handleDeleteSession = useCallback(async () => { - const sessionId = params.idAgentSession - setIsDeleting(true) - try { - await getResponseBodyFromAPI({ - routeDefinition: deleteOneAgentSessionRouteDefinition, - body: { - idAgentSession: sessionId, - }, - }) - await dataClient.invalidateQueries({ - queryKey: [ - readAllAgentSessionsRouteDefinition.path, - ], - exact: false, - }) - navigate({ - to: "/dashboard/organisations/$idOrganization/agent", - params: { - idOrganization: params.idOrganization, - }, - }) - } finally { - setIsDeleting(false) - } - }, [ - navigate, - params.idAgentSession, - params.idOrganization, - ]) - - // ── Streaming state ───────────────────────────────────────────────────────── - // ID of the assistant message currently being streamed from the worker - const [streamMessageId, setStreamMessageId] = useState(undefined) - const [isSending, setIsSending] = useState(false) - // Accumulates the text content of the in-progress assistant message - const [streamingContent, setStreamingContent] = useState("") - const [streamingToolCalls, setStreamingToolCalls] = useState([]) - const [isStreaming, setIsStreaming] = useState(false) - const [activeSubagents, setActiveSubagents] = useState< - Array<{ - role: string - depth: number - content: string - }> - >([]) - const abortStreamRef = useRef(null) - - // ── Auto-scroll to bottom on new content ──────────────────────────────── - const scrollToBottom = useCallback(() => { - messagesEndRef.current?.scrollIntoView({ - behavior: "smooth", - }) - }, []) - - // Scroll when streaming content or tool calls update - useEffect(() => { - if (isStreaming) scrollToBottom() - }, [ - isStreaming, - scrollToBottom, - ]) - - // ── Auto-detect streaming messages on page load / navigation ──────────── - // If the user opens a session that already has a message in "streaming" - // state (e.g. page refresh while the worker is running), start the SSE - // connection for it automatically. - const { data: messagesData } = useDataFromAPI({ - routeDefinition: readAllAgentMessagesRouteDefinition, - body: { - idAgentSession: params.idAgentSession, - }, - }) - - // Scroll when messages list changes (new message added, stream completed) - useEffect(() => { - if (messagesData) scrollToBottom() - }, [ - messagesData, - scrollToBottom, - ]) - - useEffect(() => { - if (streamMessageId) return // already streaming something - if (!messagesData) return - - const streamingMsg = messagesData.find((m) => m.state === "streaming") - if (streamingMsg) { - setStreamMessageId(streamingMsg.id) - } - }, [ - messagesData, - streamMessageId, - ]) - - // Single effect: open SSE stream, poll as fallback, self-cleanup - useEffect(() => { - if (!streamMessageId) return - - // Abort any previous stream - abortStreamRef.current?.abort() - const controller = new AbortController() - abortStreamRef.current = controller - - setIsStreaming(true) - setStreamingContent("") - setStreamingToolCalls([]) - setActiveSubagents([]) - - let accumulated = "" - const accumulatedToolCalls: unknown[] = [] - let lastBoundaryLen = 0 - const seenEnds = new Set() - let finished = false - const subagentStack: Array<{ - role: string - depth: number - content: string - }> = [] - - const finish = async () => { - if (finished) return - finished = true - await invalidateData({ - routeDefinition: readAllAgentMessagesRouteDefinition, - body: { - idAgentSession: params.idAgentSession, - }, - }) - setStreamingContent("") - setStreamingToolCalls([]) - setIsStreaming(false) - setStreamMessageId(undefined) - } - - // SSE stream - ;(async () => { - let streamCompleted = false - try { - const headers: Record = { - "Content-Type": "application/json", - } - const orgCookie = getCookie(`${cookiePrefix}_id_organization`) - if (orgCookie) { - headers["X-Organization-Id"] = orgCookie - } - - const apiBaseUrl = resolveApiBaseUrl(import.meta.env.VITE_API_BASE_URL) - if (!apiBaseUrl) { - throw new Error("VITE_API_BASE_URL is not configured") - } - - const response = await fetch(new URL(`${apiBaseUrl}${getStreamForAgentMessageRouteDefinition.path}`), { - method: "POST", - credentials: "include", - signal: controller.signal, - headers, - body: JSON.stringify({ - idOrganization: params.idOrganization, - idAgentMessage: streamMessageId, - }), - }) - - if (!isHealthyStreamResponse(response)) { - toast({ - title: "Le flux de reponse est indisponible", - description: "Veuillez renvoyer votre message.", - variant: "error", - }) - if (!controller.signal.aborted) { - await finish() - } - return - } - - const reader = response.body.getReader() - const decoder = new TextDecoder() - let buffer = "" - - while (true) { - const { done, value } = await reader.read() - if (done) break - - buffer += decoder.decode(value, { - stream: true, - }) - - const parts = buffer.split("\n\n") - buffer = parts.pop() ?? "" - - for (const part of parts) { - for (const line of part.split("\n")) { - if (!line.startsWith("data: ")) continue - const jsonStr = line.slice(6).trim() - if (!jsonStr) continue - - try { - const chunk = JSON.parse(jsonStr) - if (chunk.type === "TEXT_MESSAGE_CONTENT" && typeof chunk.delta === "string") { - if (chunk.subagentSkills && subagentStack.length > 0) { - // Accumulate subagent content - const current = subagentStack[subagentStack.length - 1] - if (current) { - current.content += chunk.delta - setActiveSubagents([ - ...subagentStack, - ]) - } - } else { - accumulated += chunk.delta - setStreamingContent(accumulated) - } - } - if (chunk.type === "TOOL_CALL_START") { - // Emit text boundary if text accumulated since last boundary - if (accumulated.length > lastBoundaryLen) { - accumulatedToolCalls.push({ - type: "TEXT_BOUNDARY", - contentLength: accumulated.length, - }) - lastBoundaryLen = accumulated.length - } - accumulatedToolCalls.push(chunk) - setStreamingToolCalls([ - ...accumulatedToolCalls, - ]) - } - if (chunk.type === "CONTEXT_LIMIT_WARNING") { - toast({ - title: "La conversation approche de sa limite de contexte", - description: `${chunk.usage}% de la capacité utilisée. Envisagez de créer une nouvelle session.`, - variant: "warning", - }) - } - if (chunk.type === "TOOL_CALL_END") { - // Deduplicate: framework re-emits TOOL_CALL_END after RUN_FINISHED - const tcId = chunk.toolCallId as string | undefined - if (tcId && seenEnds.has(tcId)) continue - if (tcId) seenEnds.add(tcId) - accumulatedToolCalls.push(chunk) - setStreamingToolCalls([ - ...accumulatedToolCalls, - ]) - } - if (chunk.type === "SUBAGENT_RUN_START") { - subagentStack.push({ - role: (chunk.skills as string) ?? "subagent", - depth: chunk.depth as number, - content: "", - }) - setActiveSubagents([ - ...subagentStack, - ]) - } - if (chunk.type === "SUBAGENT_RUN_END") { - subagentStack.pop() - setActiveSubagents([ - ...subagentStack, - ]) - } - } catch { - // ignore malformed chunks - } - } - } - } - - streamCompleted = true - } catch (err: unknown) { - if (err instanceof Error && err.name === "AbortError") return - console.error("[stream] SSE error — falling back to polling", err) - } finally { - // Only finish when the stream completed naturally. - // If the SSE failed (network/auth error), let the polling - // fallback continue running so it can detect completion. - if (streamCompleted && !controller.signal.aborted) { - finish() - } - } - })() - - // Polling failsafe: if SSE fails or is slow, periodically - // check the DB for the completed message - const poll = setInterval(async () => { - try { - const result = await getResponseBodyFromAPI({ - routeDefinition: readAllAgentMessagesRouteDefinition, - body: { - idAgentSession: params.idAgentSession, - }, - signal: controller.signal, - }) - if (!result.ok || !result.data) return - - const msg = result.data.find((m) => m.id === streamMessageId) - if (msg && msg.state !== "streaming") { - controller.abort() - finish() - } - } catch { - // ignore polling errors - } - }, 3000) - - return () => { - clearInterval(poll) - controller.abort() - } - }, [ - streamMessageId, - params.idAgentSession, - params.idOrganization, - ]) - - // ── Send message ──────────────────────────────────────────────────────────── - const sendMessageToWorker = useCallback( - async ( - text: string, - references?: MentionReference[], - options?: { - idYear?: string | null - attachedFiles?: Array<{ - idFile: string - name: string - }> | null - }, - ) => { - if (!text.trim() || isSending) return - setIsSending(true) - - try { - if (pendingFiles.length > 0) { - if (!options?.idYear) { - toast({ - title: "Veuillez sélectionner un exercice pour importer des fichiers", - variant: "warning", - }) - return - } - - const existingIds = (options.attachedFiles ?? []).map((file) => file.idFile) - const newIds: string[] = [] - const failedFiles: File[] = [] - - for (const file of pendingFiles) { - const hashBuffer = await crypto.subtle.digest("SHA-256", await file.arrayBuffer()) - const fileHash = Array.from(new Uint8Array(hashBuffer)) - .map((b) => b.toString(16).padStart(2, "0")) - .join("") - - const createFileResponse = await getResponseBodyFromAPI({ - routeDefinition: createOneAgentFileRouteDefinition, - body: { - idOrganization: params.idOrganization, - idAgentSession: params.idAgentSession, - fileName: file.name, - fileType: file.type || "application/octet-stream", - fileSize: file.size, - fileHash, - }, - }) - - if (createFileResponse.ok === false) { - toast({ - title: `Impossible d'importer ${file.name}`, - variant: "error", - }) - failedFiles.push(file) - continue - } - - if (createFileResponse.data.url) { - const uploadResponse = await fetch(createFileResponse.data.url, { - method: "PUT", - headers: { - "Content-Type": file.type || "application/octet-stream", - }, - body: file, - }) - if (!uploadResponse.ok) { - toast({ - title: `Échec de l'envoi de ${file.name}`, - variant: "error", - }) - failedFiles.push(file) - continue - } - } - - newIds.push(createFileResponse.data.file.id) - } - - if (newIds.length > 0) { - const attachResult = await getResponseBodyFromAPI({ - routeDefinition: updateOneAgentSessionRouteDefinition, - body: { - idAgentSession: params.idAgentSession, - fileIds: [ - ...existingIds, - ...newIds, - ], - }, - }) - - if (!attachResult.ok) { - toast({ - title: "Impossible d'attacher les fichiers a la session", - variant: "error", - }) - return - } - - await invalidateData({ - routeDefinition: readOneAgentSessionRouteDefinition, - body: { - idAgentSession: params.idAgentSession, - }, - }) - } - - setPendingFiles(failedFiles) - } - - const result = await getResponseBodyFromAPI({ - routeDefinition: createOneAgentMessageRouteDefinition, - body: { - idOrganization: params.idOrganization, - idAgentSession: params.idAgentSession, - message: text.trim(), - references: references && references.length > 0 ? references : null, - }, - }) - - if (!result.ok || !result.data) { - toast({ - title: "Impossible de créer le message", - variant: "error", - }) - return - } - - const { id: idAgentMessage } = result.data - - setStreamMessageId(idAgentMessage) - - await invalidateData({ - routeDefinition: readAllAgentMessagesRouteDefinition, - body: { - idAgentSession: params.idAgentSession, - }, - }) - scrollToBottom() - return true - } catch (error) { - console.error("[sendMessageToWorker]", error) - toast({ - title: "Une erreur est survenue lors de l'envoi du message", - variant: "error", - }) - return false - } finally { - setIsSending(false) - } - }, - [ - isSending, - params.idOrganization, - params.idAgentSession, - pendingFiles, - scrollToBottom, - ], - ) - - const [_historyIndex, setHistoryIndex] = useState(-1) - - const isSubmitting = isSending || isStreaming - - return ( - - {(agentSession) => { - const _hasContext = Boolean(agentSession.idYear || agentSession.customInstructions?.trim()) - - return ( - -
- {/* Messages area */} - - {(agentMessages) => { - const sortedAgentMessages = agentMessages.sort( - (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(), - ) - - return ( -
- {sortedAgentMessages.map((agentMessage) => { - // If this is the message being streamed, show live content - if (agentMessage.id === streamMessageId && isStreaming) { - return ( -
- 0 - ? streamingToolCalls - : agentMessage.toolCalls, - }} - /> - {activeSubagents.length > 0 && ( - - )} -
- -
-
- ) - } - return ( - - ) - })} - - {/* Thinking indicator — waiting for first token */} - {isSending && !isStreaming && ( -
- -
- )} - -
-
- ) - }} - - - {/* Input area */} -
- { - void sendMessageToWorker(text, references, { - idYear: agentSession.idYear, - attachedFiles: (agentSession.attachedFiles ?? []) as Array<{ - idFile: string - name: string - }>, - }) - }} - onValueChange={(text, references) => { - setInput(text) - setDraftReferences(references) - }} - disabled={isSubmitting} - idOrganization={params.idOrganization} - idYear={agentSession.idYear} - /> - { - const files = event.target.files - if (!files || files.length === 0) return - const newFiles = Array.from(files) - event.target.value = "" - - setPendingFiles((prev) => [ - ...prev, - ...newFiles, - ]) - }} - /> - {/* File chips */} - {(() => { - const sessionFiles = (agentSession.attachedFiles ?? []) as Array<{ - idFile: string - name: string - }> - const hasFiles = sessionFiles.length > 0 || pendingFiles.length > 0 - if (!hasFiles) return null - return ( -
- {sessionFiles.length > 0 && ( -
- {sessionFiles.map((file) => ( - - - - {file.name} - - - - ))} -
- )} - - {pendingFiles.length > 0 && ( -
- - Fichiers ajoutés: {pendingFiles.length} - -
- {pendingFiles.map((file, index) => ( - - - - {file.name} - - - - ))} -
-
- )} -
- ) - })()} -
- - - - - - - - - - - - - - - -
- - Contexte de la session - - - Ce contexte guide les réponses de l'assistant pour cette session. - -
- -
-
- - Exercice - - setEditYear(value)} - allowEmpty={true} - placeholder="Sélectionner un exercice" - options={ - yearsData === undefined - ? [] - : yearsData.map((year) => ({ - key: year.id, - label: year.label, - })) - } - /> -
- -
- - Instructions - - setEditInstructions(value)} - placeholder="Ex: Réponds de manière détaillée, utilise le compte 411 pour les clients..." - /> -
-
- - -
-
- - -
- -
-
- - ) - }} -
- ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/agent/agentSessionPage.tsx b/packages/website/src/features/dashboard/$idOrganization/agent/agentSessionPage.tsx deleted file mode 100644 index 470c47dc..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/agent/agentSessionPage.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { css } from "@arrhes/ui/css" -import { AgentSessionContent } from "./agentSessionContent.tsx" - -export function AgentSessionPage() { - return ( -
- -
- ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationApi/OrganizationApiGeneralPage.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationApi/OrganizationApiGeneralPage.tsx new file mode 100644 index 00000000..fb06013d --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationApi/OrganizationApiGeneralPage.tsx @@ -0,0 +1,35 @@ +import { ButtonOutlineContent } from "@arrhes/ui" +import { IconBook2 } from "@tabler/icons-react" +import { LinkButton } from "../../../../components/LinkButton.tsx" +import { Page } from "../../../../components/layouts/page/page.tsx" +import { SettingsSection } from "../../../../components/layouts/settingsSection/settingsSection.tsx" + +export function OrganizationApiGeneralPage() { + return ( + + + + + + + } + text="Accéder à la documentation" + /> + + + + + + ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationApi/OrganizationApiLayout.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationApi/OrganizationApiLayout.tsx new file mode 100644 index 00000000..85d08579 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationApi/OrganizationApiLayout.tsx @@ -0,0 +1,5 @@ +import { Outlet } from "@tanstack/react-router" + +export function OrganizationApiLayout() { + return +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationApi/OrganizationApiTabContent.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationApi/OrganizationApiTabContent.tsx new file mode 100644 index 00000000..68de1e75 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationApi/OrganizationApiTabContent.tsx @@ -0,0 +1,48 @@ +import { IconHome, IconKey } from "@tabler/icons-react" +import { lazy, Suspense } from "react" +import { SubPageContent } from "../../../../components/layouts/SubPageContent.tsx" + +const OrganizationApiGeneralPage = lazy(() => + import("./OrganizationApiGeneralPage.js").then((m) => ({ + default: m.OrganizationApiGeneralPage, + })), +) +const OrganizationApiKeysPage = lazy(() => + import("./keys/OrganizationApiKeysPage.js").then((m) => ({ + default: m.OrganizationApiKeysPage, + })), +) + +export function OrganizationApiTabContent(props: { idOrganization: string }) { + return ( + , + content: ( + + + + ), + }, + { + key: "clés", + label: "Clés", + icon: , + content: ( + + + + ), + }, + ], + }, + }} + /> + ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/ApiKeyListTableRow.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/ApiKeyListTableRow.tsx new file mode 100644 index 00000000..6db0c494 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/ApiKeyListTableRow.tsx @@ -0,0 +1,78 @@ +import type { readAllApiKeysRouteDefinition } from "@arrhes/application-metadata/routes" +import { ButtonOutlineContent, Chip } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconTrash } from "@tabler/icons-react" +import type * as v from "valibot" +import { ListTable } from "../../../../../components/layouts/listTable/listTable.tsx" +import { DeleteOneApiKey } from "./DeleteOneApiKey.tsx" + +export function ApiKeyListTableRow(props: { + apiKey: v.InferOutput[number] +}) { + return ( + +
+
+ + {props.apiKey.name} + +
+ {props.apiKey.isActive ? ( + + ) : ( + + )} +
+
+
+ + } + title="Supprimer la clé" + color="danger" + /> + +
+
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/ApiKeysListTable.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/ApiKeysListTable.tsx new file mode 100644 index 00000000..e7641890 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/ApiKeysListTable.tsx @@ -0,0 +1,37 @@ +import { readAllApiKeysRouteDefinition } from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { IconKey } from "@tabler/icons-react" +import type * as v from "valibot" +import { DataWrapper } from "../../../../../components/layouts/DataWrapper.tsx" +import { EmptyState } from "../../../../../components/layouts/EmptyState.tsx" +import { ListTable } from "../../../../../components/layouts/listTable/listTable.tsx" +import { ApiKeyListTableRow } from "./ApiKeyListTableRow.tsx" + +export function ApiKeysListTable(_props: { idOrganization: v.InferOutput["id"] }) { + return ( + + + {(apiKeys) => { + if (apiKeys.length === 0) { + return ( + } + title="Aucune clé API" + subtitle="Créez une clé API pour commencer" + /> + ) + } + return apiKeys.map((apiKey) => ( + + )) + }} + + + ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/CreateOneApiKey.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/CreateOneApiKey.tsx new file mode 100644 index 00000000..cb5e3845 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/CreateOneApiKey.tsx @@ -0,0 +1,133 @@ +import { createOneApiKeyRouteDefinition, readAllApiKeysRouteDefinition } from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, InputText, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPlus } from "@tabler/icons-react" +import { type JSX, useState } from "react" +import { Fragment } from "react/jsx-runtime" +import type * as v from "valibot" +import { FormControl } from "../../../../../components/forms/FormControl.tsx" +import { FormError } from "../../../../../components/forms/FormError.tsx" +import { FormField } from "../../../../../components/forms/FormField.tsx" +import { FormItem } from "../../../../../components/forms/FormItem.tsx" +import { FormLabel } from "../../../../../components/forms/FormLabel.tsx" +import { FormRoot } from "../../../../../components/forms/FormRoot.tsx" +import { useTabs } from "../../../../../contexts/tabs/useTabs.tsx" +import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../utilities/invalidateData.ts" +import { RawKeyDisplay } from "./RawKeyDisplay.tsx" + +function CreateOneApiKeyPanel(props: { onClose: () => void }) { + const [rawKey, setRawKey] = useState(null) + + return ( +
+ {rawKey !== null ? ( + + ) : ( + , + text: "Créer la clé API", + }} + onSubmit={async (data) => { + const response = await getResponseBodyFromAPI({ + routeDefinition: createOneApiKeyRouteDefinition, + body: data, + }) + if (!response.ok) { + toast({ + title: "Impossible de créer la clé API", + variant: "error", + }) + return false + } + + setRawKey(response.data.rawKey) + toast({ + title: "Clé API créée avec succès", + variant: "success", + }) + return true + }} + onCancel={undefined} + onSuccess={async () => { + await invalidateData({ + routeDefinition: readAllApiKeysRouteDefinition, + body: {}, + }) + }} + > + {(form) => ( + + ( + + + + + + + + )} + /> + + )} + + )} +
+ ) +} + +export function CreateOneApiKey(props: { + idOrganization: v.InferOutput["id"] + children: JSX.Element +}) { + const { openPanelTab, closeTab } = useTabs() + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/DeleteOneApiKey.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/DeleteOneApiKey.tsx new file mode 100644 index 00000000..587bc3d6 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/DeleteOneApiKey.tsx @@ -0,0 +1,81 @@ +import type { readAllApiKeysRouteDefinition as ReadAllApiKeysRouteDefinition } from "@arrhes/application-metadata/routes" +import { deleteOneApiKeyRouteDefinition, readAllApiKeysRouteDefinition } from "@arrhes/application-metadata/routes" +import { Button, ButtonOutlineContent, ButtonPlainContent, Dialog, toast, useModalStore } from "@arrhes/ui" +import { type ComponentPropsWithRef, type ReactElement, useId } from "react" +import type * as v from "valibot" +import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../utilities/invalidateData.ts" + +export function DeleteOneApiKey(props: { + apiKey: v.InferOutput[number] + children: ReactElement> +}) { + const modalId = useId() + const { open: openModal, close: closeModal } = useModalStore() + + async function onSubmit() { + const deleteResponse = await getResponseBodyFromAPI({ + routeDefinition: deleteOneApiKeyRouteDefinition, + body: { + idApiKey: props.apiKey.id, + }, + }) + + if (deleteResponse.ok === false) { + toast({ + title: "Erreur lors de la suppression de la clé API", + variant: "error", + }) + return + } + + await invalidateData({ + routeDefinition: readAllApiKeysRouteDefinition, + body: {}, + }) + toast({ + title: "Clé API supprimée avec succès", + variant: "success", + }) + } + + return ( + + + + , + ) + } + > + {props.children} + + ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/OrganizationApiKeysPage.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/OrganizationApiKeysPage.tsx new file mode 100644 index 00000000..ac308c0f --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/OrganizationApiKeysPage.tsx @@ -0,0 +1,39 @@ +import { ButtonOutlineContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPlus } from "@tabler/icons-react" +import { useParams } from "@tanstack/react-router" +import { Page } from "../../../../../components/layouts/page/page.tsx" +import { ApiKeysListTable } from "./ApiKeysListTable.tsx" +import { CreateOneApiKey } from "./CreateOneApiKey.tsx" + +export function OrganizationApiKeysPage({ idOrganization: idOrganizationProp }: { idOrganization?: string } = {}) { + const params = useParams({ + strict: false, + }) as { + idOrganization?: string + } + const idOrganization = idOrganizationProp ?? params.idOrganization ?? "" + + return ( + + +
+ + } + text="Créer une clé API" + /> + +
+ +
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/rawKeyDisplay.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/RawKeyDisplay.tsx similarity index 100% rename from packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/rawKeyDisplay.tsx rename to packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/RawKeyDisplay.tsx diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/apiKeyListTableRow.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/apiKeyListTableRow.tsx deleted file mode 100644 index 00915fe9..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/apiKeyListTableRow.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import type { readAllApiKeysRouteDefinition } from "@arrhes/application-metadata/routes" -import { ButtonOutlineContent, Chip } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconTrash } from "@tabler/icons-react" -import type * as v from "valibot" -import { ListTable } from "../../../../../components/layouts/listTable/listTable.tsx" -import { DeleteOneApiKey } from "./deleteOneApiKey.tsx" - -export function ApiKeyListTableRow(props: { - apiKey: v.InferOutput[number] -}) { - return ( - -
-
- - {props.apiKey.name} - -
- {props.apiKey.isActive ? ( - - ) : ( - - )} -
-
-
- - } - title="Supprimer la clé" - color="danger" - /> - -
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/apiKeysListTable.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/apiKeysListTable.tsx deleted file mode 100644 index 7d65774b..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/apiKeysListTable.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { readAllApiKeysRouteDefinition } from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { IconKey } from "@tabler/icons-react" -import type * as v from "valibot" -import { DataWrapper } from "../../../../../components/layouts/dataWrapper.tsx" -import { EmptyState } from "../../../../../components/layouts/emptyState.tsx" -import { ListTable } from "../../../../../components/layouts/listTable/listTable.tsx" -import { ApiKeyListTableRow } from "./apiKeyListTableRow.tsx" - -export function ApiKeysListTable(_props: { idOrganization: v.InferOutput["id"] }) { - return ( - - - {(apiKeys) => { - if (apiKeys.length === 0) { - return ( - } - title="Aucune clé API" - subtitle="Créez une clé API pour commencer" - /> - ) - } - return apiKeys.map((apiKey) => ( - - )) - }} - - - ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/createOneApiKey.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/createOneApiKey.tsx deleted file mode 100644 index 36760ea4..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/createOneApiKey.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { createOneApiKeyRouteDefinition, readAllApiKeysRouteDefinition } from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { InputText, toast } from "@arrhes/ui" -import { IconPlus } from "@tabler/icons-react" -import { type JSX, useState } from "react" -import { Fragment } from "react/jsx-runtime" -import type * as v from "valibot" -import { FormControl } from "../../../../../components/forms/formControl.tsx" -import { FormError } from "../../../../../components/forms/formError.tsx" -import { FormField } from "../../../../../components/forms/formField.tsx" -import { FormItem } from "../../../../../components/forms/formItem.tsx" -import { FormLabel } from "../../../../../components/forms/formLabel.tsx" -import { FormRoot } from "../../../../../components/forms/formRoot.tsx" -import { Drawer } from "../../../../../components/overlays/drawer/drawer.tsx" -import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../utilities/invalidateData.ts" -import { RawKeyDisplay } from "./rawKeyDisplay.tsx" - -export function CreateOneApiKey(props: { - idOrganization: v.InferOutput["id"] - children: JSX.Element -}) { - const [open, setOpen] = useState(false) - const [rawKey, setRawKey] = useState(null) - - return ( - - { - setOpen(value) - setRawKey(null) - }} - > - {props.children} - - - - {rawKey !== null ? ( - setOpen(false)} - /> - ) : ( - , - text: "Créer la clé API", - }} - onSubmit={async (data) => { - const response = await getResponseBodyFromAPI({ - routeDefinition: createOneApiKeyRouteDefinition, - body: data, - }) - if (!response.ok) { - toast({ - title: "Impossible de créer la clé API", - variant: "error", - }) - return false - } - - setRawKey(response.data.rawKey) - toast({ - title: "Clé API créée avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await invalidateData({ - routeDefinition: readAllApiKeysRouteDefinition, - body: {}, - }) - }} - > - {(form) => ( - - ( - - - - - - - - )} - /> - - )} - - )} - - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/deleteOneApiKey.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/deleteOneApiKey.tsx deleted file mode 100644 index e0426f7b..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/deleteOneApiKey.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import type { readAllApiKeysRouteDefinition as ReadAllApiKeysRouteDefinition } from "@arrhes/application-metadata/routes" -import { deleteOneApiKeyRouteDefinition, readAllApiKeysRouteDefinition } from "@arrhes/application-metadata/routes" -import { toast } from "@arrhes/ui" -import type { ComponentPropsWithRef, ReactElement } from "react" -import type * as v from "valibot" -import { ConfirmationModal } from "../../../../../components/overlays/dialog/confirmationModal.tsx" -import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../utilities/invalidateData.ts" - -export function DeleteOneApiKey(props: { - apiKey: v.InferOutput[number] - children: ReactElement> -}) { - async function onSubmit() { - const deleteResponse = await getResponseBodyFromAPI({ - routeDefinition: deleteOneApiKeyRouteDefinition, - body: { - idApiKey: props.apiKey.id, - }, - }) - - if (deleteResponse.ok === false) { - toast({ - title: "Erreur lors de la suppression de la clé API", - variant: "error", - }) - return - } - - await invalidateData({ - routeDefinition: readAllApiKeysRouteDefinition, - body: {}, - }) - toast({ - title: "Clé API supprimée avec succès", - variant: "success", - }) - } - - return ( - - {props.children} - - ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/organizationApiKeysPage.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/organizationApiKeysPage.tsx deleted file mode 100644 index 92205184..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationApi/keys/organizationApiKeysPage.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { ButtonOutlineContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconPlus } from "@tabler/icons-react" -import { useParams } from "@tanstack/react-router" -import { Page } from "../../../../../components/layouts/page/page.tsx" -import { organizationApiKeysRoute } from "../../../../../routes/root/dashboard/organizations/$idOrganization/organizationApi/organizationApiKeysRoute.tsx" -import { ApiKeysListTable } from "./apiKeysListTable.tsx" -import { CreateOneApiKey } from "./createOneApiKey.tsx" - -export function OrganizationApiKeysPage() { - const params = useParams({ - from: organizationApiKeysRoute.id, - }) - - return ( - - -
- - } - text="Créer une clé API" - /> - -
- -
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationApi/organizationApiGeneralPage.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationApi/organizationApiGeneralPage.tsx deleted file mode 100644 index a9b6a5b9..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationApi/organizationApiGeneralPage.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { ButtonOutlineContent } from "@arrhes/ui" -import { IconBook2 } from "@tabler/icons-react" -import { Page } from "../../../../components/layouts/page/page.tsx" -import { SettingsSection } from "../../../../components/layouts/settingsSection/settingsSection.tsx" -import { LinkButton } from "../../../../components/linkButton.tsx" - -export function OrganizationApiGeneralPage() { - return ( - - - - - - - } - text="Accéder à la documentation" - /> - - - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationApi/organizationApiLayout.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationApi/organizationApiLayout.tsx deleted file mode 100644 index f47d516c..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationApi/organizationApiLayout.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { IconHome, IconKey } from "@tabler/icons-react" -import { useParams } from "@tanstack/react-router" -import { SubPageLayout } from "../../../../components/layouts/subPageLayout.tsx" -import { organizationApiLayoutRoute } from "../../../../routes/root/dashboard/organizations/$idOrganization/organizationApi/organizationApiLayoutRoute.tsx" - -export function OrganizationApiLayout() { - const params = useParams({ - from: organizationApiLayoutRoute.id, - }) - - return ( - , - to: "/dashboard/organisations/$idOrganization/api", - params: { - idOrganization: params.idOrganization, - }, - }, - { - label: "Clés", - icon: , - to: "/dashboard/organisations/$idOrganization/api/clés", - params: { - idOrganization: params.idOrganization, - }, - }, - ], - }, - }} - /> - ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/CancelSubscription.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/CancelSubscription.tsx new file mode 100644 index 00000000..e118ce68 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/CancelSubscription.tsx @@ -0,0 +1,84 @@ +import { + cancelSubscriptionRouteDefinition, + readOrganizationBillingRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { Button, ButtonOutlineContent, ButtonPlainContent, Dialog, toast, useModalStore } from "@arrhes/ui" +import { IconPlayerPause } from "@tabler/icons-react" +import { useId } from "react" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../utilities/invalidateData.ts" + +export function CancelSubscription(_props: { idOrganization: string }) { + const modalId = useId() + const { open: openModal, close: closeModal } = useModalStore() + + async function onSubmit() { + const response = await getResponseBodyFromAPI({ + routeDefinition: cancelSubscriptionRouteDefinition, + body: {}, + }) + + if (response.ok === false) { + toast({ + title: "Erreur lors de la mise en pause de l'abonnement", + variant: "error", + }) + return + } + + await invalidateData({ + routeDefinition: readOrganizationBillingRouteDefinition, + body: {}, + }) + + toast({ + title: "Abonnement mis en pause", + variant: "success", + }) + } + + return ( + + + + , + ) + } + > + } + text="Mettre en pause l'abonnement" + color="danger" + /> + + ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/OrganizationBillingHistoryPage.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/OrganizationBillingHistoryPage.tsx new file mode 100644 index 00000000..c5434ee2 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/OrganizationBillingHistoryPage.tsx @@ -0,0 +1,179 @@ +import { readAllOrganizationPaymentsRouteDefinition } from "@arrhes/application-metadata/routes" +import { Chip, type ChipColors, FormatPrice, formatDate } from "@arrhes/ui" +import { IconReceipt } from "@tabler/icons-react" +import type { ColumnDef } from "@tanstack/react-table" +import { DataTable } from "../../../../components/layouts/DataTable.tsx" +import { DataWrapper } from "../../../../components/layouts/DataWrapper.tsx" +import { EmptyState } from "../../../../components/layouts/EmptyState.tsx" +import { Page } from "../../../../components/layouts/page/page.tsx" +import { SettingsSection } from "../../../../components/layouts/settingsSection/settingsSection.tsx" + +const statusLabel: Record = { + pending: "En attente", + paid: "Payé", + failed: "Échoué", + refunded: "Remboursé", +} + +const statusColor: Record = { + pending: "warning", + paid: "success", + failed: "error", + refunded: "neutral", +} + +type Payment = { + category: string + flow: "debit" | "credit" + id: string + idInvoice: string + serviceType: string | null + status: string + amountHTInCents: number + amountTVAInCents: number + currency: string + description: string | null + periodStart: string | null + periodEnd: string | null + createdAt: string + sequenceType: string | null +} + +function getSignedAmountHTInCents(payment: Payment): number { + return payment.flow === "debit" ? payment.amountHTInCents : -payment.amountHTInCents +} + +function getSignedAmountTVAInCents(payment: Payment): number { + return payment.flow === "debit" ? payment.amountTVAInCents : -payment.amountTVAInCents +} + +function getPaymentCategoryLabel(category: string): string { + if (category === "top_up") return "Recharge" + if (category === "withdrawal") return "Retrait" + if (category === "wallet_spending") return "Portefeuille" + if (category === "setup") return "Moyen de paiement" + return "Abonnement" +} + +function getPaymentServiceLabel(serviceType: string | null): string { + if (serviceType === "support") return "Licence" + if (serviceType === "storage_gb") return "Stockage" + if (serviceType === "agent_tokens_million") return "Tokens" + if (serviceType === "ocr_pages_hundred") return "OCR" + return "-" +} + +const columns: Array> = [ + { + accessorKey: "createdAt", + header: "Date", + minSize: 150, + size: 170, + cell: (context) => formatDate(context.row.original.createdAt), + }, + { + id: "type", + header: "Type", + minSize: 130, + size: 150, + accessorFn: (payment) => getPaymentCategoryLabel(payment.category), + }, + { + id: "service", + header: "Service", + minSize: 120, + size: 140, + accessorFn: (payment) => getPaymentServiceLabel(payment.serviceType), + }, + { + accessorKey: "description", + header: "Description", + minSize: 280, + size: 360, + cell: (context) => context.row.original.description ?? "-", + }, + { + id: "period", + header: "Période", + minSize: 250, + size: 300, + accessorFn: (payment) => + payment.periodStart && payment.periodEnd + ? `${formatDate(payment.periodStart)} - ${formatDate(payment.periodEnd)}` + : null, + }, + { + id: "status", + header: "Statut", + minSize: 130, + size: 150, + accessorFn: (payment) => statusLabel[payment.status] ?? payment.status, + cell: (context) => { + const status = context.row.original.status + return ( + + ) + }, + }, + { + id: "amountHT", + header: "Montant HT", + minSize: 150, + size: 170, + accessorFn: (payment) => getSignedAmountHTInCents(payment), + cell: (context) => , + }, + { + id: "amountTVA", + header: "TVA", + minSize: 130, + size: 150, + accessorFn: (payment) => getSignedAmountTVAInCents(payment), + cell: (context) => , + }, +] + +export function OrganizationBillingHistoryPage() { + return ( + + + + + + {(payments) => { + if (payments.length === 0) { + return ( + } + title="Aucun paiement" + subtitle="Les paiements apparaîtront ici." + /> + ) + } + + return ( + b.createdAt.localeCompare(a.createdAt))} + columns={columns} + pageSize={12} + defaultColumnVisibility={{ + type: false, + service: false, + }} + /> + ) + }} + + + + + ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/OrganizationBillingLayout.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/OrganizationBillingLayout.tsx new file mode 100644 index 00000000..a1f348d9 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/OrganizationBillingLayout.tsx @@ -0,0 +1,5 @@ +import { Outlet } from "@tanstack/react-router" + +export function OrganizationBillingLayout() { + return +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/OrganizationBillingTabContent.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/OrganizationBillingTabContent.tsx new file mode 100644 index 00000000..f62cb03d --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/OrganizationBillingTabContent.tsx @@ -0,0 +1,78 @@ +import { IconFileInvoice, IconPuzzle, IconReceipt, IconWallet } from "@tabler/icons-react" +import { lazy, Suspense } from "react" +import { SubPageContent } from "../../../../components/layouts/SubPageContent.tsx" + +const OrganizationWalletPage = lazy(() => + import("./wallet/OrganizationWalletPage.js").then((m) => ({ + default: m.OrganizationWalletPage, + })), +) +const OrganizationServicesPage = lazy(() => + import("./OrganizationServicesPage.js").then((m) => ({ + default: m.OrganizationServicesPage, + })), +) +const OrganizationBillingHistoryPage = lazy(() => + import("./OrganizationBillingHistoryPage.js").then((m) => ({ + default: m.OrganizationBillingHistoryPage, + })), +) +const OrganizationInvoicesPage = lazy(() => + import("./invoices/OrganizationInvoicesPage.js").then((m) => ({ + default: m.OrganizationInvoicesPage, + })), +) + +export function OrganizationBillingTabContent(props: { idOrganization: string }) { + return ( + , + content: ( + + + + ), + }, + { + key: "services", + label: "Services", + icon: , + content: ( + + + + ), + }, + { + key: "paiements", + label: "Paiements", + icon: , + content: ( + + + + ), + }, + { + key: "factures", + label: "Factures", + icon: , + content: ( + + + + ), + }, + ], + }, + }} + /> + ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/OrganizationServicesPage.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/OrganizationServicesPage.tsx new file mode 100644 index 00000000..9c0a6ca6 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/OrganizationServicesPage.tsx @@ -0,0 +1,541 @@ +import { readOneOrganizationRouteDefinition } from "@arrhes/application-metadata/routes" +import { + FREE_STORAGE_BYTES, + INCLUDED_AGENT_TOKENS, + INCLUDED_OCR_PAGES, + STORAGE_PRICE_PER_GB_IN_CENTS, + VAT_PERCENT, +} from "@arrhes/application-metadata/utilities" +import { Button, ButtonOutlineContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconAlertTriangle, IconPencil } from "@tabler/icons-react" +import { useParams } from "@tanstack/react-router" +import type { JSX } from "react" +import { DataWrapper } from "../../../../components/layouts/DataWrapper.tsx" +import { Page } from "../../../../components/layouts/page/page.tsx" +import { useTabs } from "../../../../contexts/tabs/useTabs.js" +import { formatEuros } from "../../../../utilities/formatEuros.tsx" +import { OrganizationBillingDisclaimerBanner } from "./wallet/OrganizationBillingDisclaimerBanner.tsx" + +function getStorageAddonQuantity(storageLimit: number) { + return Math.max(Math.round((storageLimit - FREE_STORAGE_BYTES) / FREE_STORAGE_BYTES), 0) +} + +function getRecurringStorageAmountInCents(storageLimit: number) { + return getStorageAddonQuantity(storageLimit) * STORAGE_PRICE_PER_GB_IN_CENTS +} + +function getTokenAddonQuantity(totalTokens: number) { + return Math.max(Math.round((totalTokens - INCLUDED_AGENT_TOKENS) / INCLUDED_AGENT_TOKENS), 0) +} + +function formatStorageValue(value: number) { + if (value >= 1_073_741_824) { + return `${(value / 1_073_741_824).toLocaleString("fr-FR", { + maximumFractionDigits: 1, + })} Go` + } + + if (value >= 1_048_576) { + return `${(value / 1_048_576).toLocaleString("fr-FR", { + maximumFractionDigits: 1, + })} Mo` + } + + return `${Math.round(value / 1024).toLocaleString("fr-FR")} ko` +} + +function formatTokenValue(value: number) { + if (value >= 1_000_000) { + return `${(value / 1_000_000).toLocaleString("fr-FR", { + maximumFractionDigits: 1, + })} M` + } + + if (value >= 1_000) { + return `${(value / 1_000).toLocaleString("fr-FR", { + maximumFractionDigits: 0, + })} k` + } + + return value.toLocaleString("fr-FR") +} + +function UsageBar(props: { current: number; limit: number; formatValue: (v: number) => string }) { + const safeLimit = props.limit <= 0 ? 1 : props.limit + const percentage = Math.min((props.current / safeLimit) * 100, 100) + const color = percentage >= 90 ? "danger" : percentage >= 70 ? "warning" : "success" + + return ( +
+
+ + {props.formatValue(props.current)} / {props.formatValue(props.limit)} + + {percentage.toFixed(0)}% +
+
+
+
+
+ ) +} + +function ServiceCard(props: { + title: string + description: string + frequency?: string + billingMode: "recurring" | "one_time" + details: Array<{ + label: string + value: string + }> + usage?: JSX.Element + action: JSX.Element +}) { + const isRecurring = props.billingMode === "recurring" + + return ( +
+
+
+
+ + {isRecurring ? "Abonnement" : "Paiement unique"} + + {props.frequency ? ( + + {props.frequency} + + ) : null} +
+
+ + {props.title} + +

+ {props.description} +

+
+
+
+ {props.details.map((detail) => ( +
+ + {detail.label} + + + {detail.value} + +
+ ))} +
+ {props.usage ? ( +
+ {props.usage} +
+ ) : null} +
+ {props.action} +
+
+ ) +} + +export function OrganizationServicesPage({ idOrganization: idOrganizationProp }: { idOrganization?: string } = {}) { + const { openTab } = useTabs() + const params = useParams({ + strict: false, + }) as { + idOrganization?: string + } + const idOrganization = idOrganizationProp ?? params.idOrganization ?? "" + + return ( + + + + {(organization) => { + const currentSupportAmountInCents = organization.licenceAmount + const currentStorageAmountInCents = getRecurringStorageAmountInCents(organization.storageLimit) + const _currentStorageQuantity = getStorageAddonQuantity(organization.storageLimit) + const _currentTokenQuantity = getTokenAddonQuantity( + organization.tokensTotalAvailable + organization.tokensTotalUsed, + ) + const _currentOcrAddonPages = Math.max( + organization.ocrPagesTotalAvailable + organization.ocrPagesTotalUsed - INCLUDED_OCR_PAGES, + 0, + ) + const nextMonthSubscriptionAmountInCents = + currentSupportAmountInCents + currentStorageAmountInCents + const isWalletInsufficient = + nextMonthSubscriptionAmountInCents > 0 && + organization.walletBalanceInCents < nextMonthSubscriptionAmountInCents + + return ( +
+ + {isWalletInsufficient ? ( +
+ +
+ + Solde insuffisant pour le prochain mois + + + Vos abonnements récurrents s'élèvent à{" "} + {formatEuros(nextMonthSubscriptionAmountInCents)} HT/mois (TVA{" "} + {VAT_PERCENT}%), mais votre portefeuille ne contient que{" "} + {formatEuros(organization.walletBalanceInCents)}. Rechargez votre + portefeuille pour éviter une interruption de service le 1er du mois. + +
+
+ ) : null} + + openTab({ + component: "facturation-licence", + props: { + idOrganization, + }, + }) + } + > + } + text="Modifier" + /> + + } + /> + + } + action={ + + } + /> + + openTab({ + component: "facturation-tokens", + props: { + idOrganization, + }, + }) + } + > + } + text="Modifier" + /> + + } + /> + + openTab({ + component: "facturation-ocr", + props: { + idOrganization, + }, + }) + } + > + } + text="Modifier" + /> + + } + /> +
+ ) + }} +
+
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/ResumeSubscription.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/ResumeSubscription.tsx new file mode 100644 index 00000000..e83e115b --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/ResumeSubscription.tsx @@ -0,0 +1,79 @@ +import { + createFirstPaymentRouteDefinition, + readOrganizationBillingRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { Button, ButtonOutlineContent, ButtonPlainContent, Dialog, toast, useModalStore } from "@arrhes/ui" +import { IconPlayerPlay } from "@tabler/icons-react" +import { useId } from "react" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../utilities/invalidateData.ts" + +export function ResumeSubscription() { + const modalId = useId() + const { open: openModal, close: closeModal } = useModalStore() + + async function onSubmit() { + const response = await getResponseBodyFromAPI({ + routeDefinition: createFirstPaymentRouteDefinition, + body: {}, + }) + + if (response.ok === false) { + toast({ + title: "Erreur lors de la reprise des paiements", + variant: "error", + }) + return + } + + await invalidateData({ + routeDefinition: readOrganizationBillingRouteDefinition, + body: {}, + }) + + window.location.href = response.data.checkoutUrl + } + + return ( + + + + , + ) + } + > + } + text="Reprendre l'abonnement" + /> + + ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/UpdateLicencePage.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/UpdateLicencePage.tsx new file mode 100644 index 00000000..f996dad2 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/UpdateLicencePage.tsx @@ -0,0 +1,196 @@ +import { + readOneOrganizationRouteDefinition, + updateLicenceSubscriptionRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { getAmountTTCFromHTInCents, SUPPORT_TIERS, VAT_PERCENT } from "@arrhes/application-metadata/utilities" +import { Button, ButtonOutlineContent, Dialog, toast, useModalStore } from "@arrhes/ui" +import { InputCurrency } from "@arrhes/ui/components/inputs/InputCurrency.js" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconDeviceFloppy } from "@tabler/icons-react" +import { useId, useState } from "react" +import { DataWrapper } from "../../../../components/layouts/DataWrapper.tsx" +import { Page } from "../../../../components/layouts/page/page.tsx" +import { formatEuros } from "../../../../utilities/formatEuros.tsx" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../utilities/invalidateData.ts" + +function UpdateLicenceForm(props: { idOrganization: string; currentAmountInCents: number }) { + const confirmModalId = useId() + const { open: openModal, close: closeModal } = useModalStore() + const [value, setValue] = useState(props.currentAmountInCents) + const [isSaving, setIsSaving] = useState(false) + const valueTTCInCents = getAmountTTCFromHTInCents(value) + + async function handleSave() { + if (Number.isNaN(value) || value < 0) { + toast({ + title: "Montant invalide", + variant: "error", + }) + return + } + + setIsSaving(true) + const response = await getResponseBodyFromAPI({ + routeDefinition: updateLicenceSubscriptionRouteDefinition, + body: { + newAmountInCents: value, + }, + }) + setIsSaving(false) + + if (response.ok === false) { + toast({ + title: response.error?.cause ?? "Erreur lors de la mise à jour", + variant: "error", + }) + return + } + + toast({ + title: "Modification enregistrée, effective le 1er du mois prochain", + variant: "success", + }) + + await invalidateData({ + routeDefinition: readOneOrganizationRouteDefinition, + body: { + idOrganization: props.idOrganization, + }, + }) + } + + return ( +
+

+ La licence Arrhes est un montant mensuel libre, prélevé depuis le portefeuille le 1er de chaque mois. + Vous pouvez le laisser à 0,00€ ou contribuer librement au développement d'Arrhes et bénéficier d'un + support privilégié. Le montant est HT. +

+
+ { + setValue(v ?? 0) + }} + type="number" + placeholder="Montant mensuel en €" + /> +
+ {SUPPORT_TIERS.map((tier) => ( + + ))} +
+
+ + + + , + ) + } + hasLoader + isDisabled={isSaving} + > + } + text={isSaving ? "Enregistrement..." : "Enregistrer"} + /> + +
+ ) +} + +export function UpdateLicencePage({ idOrganization }: { idOrganization: string }) { + return ( + + + Modifier le montant de la licence + + + + {(organization) => ( + + )} + + + + ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/UpdateOcrPage.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/UpdateOcrPage.tsx new file mode 100644 index 00000000..51b91953 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/UpdateOcrPage.tsx @@ -0,0 +1,323 @@ +import { + readOneOrganizationRouteDefinition, + updateOcrSubscriptionRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { + getAmountTTCFromHTInCents, + INCLUDED_OCR_PAGES, + OCR_PAGE_PRICE_IN_CENTS, + OCR_PAGE_TIERS, + VAT_PERCENT, +} from "@arrhes/application-metadata/utilities" +import { Button, ButtonOutlineContent, ButtonPlainContent, Dialog, InputNumber, toast, useModalStore } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconCreditCard, IconX } from "@tabler/icons-react" +import { type ReactNode, useEffect, useId, useState } from "react" +import { DataWrapper } from "../../../../components/layouts/DataWrapper.tsx" +import { Page } from "../../../../components/layouts/page/page.tsx" +import { formatEuros } from "../../../../utilities/formatEuros.tsx" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../utilities/invalidateData.ts" + +function formatPageDelta(quantityDelta: number) { + if (quantityDelta === 0) { + return "Aucun ajout" + } + + return `${quantityDelta.toLocaleString("fr-FR")} page${quantityDelta > 1 ? "s" : ""}` +} + +function FormSection(props: { title: string; description?: string; children: ReactNode }) { + return ( +
+
+

+ {props.title} +

+ {props.description ? ( +

+ {props.description} +

+ ) : null} +
+ {props.children} +
+ ) +} + +function UpdateOcrForm(props: { idOrganization: string; currentQuantity: number; currentPagesLeft: number }) { + const confirmModalId = useId() + const { open: openModal, close: closeModal } = useModalStore() + const [quantityDelta, setQuantityDelta] = useState(0) + const nextQuantity = props.currentQuantity + quantityDelta + const deltaAmountInCents = quantityDelta * OCR_PAGE_PRICE_IN_CENTS + const deltaAmountTTCInCents = getAmountTTCFromHTInCents(deltaAmountInCents) + const nextPagesLeft = props.currentPagesLeft + quantityDelta + + useEffect(() => { + setQuantityDelta(0) + }, []) + + async function handleSave() { + const response = await getResponseBodyFromAPI({ + routeDefinition: updateOcrSubscriptionRouteDefinition, + body: { + newQuantity: nextQuantity, + }, + }) + + if (response.ok === false) { + toast({ + title: response.error?.cause ?? "Erreur lors de la mise à jour", + variant: "error", + }) + return + } + + toast({ + title: "Pages OCR mises à jour", + variant: "success", + }) + + await invalidateData({ + routeDefinition: readOneOrganizationRouteDefinition, + body: { + idOrganization: props.idOrganization, + }, + }) + } + + return ( +
+

+ Chaque page supplémentaire est débitée 0,01€ (HT) une seule fois depuis le portefeuille. Les pages + achetées restent disponibles tant qu'elles ne sont pas consommées. +

+ +
+ +
+ {OCR_PAGE_TIERS.map((tier) => ( + + ))} +
+
+
+ +
+ {[ + { + label: "Quota actuel", + value: `${props.currentPagesLeft.toLocaleString("fr-FR")} pages`, + bg: "neutral/1", + }, + { + label: "Nouveau quota", + value: `${nextPagesLeft.toLocaleString("fr-FR")} pages`, + bg: deltaAmountInCents > 0 ? "warning/5" : "neutral/1", + }, + { + label: "Débité du portefeuille", + value: `${formatEuros(deltaAmountTTCInCents)} TTC`, + bg: "neutral/1", + }, + ].map((item) => ( +
+ + {item.label} + + + {item.value} + +
+ ))} +
+
+
+ + + + + , + ) + } + isDisabled={quantityDelta === 0} + > + } + text="Confirmer l'achat" + isDisabled={quantityDelta === 0} + /> + +
+
+ ) +} + +export function UpdateOcrPage({ idOrganization }: { idOrganization: string }) { + return ( + + + Ajouter des pages OCR + + + + {(organization) => ( + + )} + + + + ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/UpdateStoragePage.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/UpdateStoragePage.tsx new file mode 100644 index 00000000..9e3bfed7 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/UpdateStoragePage.tsx @@ -0,0 +1,486 @@ +import { + readOneOrganizationRouteDefinition, + updateStorageSubscriptionRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { + FREE_STORAGE_BYTES, + getAmountTTCFromHTInCents, + STORAGE_PRICE_PER_GB_IN_CENTS, + VAT_PERCENT, +} from "@arrhes/application-metadata/utilities" +import { Button, ButtonOutlineContent, Dialog, InputNumber, toast, useModalStore } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconMinus, IconPlus } from "@tabler/icons-react" +import { type ReactNode, useEffect, useId, useState } from "react" +import { DataWrapper } from "../../../../components/layouts/DataWrapper.tsx" +import { Page } from "../../../../components/layouts/page/page.tsx" +import { formatEuros } from "../../../../utilities/formatEuros.tsx" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../utilities/invalidateData.ts" + +function formatBytes(bytes: number): string { + if (bytes >= 1_073_741_824) { + return `${(bytes / 1_073_741_824).toFixed(2)} Go` + } + + if (bytes >= 1_048_576) { + return `${(bytes / 1_048_576).toFixed(2)} Mo` + } + + if (bytes >= 1024) { + return `${(bytes / 1024).toFixed(2)} Ko` + } + + return `${bytes} o` +} + +function getStorageLimitFromQuantity(quantity: number): number { + return FREE_STORAGE_BYTES + quantity * FREE_STORAGE_BYTES +} + +function getMinimumStorageQuantityFromUsage(storageCurrentUsage: number): number { + return Math.max(Math.ceil(storageCurrentUsage / FREE_STORAGE_BYTES) - 1, 0) +} + +function formatStorageDelta(quantityDelta: number): string { + if (quantityDelta === 0) { + return "Aucun changement" + } + + if (quantityDelta > 0) { + return `Ajouter ${quantityDelta} Go` + } + + return `Retirer ${Math.abs(quantityDelta)} Go` +} + +function getProRataFraction(): number { + const now = new Date() + const daysInMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 0)).getUTCDate() + const remainingDays = daysInMonth - now.getUTCDate() + 1 + return remainingDays / daysInMonth +} + +function FormSection(props: { title: string; description?: string; children: ReactNode }) { + return ( +
+
+

+ {props.title} +

+ {props.description ? ( +

+ {props.description} +

+ ) : null} +
+ {props.children} +
+ ) +} + +function UpdateStorageForm(props: { + idOrganization: string + currentQuantity: number + currentUsageInBytes: number + currentMaxUsageInBytes: number +}) { + const confirmModalId = useId() + const { open: openModal, close: closeModal } = useModalStore() + const [quantityDelta, setQuantityDelta] = useState(0) + const [isLoading, setIsLoading] = useState(false) + const minimumQuantity = getMinimumStorageQuantityFromUsage(props.currentUsageInBytes) + const nextQuantity = props.currentQuantity + quantityDelta + const currentAmountInCents = props.currentQuantity * STORAGE_PRICE_PER_GB_IN_CENTS + const nextAmountInCents = nextQuantity * STORAGE_PRICE_PER_GB_IN_CENTS + const deltaAmountInCents = nextAmountInCents - currentAmountInCents + const proRataAmountInCents = Math.round(deltaAmountInCents * getProRataFraction()) + const proRataAmountTTCInCents = getAmountTTCFromHTInCents(proRataAmountInCents) + const nextStorageLimitInBytes = getStorageLimitFromQuantity(nextQuantity) + + useEffect(() => { + setQuantityDelta(0) + }, []) + + async function handleSave() { + setIsLoading(true) + const response = await getResponseBodyFromAPI({ + routeDefinition: updateStorageSubscriptionRouteDefinition, + body: { + newQuantity: nextQuantity, + }, + }) + setIsLoading(false) + + if (response.ok === false) { + toast({ + title: response.error?.cause ?? "Erreur lors de la mise à jour", + variant: "error", + }) + return + } + + toast({ + title: + quantityDelta > 0 + ? "Stockage augmenté immédiatement" + : "Réduction enregistrée, effective le 1er du mois prochain", + variant: "success", + }) + + await invalidateData({ + routeDefinition: readOneOrganizationRouteDefinition, + body: { + idOrganization: props.idOrganization, + }, + }) + } + + return ( +
+

+ {quantityDelta > 0 + ? `Augmenter le stockage est immédiat : le montant prorata du mois est débité du portefeuille. Montants en HT (TVA ${VAT_PERCENT}\u00a0%).` + : quantityDelta < 0 + ? `Réduire le stockage est effectif le 1er du mois prochain. Aucun remboursement n'est appliqué. Montants en HT (TVA ${VAT_PERCENT}\u00a0%).` + : `Ajustez le stockage disponible pour l'organisation. Montants en HT (TVA ${VAT_PERCENT}\u00a0%).`} +

+ +
+ {[ + { + label: "Utilisation actuelle", + value: formatBytes(props.currentUsageInBytes), + }, + { + label: "Capacité actuelle", + value: formatBytes(props.currentMaxUsageInBytes), + }, + { + label: "Go supplémentaires actifs", + value: + props.currentQuantity === 0 + ? "Aucun supplément" + : `${props.currentQuantity} Go supplémentaires`, + }, + { + label: "Montant actuel", + value: `${formatEuros(currentAmountInCents)} HT / mois`, + }, + ].map((item) => ( +
+ + {item.label} + + + {item.value} + +
+ ))} +
+
+ +
+ + + Le minimum autorisé est calé sur l'usage actuel: {formatBytes(props.currentUsageInBytes)}. + +
+
+ +
+
+ + Capacité après modification + + + {formatBytes(nextStorageLimitInBytes)} + +
+
0 + ? "warning/5" + : deltaAmountInCents < 0 + ? "success/5" + : "neutral/1", + })} + > + + {deltaAmountInCents > 0 + ? "Débité maintenant (prorata)" + : deltaAmountInCents < 0 + ? "Effectif le 1er du mois" + : "Ajustement portefeuille"} + + + {deltaAmountInCents > 0 + ? `≈\u2009${formatEuros(proRataAmountTTCInCents)} TTC` + : formatEuros(Math.abs(deltaAmountInCents))} + +
+
+ + Nouveau montant mensuel + + + {formatEuros(nextAmountInCents)} HT / mois + +
+
+ + Nouvelle capacité totale + + + {formatStorageDelta(quantityDelta) === "Aucun changement" + ? formatBytes(props.currentMaxUsageInBytes) + : formatBytes(nextStorageLimitInBytes)} + +
+
+
+ + + + , + ) + } + isDisabled={isLoading || quantityDelta === 0} + > + : } + text="Enregistrer le stockage" + /> + +
+ ) +} + +export function UpdateStoragePage({ idOrganization }: { idOrganization: string }) { + return ( + + + Modifier le stockage + + + + {(organization) => ( + + )} + + + + ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/UpdateTokensPage.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/UpdateTokensPage.tsx new file mode 100644 index 00000000..b489b95d --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/UpdateTokensPage.tsx @@ -0,0 +1,340 @@ +import { + readOneOrganizationRouteDefinition, + updateTokensSubscriptionRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { + getAmountTTCFromHTInCents, + INCLUDED_AGENT_TOKENS, + TOKEN_PACK_PRICE_IN_CENTS, + TOKEN_TIERS, + TOKENS_PER_PACK, + VAT_PERCENT, +} from "@arrhes/application-metadata/utilities" +import { Button, ButtonOutlineContent, Dialog, InputNumber, toast, useModalStore } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPlus } from "@tabler/icons-react" +import { type ReactNode, useEffect, useId, useState } from "react" +import { DataWrapper } from "../../../../components/layouts/DataWrapper.tsx" +import { Page } from "../../../../components/layouts/page/page.tsx" +import { formatEuros } from "../../../../utilities/formatEuros.tsx" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../utilities/invalidateData.ts" + +function formatTokenValue(value: number) { + if (value >= 1_000_000) { + return `${(value / 1_000_000).toLocaleString("fr-FR", { + maximumFractionDigits: 1, + })} M` + } + + if (value >= 1_000) { + return `${(value / 1_000).toLocaleString("fr-FR", { + maximumFractionDigits: 0, + })} k` + } + + return value.toLocaleString("fr-FR") +} + +function formatTokenUnitDelta(quantityDelta: number) { + if (quantityDelta === 0) { + return "Aucun ajout" + } + + return `${quantityDelta} M tokens` +} + +function FormSection(props: { title: string; description?: string; children: ReactNode }) { + return ( +
+
+

+ {props.title} +

+ {props.description ? ( +

+ {props.description} +

+ ) : null} +
+ {props.children} +
+ ) +} + +function UpdateTokensForm(props: { idOrganization: string; currentQuantity: number; currentTokensLeft: number }) { + const confirmModalId = useId() + const { open: openModal, close: closeModal } = useModalStore() + const [quantityDelta, setQuantityDelta] = useState(0) + const nextQuantity = props.currentQuantity + quantityDelta + const deltaAmountInCents = quantityDelta * TOKEN_PACK_PRICE_IN_CENTS + const deltaAmountTTCInCents = getAmountTTCFromHTInCents(deltaAmountInCents) + const addedTokens = quantityDelta * TOKENS_PER_PACK + const nextTokensLeft = props.currentTokensLeft + addedTokens + + useEffect(() => { + setQuantityDelta(0) + }, []) + + async function handleSave() { + const response = await getResponseBodyFromAPI({ + routeDefinition: updateTokensSubscriptionRouteDefinition, + body: { + newQuantity: nextQuantity, + }, + }) + + if (response.ok === false) { + toast({ + title: response.error?.cause ?? "Erreur lors de la mise à jour", + variant: "error", + }) + return + } + + toast({ + title: "Tokens IA mis à jour", + variant: "success", + }) + + await invalidateData({ + routeDefinition: readOneOrganizationRouteDefinition, + body: { + idOrganization: props.idOrganization, + }, + }) + } + + return ( +
+

+ Chaque million de tokens supplémentaire est débité une seule fois depuis le portefeuille. Les tokens + achetés restent disponibles tant qu'ils n'ont pas été consommés. Montants en HT (TVA {VAT_PERCENT}%). +

+ +
+ +
+ {TOKEN_TIERS.map((tier) => ( + + ))} +
+
+
+ +
+ {[ + { + label: "Solde actuel", + value: `${formatTokenValue(props.currentTokensLeft)} tokens`, + bg: "neutral/1", + }, + { + label: "Nouveau solde", + value: `${formatTokenValue(nextTokensLeft)} tokens`, + bg: deltaAmountInCents > 0 ? "warning/5" : "neutral/1", + }, + { + label: "Débité du portefeuille", + value: `${formatEuros(deltaAmountTTCInCents)} TTC`, + bg: "neutral/1", + }, + ].map((item) => ( +
+ + {item.label} + + + {item.value} + +
+ ))} +
+
+ + + + , + ) + } + isDisabled={quantityDelta === 0} + > + } + text="Enregistrer les tokens" + /> + +
+ ) +} + +export function UpdateTokensPage({ idOrganization }: { idOrganization: string }) { + return ( + + + Modifier les tokens Assistant IA + + + + {(organization) => ( + + )} + + + + ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/cancelSubscription.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/cancelSubscription.tsx deleted file mode 100644 index bee5f575..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/cancelSubscription.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { - cancelSubscriptionRouteDefinition, - readOrganizationBillingRouteDefinition, -} from "@arrhes/application-metadata/routes" -import { ButtonOutlineContent, toast } from "@arrhes/ui" -import { IconPlayerPause } from "@tabler/icons-react" -import { ConfirmationModal } from "../../../../components/overlays/dialog/confirmationModal.tsx" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../utilities/invalidateData.ts" - -export function CancelSubscription(_props: { idOrganization: string }) { - return ( - - Votre accès Premium sera maintenu jusqu'à la fin de la période en cours. -
- Aucun nouveau paiement ne sera prélevé. -
- Vous pourrez vous abonner de nouveau à tout moment. - - } - submitButtonProps={{ - text: "Mettre en pause l'abonnement", - color: "danger", - leftIcon: , - }} - onSubmit={async () => { - const response = await getResponseBodyFromAPI({ - routeDefinition: cancelSubscriptionRouteDefinition, - body: {}, - }) - - if (response.ok === false) { - toast({ - title: "Erreur lors de la mise en pause de l'abonnement", - variant: "error", - }) - return - } - - await invalidateData({ - routeDefinition: readOrganizationBillingRouteDefinition, - body: {}, - }) - - toast({ - title: "Abonnement mis en pause", - variant: "success", - }) - }} - > - } - text="Mettre en pause l'abonnement" - color="danger" - /> -
- ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/invoices/InvoiceActionsPopover.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/invoices/InvoiceActionsPopover.tsx index 1b8d7edf..b5564579 100644 --- a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/invoices/InvoiceActionsPopover.tsx +++ b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/invoices/InvoiceActionsPopover.tsx @@ -3,7 +3,7 @@ import { Button, ButtonGhostContent } from "@arrhes/ui" import { css } from "@arrhes/ui/utilities/cn.js" import { IconDotsVertical, IconDownload, IconEye } from "@tabler/icons-react" import type * as v from "valibot" -import { LinkButton } from "../../../../../components/linkButton.js" +import { LinkButton } from "../../../../../components/LinkButton.js" import { Popover } from "../../../../../components/overlays/popover/popover.js" import { DownloadInvoiceAsPDFButton } from "./DownloadInvoiceAsPDFButton.tsx" import { DownloadInvoiceAsXMLButton } from "./DownloadInvoiceAsXMLButton.tsx" diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/invoices/InvoicePaymentsTable.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/invoices/InvoicePaymentsTable.tsx index beb7b9d2..c822330b 100644 --- a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/invoices/InvoicePaymentsTable.tsx +++ b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/invoices/InvoicePaymentsTable.tsx @@ -2,7 +2,7 @@ import { readAllOrganizationPaymentsRouteDefinition } from "@arrhes/application- import { getTaxAmountFromHTInCents, OCR_PAGE_PRICE_IN_CENTS } from "@arrhes/application-metadata/utilities" import { FormatDateTime, FormatPrice } from "@arrhes/ui" import { css } from "@arrhes/ui/utilities/cn.js" -import { DataWrapper } from "../../../../../components/layouts/dataWrapper.tsx" +import { DataWrapper } from "../../../../../components/layouts/DataWrapper.tsx" function getPaymentServiceLabel(serviceType: string | null) { if (serviceType === "support") return "Licence" diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/invoices/InvoicesTable.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/invoices/InvoicesTable.tsx index b5664fa8..c6d55a3c 100644 --- a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/invoices/InvoicesTable.tsx +++ b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/invoices/InvoicesTable.tsx @@ -1,11 +1,9 @@ import type { returnedSchemas } from "@arrhes/application-metadata" import { getAmountTTCFromHTInCents, getTaxAmountFromHTInCents } from "@arrhes/application-metadata/utilities" import { Chip, type ChipColors } from "@arrhes/ui" -import { useParams } from "@tanstack/react-router" import type { ColumnDef } from "@tanstack/react-table" import type * as v from "valibot" -import { DataTable } from "../../../../../components/layouts/dataTable.tsx" -import { organizationInvoicesRoute } from "../../../../../routes/root/dashboard/organizations/$idOrganization/organizationBilling/organizationInvoicesRoute.tsx" +import { DataTable } from "../../../../../components/layouts/DataTable.tsx" import { InvoiceActionsPopover } from "./InvoiceActionsPopover.tsx" const invoiceStatusLabel: Record = { @@ -39,11 +37,7 @@ function getInvoiceTotalAmountInCents(invoice: Invoice) { type Invoice = v.InferOutput -export function InvoicesTable(props: { invoices: Array }) { - const params = useParams({ - from: organizationInvoicesRoute.id, - }) - +export function InvoicesTable(props: { invoices: Array; idOrganization: string }) { const columns: Array> = [ { id: "period", @@ -93,7 +87,7 @@ export function InvoicesTable(props: { invoices: Array }) { header: " ", cell: (context) => ( ), diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/invoices/OrganizationInvoicePage.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/invoices/OrganizationInvoicePage.tsx new file mode 100644 index 00000000..74601692 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/invoices/OrganizationInvoicePage.tsx @@ -0,0 +1,113 @@ +import { readOneInvoiceRouteDefinition } from "@arrhes/application-metadata/routes" +import { Button, ButtonGhostContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { useParams } from "@tanstack/react-router" +import { useState } from "react" +import type * as v from "valibot" +import { Banner } from "../../../../../components/layouts/Banner.tsx" +import { DataWrapper } from "../../../../../components/layouts/DataWrapper.tsx" +import { Page } from "../../../../../components/layouts/page/page.tsx" +import { SettingsSection } from "../../../../../components/layouts/settingsSection/settingsSection.tsx" + +import { InvoicePaymentsTable } from "./InvoicePaymentsTable.tsx" +import { InvoiceXMLViewer } from "./InvoiceXMLViewer.tsx" + +function formatInvoiceMonthYear(periodStart: string) { + return new Intl.DateTimeFormat("fr-FR", { + month: "long", + year: "numeric", + }).format(new Date(periodStart)) +} + +type InvoiceTab = "payments" | "xml" + +function InvoiceTabsContent(props: { + idInvoice: string + invoice: v.InferOutput +}) { + const [activeTab, setActiveTab] = useState("payments") + + return ( +
+
+ + +
+ + {activeTab === "payments" && } + + {activeTab === "xml" && + (props.invoice.xmlStorageKey === null ? ( + + La facture XML/PDF n'est pas encore générée. Elle sera disponible au début du mois prochain. + + ) : ( + + ))} +
+ ) +} +export function OrganizationInvoicePage() { + const params = useParams({ + strict: false, + }) as { + idInvoice: string + } + + return ( + + + + + {(invoice) => { + return ( + <> + + + + ) + }} + + + + + ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/invoices/OrganizationInvoicesPage.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/invoices/OrganizationInvoicesPage.tsx new file mode 100644 index 00000000..6965996a --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/invoices/OrganizationInvoicesPage.tsx @@ -0,0 +1,50 @@ +import { readAllInvoicesRouteDefinition } from "@arrhes/application-metadata/routes" +import { IconFileInvoice } from "@tabler/icons-react" +import { useParams } from "@tanstack/react-router" +import { DataWrapper } from "../../../../../components/layouts/DataWrapper.tsx" +import { EmptyState } from "../../../../../components/layouts/EmptyState.tsx" +import { Page } from "../../../../../components/layouts/page/page.tsx" +import { SettingsSection } from "../../../../../components/layouts/settingsSection/settingsSection.tsx" +import { InvoicesTable } from "./InvoicesTable.tsx" + +export function OrganizationInvoicesPage({ idOrganization: idOrganizationProp }: { idOrganization?: string } = {}) { + const params = useParams({ + strict: false, + }) as { + idOrganization?: string + } + const idOrganization = idOrganizationProp ?? params.idOrganization ?? "" + + return ( + + + + + + {(invoices) => { + if (invoices.length === 0) { + return ( + } + title="Aucune facture" + subtitle="Vos factures mensuelles apparaitront ici a partir du mois suivant votre premier paiement." + /> + ) + } + + return ( + + ) + }} + + + + + ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/invoices/organizationInvoicePage.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/invoices/organizationInvoicePage.tsx deleted file mode 100644 index d505726e..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/invoices/organizationInvoicePage.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { readOneInvoiceRouteDefinition } from "@arrhes/application-metadata/routes" -import { Button, ButtonGhostContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { useParams } from "@tanstack/react-router" -import { useState } from "react" -import type * as v from "valibot" -import { Banner } from "../../../../../components/layouts/banner.tsx" -import { DataWrapper } from "../../../../../components/layouts/dataWrapper.tsx" -import { Page } from "../../../../../components/layouts/page/page.tsx" -import { SettingsSection } from "../../../../../components/layouts/settingsSection/settingsSection.tsx" -import { organizationInvoiceUblRoute } from "../../../../../routes/root/dashboard/organizations/$idOrganization/organizationBilling/organizationInvoiceUblRoute.js" -import { InvoicePaymentsTable } from "./InvoicePaymentsTable.tsx" -import { InvoiceXMLViewer } from "./InvoiceXMLViewer.tsx" - -function formatInvoiceMonthYear(periodStart: string) { - return new Intl.DateTimeFormat("fr-FR", { - month: "long", - year: "numeric", - }).format(new Date(periodStart)) -} - -type InvoiceTab = "payments" | "xml" - -function InvoiceTabsContent(props: { - idInvoice: string - invoice: v.InferOutput -}) { - const [activeTab, setActiveTab] = useState("payments") - - return ( -
-
- - -
- - {activeTab === "payments" && } - - {activeTab === "xml" && - (props.invoice.xmlStorageKey === null ? ( - - La facture XML/PDF n'est pas encore générée. Elle sera disponible au début du mois prochain. - - ) : ( - - ))} -
- ) -} -export function OrganizationInvoicePage() { - const params = useParams({ - from: organizationInvoiceUblRoute.id, - }) - - return ( - - - - - {(invoice) => { - return ( - <> - - - - ) - }} - - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/invoices/organizationInvoicesPage.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/invoices/organizationInvoicesPage.tsx deleted file mode 100644 index 13a247e3..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/invoices/organizationInvoicesPage.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { readAllInvoicesRouteDefinition } from "@arrhes/application-metadata/routes" -import { IconFileInvoice } from "@tabler/icons-react" -import { useParams } from "@tanstack/react-router" -import { DataWrapper } from "../../../../../components/layouts/dataWrapper.tsx" -import { EmptyState } from "../../../../../components/layouts/emptyState.tsx" -import { Page } from "../../../../../components/layouts/page/page.tsx" -import { SettingsSection } from "../../../../../components/layouts/settingsSection/settingsSection.tsx" -import { organizationInvoicesRoute } from "../../../../../routes/root/dashboard/organizations/$idOrganization/organizationBilling/organizationInvoicesRoute.tsx" -import { InvoicesTable } from "./InvoicesTable.tsx" - -export function OrganizationInvoicesPage() { - const _params = useParams({ - from: organizationInvoicesRoute.id, - }) - - return ( - - - - - - {(invoices) => { - if (invoices.length === 0) { - return ( - } - title="Aucune facture" - subtitle="Vos factures mensuelles apparaitront ici a partir du mois suivant votre premier paiement." - /> - ) - } - - return - }} - - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/organizationBillingHistoryPage.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/organizationBillingHistoryPage.tsx deleted file mode 100644 index 40733c56..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/organizationBillingHistoryPage.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import { readAllOrganizationPaymentsRouteDefinition } from "@arrhes/application-metadata/routes" -import { Chip, type ChipColors, FormatPrice, formatDate } from "@arrhes/ui" -import { IconReceipt } from "@tabler/icons-react" -import type { ColumnDef } from "@tanstack/react-table" -import { DataTable } from "../../../../components/layouts/dataTable.tsx" -import { DataWrapper } from "../../../../components/layouts/dataWrapper.tsx" -import { EmptyState } from "../../../../components/layouts/emptyState.tsx" -import { Page } from "../../../../components/layouts/page/page.tsx" -import { SettingsSection } from "../../../../components/layouts/settingsSection/settingsSection.tsx" - -const statusLabel: Record = { - pending: "En attente", - paid: "Payé", - failed: "Échoué", - refunded: "Remboursé", -} - -const statusColor: Record = { - pending: "warning", - paid: "success", - failed: "error", - refunded: "neutral", -} - -type Payment = { - category: string - flow: "debit" | "credit" - id: string - idInvoice: string - serviceType: string | null - status: string - amountHTInCents: number - amountTVAInCents: number - currency: string - description: string | null - periodStart: string | null - periodEnd: string | null - createdAt: string - sequenceType: string | null -} - -function getSignedAmountHTInCents(payment: Payment): number { - return payment.flow === "debit" ? payment.amountHTInCents : -payment.amountHTInCents -} - -function getSignedAmountTVAInCents(payment: Payment): number { - return payment.flow === "debit" ? payment.amountTVAInCents : -payment.amountTVAInCents -} - -function getPaymentCategoryLabel(category: string): string { - if (category === "top_up") return "Recharge" - if (category === "withdrawal") return "Retrait" - if (category === "wallet_spending") return "Portefeuille" - if (category === "setup") return "Moyen de paiement" - return "Abonnement" -} - -function getPaymentServiceLabel(serviceType: string | null): string { - if (serviceType === "support") return "Licence" - if (serviceType === "storage_gb") return "Stockage" - if (serviceType === "agent_tokens_million") return "Tokens" - if (serviceType === "ocr_pages_hundred") return "OCR" - return "-" -} - -const columns: Array> = [ - { - accessorKey: "createdAt", - header: "Date", - minSize: 150, - size: 170, - cell: (context) => formatDate(context.row.original.createdAt), - }, - { - id: "type", - header: "Type", - minSize: 130, - size: 150, - accessorFn: (payment) => getPaymentCategoryLabel(payment.category), - }, - { - id: "service", - header: "Service", - minSize: 120, - size: 140, - accessorFn: (payment) => getPaymentServiceLabel(payment.serviceType), - }, - { - accessorKey: "description", - header: "Description", - minSize: 280, - size: 360, - cell: (context) => context.row.original.description ?? "-", - }, - { - id: "period", - header: "Période", - minSize: 250, - size: 300, - accessorFn: (payment) => - payment.periodStart && payment.periodEnd - ? `${formatDate(payment.periodStart)} - ${formatDate(payment.periodEnd)}` - : null, - }, - { - id: "status", - header: "Statut", - minSize: 130, - size: 150, - accessorFn: (payment) => statusLabel[payment.status] ?? payment.status, - cell: (context) => { - const status = context.row.original.status - return ( - - ) - }, - }, - { - id: "amountHT", - header: "Montant HT", - minSize: 150, - size: 170, - accessorFn: (payment) => getSignedAmountHTInCents(payment), - cell: (context) => , - }, - { - id: "amountTVA", - header: "TVA", - minSize: 130, - size: 150, - accessorFn: (payment) => getSignedAmountTVAInCents(payment), - cell: (context) => , - }, -] - -export function OrganizationBillingHistoryPage() { - return ( - - - - - - {(payments) => { - if (payments.length === 0) { - return ( - } - title="Aucun paiement" - subtitle="Les paiements apparaîtront ici." - /> - ) - } - - return ( - b.createdAt.localeCompare(a.createdAt))} - columns={columns} - pageSize={12} - defaultColumnVisibility={{ - type: false, - service: false, - }} - /> - ) - }} - - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/organizationBillingLayout.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/organizationBillingLayout.tsx deleted file mode 100644 index fe1f8646..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/organizationBillingLayout.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { IconFileInvoice, IconPuzzle, IconReceipt, IconWallet } from "@tabler/icons-react" -import { useParams } from "@tanstack/react-router" -import { SubPageLayout } from "../../../../components/layouts/subPageLayout.tsx" -import { organizationBillingLayoutRoute } from "../../../../routes/root/dashboard/organizations/$idOrganization/organizationBilling/organizationBillingLayoutRoute.tsx" - -export function OrganizationBillingLayout() { - const params = useParams({ - from: organizationBillingLayoutRoute.id, - }) - - return ( - , - to: "/dashboard/organisations/$idOrganization/facturation", - params: { - idOrganization: params.idOrganization, - }, - }, - { - label: "Services", - icon: , - to: "/dashboard/organisations/$idOrganization/facturation/services", - params: { - idOrganization: params.idOrganization, - }, - }, - { - label: "Paiements", - icon: , - to: "/dashboard/organisations/$idOrganization/facturation/historique", - params: { - idOrganization: params.idOrganization, - }, - }, - { - label: "Factures", - icon: , - to: "/dashboard/organisations/$idOrganization/facturation/factures", - params: { - idOrganization: params.idOrganization, - }, - }, - ], - }, - }} - /> - ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/organizationServicesPage.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/organizationServicesPage.tsx deleted file mode 100644 index f989e8a8..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/organizationServicesPage.tsx +++ /dev/null @@ -1,533 +0,0 @@ -import { readOneOrganizationRouteDefinition } from "@arrhes/application-metadata/routes" -import { - FREE_STORAGE_BYTES, - INCLUDED_AGENT_TOKENS, - INCLUDED_OCR_PAGES, - STORAGE_PRICE_PER_GB_IN_CENTS, - VAT_PERCENT, -} from "@arrhes/application-metadata/utilities" -import { Button, ButtonOutlineContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconAlertTriangle, IconPencil } from "@tabler/icons-react" -import { useParams } from "@tanstack/react-router" -import { type JSX, useState } from "react" -import { DataWrapper } from "../../../../components/layouts/dataWrapper.tsx" -import { Page } from "../../../../components/layouts/page/page.tsx" -import { organizationServicesRoute } from "../../../../routes/root/dashboard/organizations/$idOrganization/organizationBilling/organizationBillingsRoute.tsx" -import { formatEuros } from "../../../../utilities/formatEuros.tsx" -import { UpdateLicenceSubscriptionDrawer } from "./updateLicenceSubscriptionDrawer.tsx" -import { UpdateOcrSubscriptionDrawer } from "./updateOcrSubscriptionDrawer.tsx" -import { UpdateStorageSubscriptionDrawer } from "./updateStorageSubscriptionDrawer.tsx" -import { UpdateTokensSubscriptionDrawer } from "./updateTokensSubscriptionDrawer.tsx" -import { OrganizationBillingDisclaimerBanner } from "./wallet/OrganizationBillingDisclaimerBanner.tsx" - -function getStorageAddonQuantity(storageLimit: number) { - return Math.max(Math.round((storageLimit - FREE_STORAGE_BYTES) / FREE_STORAGE_BYTES), 0) -} - -function getRecurringStorageAmountInCents(storageLimit: number) { - return getStorageAddonQuantity(storageLimit) * STORAGE_PRICE_PER_GB_IN_CENTS -} - -function getTokenAddonQuantity(totalTokens: number) { - return Math.max(Math.round((totalTokens - INCLUDED_AGENT_TOKENS) / INCLUDED_AGENT_TOKENS), 0) -} - -function formatStorageValue(value: number) { - if (value >= 1_073_741_824) { - return `${(value / 1_073_741_824).toLocaleString("fr-FR", { - maximumFractionDigits: 1, - })} Go` - } - - if (value >= 1_048_576) { - return `${(value / 1_048_576).toLocaleString("fr-FR", { - maximumFractionDigits: 1, - })} Mo` - } - - return `${Math.round(value / 1024).toLocaleString("fr-FR")} ko` -} - -function formatTokenValue(value: number) { - if (value >= 1_000_000) { - return `${(value / 1_000_000).toLocaleString("fr-FR", { - maximumFractionDigits: 1, - })} M` - } - - if (value >= 1_000) { - return `${(value / 1_000).toLocaleString("fr-FR", { - maximumFractionDigits: 0, - })} k` - } - - return value.toLocaleString("fr-FR") -} - -function UsageBar(props: { current: number; limit: number; formatValue: (v: number) => string }) { - const safeLimit = props.limit <= 0 ? 1 : props.limit - const percentage = Math.min((props.current / safeLimit) * 100, 100) - const color = percentage >= 90 ? "danger" : percentage >= 70 ? "warning" : "success" - - return ( -
-
- - {props.formatValue(props.current)} / {props.formatValue(props.limit)} - - {percentage.toFixed(0)}% -
-
-
-
-
- ) -} - -function ServiceCard(props: { - title: string - description: string - frequency?: string - billingMode: "recurring" | "one_time" - details: Array<{ - label: string - value: string - }> - usage?: JSX.Element - action: JSX.Element -}) { - const isRecurring = props.billingMode === "recurring" - - return ( -
-
-
-
- - {isRecurring ? "Abonnement" : "Paiement unique"} - - {props.frequency ? ( - - {props.frequency} - - ) : null} -
-
- - {props.title} - -

- {props.description} -

-
-
-
- {props.details.map((detail) => ( -
- - {detail.label} - - - {detail.value} - -
- ))} -
- {props.usage ? ( -
- {props.usage} -
- ) : null} -
- {props.action} -
-
- ) -} - -export function OrganizationServicesPage() { - const [refreshKey, setRefreshKey] = useState(0) - const params = useParams({ - from: organizationServicesRoute.id, - }) - - return ( - - - - {(organization) => { - const currentSupportAmountInCents = organization.licenceAmount - const currentStorageAmountInCents = getRecurringStorageAmountInCents(organization.storageLimit) - const currentStorageQuantity = getStorageAddonQuantity(organization.storageLimit) - const currentTokenQuantity = getTokenAddonQuantity( - organization.tokensTotalAvailable + organization.tokensTotalUsed, - ) - const currentOcrAddonPages = Math.max( - organization.ocrPagesTotalAvailable + organization.ocrPagesTotalUsed - INCLUDED_OCR_PAGES, - 0, - ) - const nextMonthSubscriptionAmountInCents = - currentSupportAmountInCents + currentStorageAmountInCents - const isWalletInsufficient = - nextMonthSubscriptionAmountInCents > 0 && - organization.walletBalanceInCents < nextMonthSubscriptionAmountInCents - - return ( -
- - {isWalletInsufficient ? ( -
- -
- - Solde insuffisant pour le prochain mois - - - Vos abonnements récurrents s'élèvent à{" "} - {formatEuros(nextMonthSubscriptionAmountInCents)} HT/mois (TVA{" "} - {VAT_PERCENT}%), mais votre portefeuille ne contient que{" "} - {formatEuros(organization.walletBalanceInCents)}. Rechargez votre - portefeuille pour éviter une interruption de service le 1er du mois. - -
-
- ) : null} - setRefreshKey((key) => key + 1)} - > - - - } - /> - - } - action={ - setRefreshKey((key) => key + 1)} - > - - - } - /> - setRefreshKey((key) => key + 1)} - > - - - } - /> - setRefreshKey((key) => key + 1)} - > - - - } - /> -
- ) - }} -
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/resumeSubscription.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/resumeSubscription.tsx deleted file mode 100644 index 6a8435e9..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/resumeSubscription.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { - createFirstPaymentRouteDefinition, - readOrganizationBillingRouteDefinition, -} from "@arrhes/application-metadata/routes" -import { ButtonOutlineContent, toast } from "@arrhes/ui" -import { IconPlayerPlay } from "@tabler/icons-react" -import { ConfirmationModal } from "../../../../components/overlays/dialog/confirmationModal.tsx" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../utilities/invalidateData.ts" - -export function ResumeSubscription() { - return ( - - Les prélèvements mensuels automatiques reprendront à partir de la prochaine échéance. -
- Aucun nouveau paiement immédiat ne sera prélevé. - - } - submitButtonProps={{ - color: "default", - text: "Reprendre les paiements", - leftIcon: , - }} - onSubmit={async () => { - const response = await getResponseBodyFromAPI({ - routeDefinition: createFirstPaymentRouteDefinition, - body: {}, - }) - - if (response.ok === false) { - toast({ - title: "Erreur lors de la reprise des paiements", - variant: "error", - }) - return - } - - await invalidateData({ - routeDefinition: readOrganizationBillingRouteDefinition, - body: {}, - }) - - window.location.href = response.data.checkoutUrl - }} - > - } - text="Reprendre l'abonnement" - /> -
- ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/updateLicenceSubscriptionDrawer.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/updateLicenceSubscriptionDrawer.tsx deleted file mode 100644 index 040c8501..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/updateLicenceSubscriptionDrawer.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import { - readOneOrganizationRouteDefinition, - updateLicenceSubscriptionRouteDefinition, -} from "@arrhes/application-metadata/routes" -import { getAmountTTCFromHTInCents, SUPPORT_TIERS, VAT_PERCENT } from "@arrhes/application-metadata/utilities" -import { Button, ButtonOutlineContent, toast } from "@arrhes/ui" -import { InputCurrency } from "@arrhes/ui/components/inputs/inputCurrency.js" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconDeviceFloppy } from "@tabler/icons-react" -import { type JSX, useState } from "react" -import { ConfirmationModal } from "../../../../components/overlays/dialog/confirmationModal.tsx" -import { Drawer } from "../../../../components/overlays/drawer/drawer.tsx" -import { formatEuros } from "../../../../utilities/formatEuros.tsx" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../utilities/invalidateData.ts" - -export function UpdateLicenceSubscriptionDrawer(props: { - children: JSX.Element - idOrganization: string - currentAmountInCents: number - onSuccess: () => void -}) { - const [open, setOpen] = useState(false) - const [confirmOpen, setConfirmOpen] = useState(false) - const [value, setValue] = useState(props.currentAmountInCents) - const [isSaving, setIsSaving] = useState(false) - const valueTTCInCents = getAmountTTCFromHTInCents(value) - - async function handleSaveSupport() { - if (Number.isNaN(value) || value < 0) { - toast({ - title: "Montant invalide", - variant: "error", - }) - return - } - - setIsSaving(true) - const response = await getResponseBodyFromAPI({ - routeDefinition: updateLicenceSubscriptionRouteDefinition, - body: { - newAmountInCents: value, - }, - }) - setIsSaving(false) - - if (response.ok === false) { - toast({ - title: response.error?.cause ?? "Erreur lors de la mise à jour", - variant: "error", - }) - return - } - - toast({ - title: "Modification enregistrée, effective le 1er du mois prochain", - variant: "success", - }) - - await invalidateData({ - routeDefinition: readOneOrganizationRouteDefinition, - body: { - idOrganization: props.idOrganization, - }, - }) - setOpen(false) - props.onSuccess() - } - - return ( - - {props.children} - - - -
-

- La licence Arrhes est un montant mensuel libre, prélevé depuis le portefeuille le 1er de - chaque mois. Vous pouvez le laisser à 0,00€ ou contribuer librement au développement - d'Arrhes et bénéficier d'un support privilégié. Le montant est HT. -

-
- { - setValue(value ?? 0) - }} - type="number" - placeholder="Montant mensuel en €" - /> -
- {SUPPORT_TIERS.map((tier) => ( - - ))} -
-
- - -
-
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/updateOcrSubscriptionDrawer.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/updateOcrSubscriptionDrawer.tsx deleted file mode 100644 index 3dffe674..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/updateOcrSubscriptionDrawer.tsx +++ /dev/null @@ -1,316 +0,0 @@ -import { updateOcrSubscriptionRouteDefinition } from "@arrhes/application-metadata/routes" -import { - getAmountTTCFromHTInCents, - OCR_PAGE_PRICE_IN_CENTS, - OCR_PAGE_TIERS, - VAT_PERCENT, -} from "@arrhes/application-metadata/utilities" -import { Button, ButtonOutlineContent, ButtonPlainContent, InputNumber, toast } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconCreditCard, IconX } from "@tabler/icons-react" -import { type JSX, type ReactNode, useEffect, useState } from "react" -import { ConfirmationModal } from "../../../../components/overlays/dialog/confirmationModal.tsx" -import { Drawer } from "../../../../components/overlays/drawer/drawer.tsx" -import { formatEuros } from "../../../../utilities/formatEuros.tsx" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" - -function formatPageDelta(quantityDelta: number) { - if (quantityDelta === 0) { - return "Aucun ajout" - } - - return `${quantityDelta.toLocaleString("fr-FR")} page${quantityDelta > 1 ? "s" : ""}` -} - -function DrawerSection(props: { title: string; description?: string; children: ReactNode }) { - return ( -
-
-

- {props.title} -

- {props.description ? ( -

- {props.description} -

- ) : null} -
- {props.children} -
- ) -} - -export function UpdateOcrSubscriptionDrawer(props: { - children: JSX.Element - currentQuantity: number - currentPagesLeft: number - onSuccess: () => void -}) { - const [open, setOpen] = useState(false) - const [confirmOpen, setConfirmOpen] = useState(false) - const [quantityDelta, setQuantityDelta] = useState(0) - const nextQuantity = props.currentQuantity + quantityDelta - const deltaAmountInCents = quantityDelta * OCR_PAGE_PRICE_IN_CENTS - const deltaAmountTTCInCents = getAmountTTCFromHTInCents(deltaAmountInCents) - const nextPagesLeft = props.currentPagesLeft + quantityDelta - - useEffect(() => { - if (open) { - setQuantityDelta(0) - } - }, [ - open, - ]) - - async function handleSave() { - const response = await getResponseBodyFromAPI({ - routeDefinition: updateOcrSubscriptionRouteDefinition, - body: { - newQuantity: nextQuantity, - }, - }) - - if (response.ok === false) { - toast({ - title: response.error?.cause ?? "Erreur lors de la mise à jour", - variant: "error", - }) - return - } - toast({ - title: "Pages OCR mises à jour", - variant: "success", - }) - setOpen(false) - props.onSuccess() - } - - return ( - - {props.children} - - - -
-

- Chaque page supplémentaire est débitée 0,01€ (HT) une seule fois depuis le portefeuille. Les - pages achetées restent disponibles tant qu'elles ne sont pas consommées. -

- -
- -
- {OCR_PAGE_TIERS.map((tier) => ( - - ))} -
-
-
- -
-
- - Quota actuel - - - {props.currentPagesLeft.toLocaleString("fr-FR")} pages - -
-
0 ? "warning/5" : "neutral/1", - })} - > - - Nouveau quota - - - {nextPagesLeft.toLocaleString("fr-FR")} pages - -
-
- - Débité du portefeuille - - - {formatEuros(deltaAmountTTCInCents)} TTC - -
-
-
-
- - -
- -
-
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/updateStorageSubscriptionDrawer.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/updateStorageSubscriptionDrawer.tsx deleted file mode 100644 index 3f224467..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/updateStorageSubscriptionDrawer.tsx +++ /dev/null @@ -1,520 +0,0 @@ -import { - readOneOrganizationRouteDefinition, - updateStorageSubscriptionRouteDefinition, -} from "@arrhes/application-metadata/routes" -import { - FREE_STORAGE_BYTES, - getAmountTTCFromHTInCents, - STORAGE_PRICE_PER_GB_IN_CENTS, - VAT_PERCENT, -} from "@arrhes/application-metadata/utilities" -import { Button, ButtonOutlineContent, InputNumber, toast } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconMinus, IconPlus } from "@tabler/icons-react" -import { type JSX, type ReactNode, useEffect, useState } from "react" -import { ConfirmationModal } from "../../../../components/overlays/dialog/confirmationModal.tsx" -import { Drawer } from "../../../../components/overlays/drawer/drawer.tsx" -import { formatEuros } from "../../../../utilities/formatEuros.tsx" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../utilities/invalidateData.ts" - -function formatBytes(bytes: number): string { - if (bytes >= 1_073_741_824) { - return `${(bytes / 1_073_741_824).toFixed(2)} Go` - } - - if (bytes >= 1_048_576) { - return `${(bytes / 1_048_576).toFixed(2)} Mo` - } - - if (bytes >= 1024) { - return `${(bytes / 1024).toFixed(2)} Ko` - } - - return `${bytes} o` -} - -function getStorageLimitFromQuantity(quantity: number): number { - return FREE_STORAGE_BYTES + quantity * FREE_STORAGE_BYTES -} - -function getMinimumStorageQuantityFromUsage(storageCurrentUsage: number): number { - return Math.max(Math.ceil(storageCurrentUsage / FREE_STORAGE_BYTES) - 1, 0) -} - -function formatStorageSelection(quantity: number): string { - return `${quantity + 1} Go disponibles` -} - -function getProRataFraction(): number { - const now = new Date() - const daysInMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 0)).getUTCDate() - const remainingDays = daysInMonth - now.getUTCDate() + 1 - return remainingDays / daysInMonth -} - -function formatStorageDelta(quantityDelta: number): string { - if (quantityDelta === 0) { - return "Aucun changement" - } - - if (quantityDelta > 0) { - return `Ajouter ${quantityDelta} Go` - } - - return `Retirer ${Math.abs(quantityDelta)} Go` -} - -function DrawerSection(props: { title: string; description?: string; children: ReactNode }) { - return ( -
-
-

- {props.title} -

- {props.description ? ( -

- {props.description} -

- ) : null} -
- {props.children} -
- ) -} - -export function UpdateStorageSubscriptionDrawer(props: { - children: JSX.Element - idOrganization: string - currentQuantity: number - currentUsageInBytes: number - currentMaxUsageInBytes: number - onSuccess: () => void -}) { - const [open, setOpen] = useState(false) - const [confirmOpen, setConfirmOpen] = useState(false) - const [quantityDelta, setQuantityDelta] = useState(0) - const [isLoading, setIsLoading] = useState(false) - const minimumQuantity = getMinimumStorageQuantityFromUsage(props.currentUsageInBytes) - const nextQuantity = props.currentQuantity + quantityDelta - const currentAmountInCents = props.currentQuantity * STORAGE_PRICE_PER_GB_IN_CENTS - const nextAmountInCents = nextQuantity * STORAGE_PRICE_PER_GB_IN_CENTS - const deltaAmountInCents = nextAmountInCents - currentAmountInCents - const proRataAmountInCents = Math.round(deltaAmountInCents * getProRataFraction()) - const proRataAmountTTCInCents = getAmountTTCFromHTInCents(proRataAmountInCents) - const nextStorageLimitInBytes = getStorageLimitFromQuantity(nextQuantity) - - useEffect(() => { - if (open) { - setQuantityDelta(0) - } - }, [ - open, - ]) - - async function handleSave() { - setIsLoading(true) - const response = await getResponseBodyFromAPI({ - routeDefinition: updateStorageSubscriptionRouteDefinition, - body: { - newQuantity: nextQuantity, - }, - }) - setIsLoading(false) - - if (response.ok === false) { - toast({ - title: response.error?.cause ?? "Erreur lors de la mise à jour", - variant: "error", - }) - return - } - toast({ - title: - quantityDelta > 0 - ? "Stockage augmenté immédiatement" - : "Réduction enregistrée, effective le 1er du mois prochain", - variant: "success", - }) - - await invalidateData({ - routeDefinition: readOneOrganizationRouteDefinition, - body: { - idOrganization: props.idOrganization, - }, - }) - - setOpen(false) - props.onSuccess() - } - - return ( - - {props.children} - - - -
-

- {quantityDelta > 0 - ? `Augmenter le stockage est immédiat : le montant prorata du mois est débité du portefeuille. Montants en HT (TVA ${VAT_PERCENT}\u00a0%).` - : quantityDelta < 0 - ? `Réduire le stockage est effectif le 1er du mois prochain. Aucun remboursement n'est appliqué. Montants en HT (TVA ${VAT_PERCENT}\u00a0%).` - : `Ajustez le stockage disponible pour l'organisation. Montants en HT (TVA ${VAT_PERCENT}\u00a0%).`} -

- -
-
- - Utilisation actuelle - - - {formatBytes(props.currentUsageInBytes)} - -
-
- - Capacité actuelle - - - {formatBytes(props.currentMaxUsageInBytes)} - -
-
- - Go supplémentaires actifs - - - {props.currentQuantity === 0 - ? "Aucun supplément" - : `${props.currentQuantity} Go supplémentaires`} - -
-
- - Montant actuel - - - {formatEuros(currentAmountInCents)} HT / mois - -
-
-
- -
- - - Le minimum autorisé est calé sur l'usage actuel:{" "} - {formatBytes(props.currentUsageInBytes)}. - -
-
- -
-
- - Capacité après modification - - - {formatBytes(nextStorageLimitInBytes)} - -
-
0 - ? "warning/5" - : deltaAmountInCents < 0 - ? "success/5" - : "neutral/1", - })} - > - - {deltaAmountInCents > 0 - ? "Débité maintenant (prorata)" - : deltaAmountInCents < 0 - ? "Effectif le 1er du mois" - : "Ajustement portefeuille"} - - - {deltaAmountInCents > 0 - ? `≈\u2009${formatEuros(proRataAmountTTCInCents)} TTC` - : formatEuros(Math.abs(deltaAmountInCents))} - -
-
- - Nouveau montant mensuel - - - {formatEuros(nextAmountInCents)} HT / mois - -
-
- - Nouvelle capacité totale - - - {formatStorageSelection(nextQuantity)} - -
-
-
- - 0 - ? `${formatStorageDelta(quantityDelta)} sera appliqué immédiatement. ${formatEuros(proRataAmountTTCInCents)} TTC seront débités du portefeuille (${formatEuros(proRataAmountInCents)} HT + TVA ${VAT_PERCENT}%, prorata du mois).` - : `${formatStorageDelta(quantityDelta)} sera effectif le 1er du mois prochain. Aucun remboursement ne sera appliqué.` - } - submitButtonProps={{ - text: "Confirmer", - }} - onSubmit={handleSave} - /> -
-
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/updateTokensSubscriptionDrawer.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/updateTokensSubscriptionDrawer.tsx deleted file mode 100644 index 0cffd1f9..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/updateTokensSubscriptionDrawer.tsx +++ /dev/null @@ -1,327 +0,0 @@ -import { updateTokensSubscriptionRouteDefinition } from "@arrhes/application-metadata/routes" -import { - getAmountTTCFromHTInCents, - TOKEN_PACK_PRICE_IN_CENTS, - TOKEN_TIERS, - TOKENS_PER_PACK, - VAT_PERCENT, -} from "@arrhes/application-metadata/utilities" -import { Button, ButtonOutlineContent, InputNumber, toast } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconPlus } from "@tabler/icons-react" -import { type JSX, type ReactNode, useEffect, useState } from "react" -import { ConfirmationModal } from "../../../../components/overlays/dialog/confirmationModal.tsx" -import { Drawer } from "../../../../components/overlays/drawer/drawer.tsx" -import { formatEuros } from "../../../../utilities/formatEuros.tsx" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" - -function formatTokenValue(value: number) { - if (value >= 1_000_000) { - return `${(value / 1_000_000).toLocaleString("fr-FR", { - maximumFractionDigits: 1, - })} M` - } - - if (value >= 1_000) { - return `${(value / 1_000).toLocaleString("fr-FR", { - maximumFractionDigits: 0, - })} k` - } - - return value.toLocaleString("fr-FR") -} - -function formatTokenUnitDelta(quantityDelta: number) { - if (quantityDelta === 0) { - return "Aucun ajout" - } - - return `${quantityDelta} M tokens` -} - -function DrawerSection(props: { title: string; description?: string; children: ReactNode }) { - return ( -
-
-

- {props.title} -

- {props.description ? ( -

- {props.description} -

- ) : null} -
- {props.children} -
- ) -} - -export function UpdateTokensSubscriptionDrawer(props: { - children: JSX.Element - currentQuantity: number - currentTokensLeft: number - onSuccess: () => void -}) { - const [open, setOpen] = useState(false) - const [confirmOpen, setConfirmOpen] = useState(false) - const [quantityDelta, setQuantityDelta] = useState(0) - const nextQuantity = props.currentQuantity + quantityDelta - const deltaAmountInCents = quantityDelta * TOKEN_PACK_PRICE_IN_CENTS - const deltaAmountTTCInCents = getAmountTTCFromHTInCents(deltaAmountInCents) - const addedTokens = quantityDelta * TOKENS_PER_PACK - const nextTokensLeft = props.currentTokensLeft + addedTokens - - useEffect(() => { - if (open) { - setQuantityDelta(0) - } - }, [ - open, - ]) - - async function handleSave() { - const response = await getResponseBodyFromAPI({ - routeDefinition: updateTokensSubscriptionRouteDefinition, - body: { - newQuantity: nextQuantity, - }, - }) - - if (response.ok === false) { - toast({ - title: response.error?.cause ?? "Erreur lors de la mise à jour", - variant: "error", - }) - return - } - toast({ - title: "Tokens IA mis à jour", - variant: "success", - }) - setOpen(false) - props.onSuccess() - } - - return ( - - {props.children} - - - -
-

- Chaque million de tokens supplémentaire est débité une seule fois depuis le portefeuille. - Les tokens achetés restent disponibles tant qu'ils n'ont pas été consommés. Montants en HT - (TVA {VAT_PERCENT}%). -

- -
- -
- {TOKEN_TIERS.map((tier) => ( - - ))} -
-
-
- -
-
- - Solde actuel - - - {formatTokenValue(props.currentTokensLeft)} tokens - -
-
0 ? "warning/5" : "neutral/1", - })} - > - - Nouveau solde - - - {formatTokenValue(nextTokensLeft)} tokens - -
-
- - Débité du portefeuille - - - {formatEuros(deltaAmountTTCInCents)} TTC - -
-
-
- - -
-
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/wallet/OrganizationBillingDisclaimerBanner.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/wallet/OrganizationBillingDisclaimerBanner.tsx index 7fa07050..c2dd1a6d 100644 --- a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/wallet/OrganizationBillingDisclaimerBanner.tsx +++ b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/wallet/OrganizationBillingDisclaimerBanner.tsx @@ -7,8 +7,8 @@ import { Button, ButtonOutlineContent } from "@arrhes/ui" import { css } from "@arrhes/ui/utilities/cn.js" import { IconAlertTriangle, IconPencil } from "@tabler/icons-react" import type * as v from "valibot" -import { DataWrapper } from "../../../../../components/layouts/dataWrapper.tsx" -import { UpdateOneOrganization } from "../../organizationSettings/updateOneOrganization.tsx" +import { DataWrapper } from "../../../../../components/layouts/DataWrapper.tsx" +import { UpdateOneOrganization } from "../../organizationSettings/UpdateOneOrganization.tsx" export type SubscriptionFeatureType = "storage_gb" | "agent_tokens_million" | "ocr_pages_hundred" diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/wallet/OrganizationWalletPage.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/wallet/OrganizationWalletPage.tsx new file mode 100644 index 00000000..7022d7a8 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/wallet/OrganizationWalletPage.tsx @@ -0,0 +1,270 @@ +import { readOneOrganizationRouteDefinition } from "@arrhes/application-metadata/routes" +import { FREE_STORAGE_BYTES, STORAGE_PRICE_PER_GB_IN_CENTS } from "@arrhes/application-metadata/utilities" +import { Button, ButtonOutlineContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconAlertTriangle, IconCashMinus, IconPigMoney, IconPlus, IconWallet } from "@tabler/icons-react" +import { useParams } from "@tanstack/react-router" +import { DataWrapper } from "../../../../../components/layouts/DataWrapper.tsx" +import { Page } from "../../../../../components/layouts/page/page.tsx" +import { SettingsSection } from "../../../../../components/layouts/settingsSection/settingsSection.tsx" +import { useTabs } from "../../../../../contexts/tabs/useTabs.js" +import { formatEuros } from "../../../../../utilities/formatEuros.tsx" +import { OrganizationBillingDisclaimerBanner } from "./OrganizationBillingDisclaimerBanner.tsx" +import { WalletCurrentMonth } from "./WalletCurrentMonth.tsx" + +function getRecurringStorageAmountInCents(storageLimit: number) { + return ( + Math.max(Math.round((storageLimit - FREE_STORAGE_BYTES) / FREE_STORAGE_BYTES), 0) * + STORAGE_PRICE_PER_GB_IN_CENTS + ) +} + +function getNextMonthSubscriptionAmountInCents(organization: { licenceAmount: number; storageLimit: number }) { + return organization.licenceAmount + getRecurringStorageAmountInCents(organization.storageLimit) +} + +export function OrganizationWalletPage({ idOrganization: idOrganizationProp }: { idOrganization?: string } = {}) { + const { openTab } = useTabs() + const params = useParams({ + strict: false, + }) as { + idOrganization?: string + } + const idOrganization = idOrganizationProp ?? params.idOrganization ?? "" + + return ( + + + + {(organization) => ( +
+ {(() => { + const nextMonthSubscriptionAmountInCents = + getNextMonthSubscriptionAmountInCents(organization) + const isWalletShortForNextMonth = + nextMonthSubscriptionAmountInCents > 0 && + organization.walletBalanceInCents < nextMonthSubscriptionAmountInCents + + return ( + <> + + {isWalletShortForNextMonth ? ( +
+
+ +
+ + Solde insuffisant pour le prochain prélèvement mensuel + + + {formatEuros(organization.walletBalanceInCents)} disponibles + pour {formatEuros(nextMonthSubscriptionAmountInCents)}{" "} + requis au début du mois prochain. + +
+
+ +
+ ) : null} + + +
+
+ + +
+
+
+
+ + + Montant disponible + + + {formatEuros(organization.walletBalanceInCents)} + +
+
+
+
+
+ + + + + + ) + })()} +
+ )} +
+
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/wallet/WalletCurrentMonth.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/wallet/WalletCurrentMonth.tsx new file mode 100644 index 00000000..279bf8e3 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/wallet/WalletCurrentMonth.tsx @@ -0,0 +1,388 @@ +import type { returnedSchemas } from "@arrhes/application-metadata" +import { + readAllInvoicesRouteDefinition, + readAllOrganizationPaymentsRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { css } from "@arrhes/ui/utilities/cn.js" +import type * as v from "valibot" +import { DataWrapper } from "../../../../../components/layouts/DataWrapper.tsx" +import { formatEuros } from "../../../../../utilities/formatEuros.tsx" + +function getPaymentLineType(payment: { + category: string + serviceType: string | null + sequenceType: string | null +}): "subscription" | "storage_gb" | "agent_tokens_million" | "ocr_pages_hundred" | null { + if ( + payment.category === "top_up" || + payment.category === "withdrawal" || + payment.category === "setup" || + payment.sequenceType === "setup" + ) { + return null + } + + if (payment.serviceType === "storage_gb") { + return "storage_gb" + } + + if (payment.serviceType === "agent_tokens_million") { + return "agent_tokens_million" + } + + if (payment.serviceType === "ocr_pages_hundred") { + return "ocr_pages_hundred" + } + + return "subscription" +} + +export function WalletCurrentMonth(props: { organization: v.InferOutput }) { + return ( + + {(invoices) => ( + + {(payments) => { + const now = new Date() + const currentPeriodStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)) + const currentInvoice = invoices.find((inv) => { + const invStart = new Date(inv.startingAt) + return ( + invStart.getUTCFullYear() === currentPeriodStart.getUTCFullYear() && + invStart.getUTCMonth() === currentPeriodStart.getUTCMonth() + ) + }) + + const currentMonthPayments = currentInvoice + ? payments.filter( + (payment) => + payment.idInvoice === currentInvoice.id && + payment.status !== "failed" && + payment.status !== "refunded", + ) + : [] + + const lineItems = [ + { + type: "subscription", + label: "Abonnement", + unit: null, + }, + { + type: "storage_gb", + label: "Stockage", + unit: "1 Go / mois", + }, + { + type: "agent_tokens_million", + label: "Tokens Assistant IA", + unit: "1 million de tokens", + }, + { + type: "ocr_pages_hundred", + label: "Traitement de pages par OCR", + unit: "1 page", + }, + ] as const + + const totalsByType = currentMonthPayments.reduce< + Record< + string, + { + amountHT: number + amountTVA: number + quantity: number + } + > + >((acc, payment) => { + const lineType = getPaymentLineType(payment) + + if (lineType === null) return acc + if (!acc[lineType]) { + acc[lineType] = { + amountHT: 0, + amountTVA: 0, + quantity: 0, + } + } + + acc[lineType].amountHT += payment.amountHTInCents + acc[lineType].amountTVA += payment.amountTVAInCents + acc[lineType].quantity += 1 + + return acc + }, {}) + + // The first 1 Go of storage is always included free + // in the subscription — add it to the display so the + // quantity reflects the actual allocated storage. + if (props.organization.licenceAmount > 0) { + if (!totalsByType.storage_gb) + totalsByType.storage_gb = { + amountHT: 0, + amountTVA: 0, + quantity: 0, + } + totalsByType.storage_gb.quantity += 1 + } + + const totalHTAmount = currentMonthPayments.reduce( + (sum, payment) => + getPaymentLineType(payment) === null ? sum : sum + payment.amountHTInCents, + 0, + ) + + const totalTVAAmount = currentMonthPayments.reduce( + (sum, payment) => + getPaymentLineType(payment) === null ? sum : sum + payment.amountTVAInCents, + 0, + ) + const totalTTCAmount = totalHTAmount + totalTVAAmount + + return ( +
+
+
+ + Service + + + Unité + + + Quantité + + + Montant HT + + + TVA + + + Montant TTC + +
+ {lineItems.map((line, index) => { + const data = totalsByType[line.type] ?? { + amountHT: 0, + amountTVA: 0, + quantity: 0, + } + const amountTTC = data.amountHT + data.amountTVA + + return ( +
+ + {line.label} + + + {line.unit ?? "-"} + + + {data.quantity === 0 ? "-" : data.quantity} + + + {formatEuros(data.amountHT)} + + + {formatEuros(data.amountTVA)} + + + {formatEuros(amountTTC)} + +
+ ) + })} +
+ + Total + + + + + {formatEuros(totalHTAmount)} + + + {formatEuros(totalTVAAmount)} + + + {formatEuros(totalTTCAmount)} + +
+
+
+ ) + }} +
+ )} +
+ ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/wallet/WalletTopUpDrawer.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/wallet/WalletTopUpDrawer.tsx deleted file mode 100644 index ac8287b3..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/wallet/WalletTopUpDrawer.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { createWalletTopUpCheckoutRouteDefinition } from "@arrhes/application-metadata/routes" -import { Button, ButtonOutlineContent, InputText, toast } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconPlus } from "@tabler/icons-react" -import { type JSX, useState } from "react" -import { Drawer } from "../../../../../components/overlays/drawer/drawer.tsx" -import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" -import { parseEuroAmountToCents } from "../../../../../utilities/parseEuroAmountToCents.ts" - -export function WalletTopUpDrawer(props: { children: JSX.Element; onSuccess: () => void }) { - const [open, setOpen] = useState(false) - const [amount, setAmount] = useState("25") - const [isLoading, setIsLoading] = useState(false) - - async function handleTopUp() { - const amountInCents = parseEuroAmountToCents(amount) - - if (Number.isNaN(amountInCents) || amountInCents <= 0) { - toast({ - title: "Montant invalide", - variant: "error", - }) - return - } - - setIsLoading(true) - const response = await getResponseBodyFromAPI({ - routeDefinition: createWalletTopUpCheckoutRouteDefinition, - body: { - amountInCents, - }, - }) - setIsLoading(false) - - if (response.ok === false) { - toast({ - title: "Impossible de démarrer le rechargement", - variant: "error", - }) - return - } - - props.onSuccess() - window.location.href = response.data.checkoutUrl - } - - return ( - - {props.children} - - - -
-

- Ajoutez un solde disponible immédiatement pour payer les achats ponctuels et les avances sur - abonnement. -

- setAmount(value ?? "")} - type="number" - placeholder="Montant en €" - /> - -
-
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/wallet/WalletTopUpPage.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/wallet/WalletTopUpPage.tsx new file mode 100644 index 00000000..f4104bb4 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/wallet/WalletTopUpPage.tsx @@ -0,0 +1,89 @@ +import { createWalletTopUpCheckoutRouteDefinition } from "@arrhes/application-metadata/routes" +import { Button, ButtonOutlineContent, InputText, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPlus } from "@tabler/icons-react" +import { useState } from "react" +import { Page } from "../../../../../components/layouts/page/page.tsx" +import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" +import { parseEuroAmountToCents } from "../../../../../utilities/parseEuroAmountToCents.ts" + +export function WalletTopUpPage(_props: { idOrganization: string }) { + const [amount, setAmount] = useState("25") + const [isLoading, setIsLoading] = useState(false) + + async function handleTopUp() { + const amountInCents = parseEuroAmountToCents(amount) + + if (Number.isNaN(amountInCents) || amountInCents <= 0) { + toast({ + title: "Montant invalide", + variant: "error", + }) + return + } + + setIsLoading(true) + const response = await getResponseBodyFromAPI({ + routeDefinition: createWalletTopUpCheckoutRouteDefinition, + body: { + amountInCents, + }, + }) + setIsLoading(false) + + if (response.ok === false) { + toast({ + title: "Impossible de démarrer le rechargement", + variant: "error", + }) + return + } + + window.location.href = response.data.checkoutUrl + } + + return ( + + + Recharger le portefeuille + + +
+

+ Ajoutez un solde disponible immédiatement pour payer les achats ponctuels et les avances sur + abonnement. +

+ setAmount(value ?? "")} + type="number" + placeholder="Montant en €" + /> + +
+
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/wallet/WalletWithdrawalDrawer.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/wallet/WalletWithdrawalDrawer.tsx deleted file mode 100644 index 30d96d2b..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/wallet/WalletWithdrawalDrawer.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { createWalletWithdrawalRouteDefinition } from "@arrhes/application-metadata/routes" -import { Button, ButtonOutlineContent, InputText, toast } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconMinus } from "@tabler/icons-react" -import { type JSX, useEffect, useState } from "react" -import { Drawer } from "../../../../../components/overlays/drawer/drawer.tsx" -import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" -import { parseEuroAmountToCents } from "../../../../../utilities/parseEuroAmountToCents.ts" - -export function WalletWithdrawalDrawer(props: { - children: JSX.Element - currentBalanceInCents: number - hasWithdrawalThisMonth: boolean - onSuccess: () => void -}) { - const [open, setOpen] = useState(false) - const [amount, setAmount] = useState("") - const [isLoading, setIsLoading] = useState(false) - - useEffect(() => { - if (open) { - setAmount((props.currentBalanceInCents / 100).toFixed(2).replace(".", ",")) - } - }, [ - open, - props.currentBalanceInCents, - ]) - - async function handleWithdrawal() { - const amountInCents = parseEuroAmountToCents(amount) - - if (Number.isNaN(amountInCents) || amountInCents <= 0) { - toast({ - title: "Montant invalide", - variant: "error", - }) - return - } - - setIsLoading(true) - const response = await getResponseBodyFromAPI({ - routeDefinition: createWalletWithdrawalRouteDefinition, - body: { - amountInCents, - }, - }) - setIsLoading(false) - - if (response.ok === false) { - toast({ - title: response.error?.cause ?? "Impossible d'initier le retrait", - variant: "error", - }) - return - } - - toast({ - title: "Retrait demandé", - variant: "success", - }) - setOpen(false) - props.onSuccess() - } - - return ( - - {props.children} - - - -
-

- Le retrait est effectué par remboursement Mollie depuis un rechargement compatible déjà - payé. -

-

- Un seul retrait portefeuille est autorisé par mois calendaire pour limiter les abus. -

- setAmount(value ?? "")} - type="number" - placeholder="Montant en €" - /> - -
-
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/wallet/WalletWithdrawalPage.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/wallet/WalletWithdrawalPage.tsx new file mode 100644 index 00000000..d142d259 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/wallet/WalletWithdrawalPage.tsx @@ -0,0 +1,161 @@ +import { + createWalletWithdrawalRouteDefinition, + readAllOrganizationPaymentsRouteDefinition, + readOneOrganizationRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { Button, ButtonOutlineContent, InputText, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconMinus } from "@tabler/icons-react" +import { useEffect, useState } from "react" +import { DataWrapper } from "../../../../../components/layouts/DataWrapper.tsx" +import { Page } from "../../../../../components/layouts/page/page.tsx" +import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" +import { parseEuroAmountToCents } from "../../../../../utilities/parseEuroAmountToCents.ts" + +function hasCurrentMonthWithdrawal( + payments: Array<{ + category: string + createdAt: string + status: string + }>, +) { + const now = new Date() + + return payments.some((payment) => { + if (payment.category !== "withdrawal" || payment.status === "failed") { + return false + } + + const createdAt = new Date(payment.createdAt) + return createdAt.getUTCFullYear() === now.getUTCFullYear() && createdAt.getUTCMonth() === now.getUTCMonth() + }) +} + +function WalletWithdrawalForm(props: { currentBalanceInCents: number; hasWithdrawalThisMonth: boolean }) { + const [amount, setAmount] = useState("") + const [isLoading, setIsLoading] = useState(false) + + useEffect(() => { + setAmount((props.currentBalanceInCents / 100).toFixed(2).replace(".", ",")) + }, [ + props.currentBalanceInCents, + ]) + + async function handleWithdrawal() { + const amountInCents = parseEuroAmountToCents(amount) + + if (Number.isNaN(amountInCents) || amountInCents <= 0) { + toast({ + title: "Montant invalide", + variant: "error", + }) + return + } + + setIsLoading(true) + const response = await getResponseBodyFromAPI({ + routeDefinition: createWalletWithdrawalRouteDefinition, + body: { + amountInCents, + }, + }) + setIsLoading(false) + + if (response.ok === false) { + toast({ + title: response.error?.cause ?? "Impossible d'initier le retrait", + variant: "error", + }) + return + } + + toast({ + title: "Retrait demandé", + variant: "success", + }) + } + + return ( +
+

+ Le retrait est effectué par remboursement Mollie depuis un rechargement compatible déjà payé. +

+

+ Un seul retrait portefeuille est autorisé par mois calendaire pour limiter les abus. +

+ setAmount(value ?? "")} + type="number" + placeholder="Montant en €" + /> + +
+ ) +} + +export function WalletWithdrawalPage({ idOrganization }: { idOrganization: string }) { + return ( + + + Retirer du portefeuille + + + + {(organization) => ( + + {(payments) => ( + + )} + + )} + + + + ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/wallet/organizationWalletPage.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/wallet/organizationWalletPage.tsx deleted file mode 100644 index 3f83d50c..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/wallet/organizationWalletPage.tsx +++ /dev/null @@ -1,291 +0,0 @@ -import { - readAllOrganizationPaymentsRouteDefinition, - readOneOrganizationRouteDefinition, -} from "@arrhes/application-metadata/routes" -import { FREE_STORAGE_BYTES, STORAGE_PRICE_PER_GB_IN_CENTS } from "@arrhes/application-metadata/utilities" -import { Button, ButtonOutlineContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconAlertTriangle, IconCashMinus, IconPigMoney, IconPlus, IconWallet } from "@tabler/icons-react" -import { useParams } from "@tanstack/react-router" -import { useState } from "react" -import { DataWrapper } from "../../../../../components/layouts/dataWrapper.tsx" -import { Page } from "../../../../../components/layouts/page/page.tsx" -import { SettingsSection } from "../../../../../components/layouts/settingsSection/settingsSection.tsx" -import { organizationBillingRoute } from "../../../../../routes/root/dashboard/organizations/$idOrganization/organizationBilling/organizationBillingRoute.tsx" -import { formatEuros } from "../../../../../utilities/formatEuros.tsx" -import { OrganizationBillingDisclaimerBanner } from "./OrganizationBillingDisclaimerBanner.tsx" -import { WalletTopUpDrawer } from "./WalletTopUpDrawer.tsx" -import { WalletWithdrawalDrawer } from "./WalletWithdrawalDrawer.tsx" -import { WalletCurrentMonth } from "./walletCurrentMonth.tsx" - -function getRecurringStorageAmountInCents(storageLimit: number) { - return ( - Math.max(Math.round((storageLimit - FREE_STORAGE_BYTES) / FREE_STORAGE_BYTES), 0) * - STORAGE_PRICE_PER_GB_IN_CENTS - ) -} - -function getNextMonthSubscriptionAmountInCents(organization: { licenceAmount: number; storageLimit: number }) { - return organization.licenceAmount + getRecurringStorageAmountInCents(organization.storageLimit) -} - -function hasCurrentMonthWithdrawal( - payments: Array<{ - category: string - createdAt: string - status: string - }>, -) { - const now = new Date() - - return payments.some((payment) => { - if (payment.category !== "withdrawal" || payment.status === "failed") { - return false - } - - const createdAt = new Date(payment.createdAt) - return createdAt.getUTCFullYear() === now.getUTCFullYear() && createdAt.getUTCMonth() === now.getUTCMonth() - }) -} - -export function OrganizationWalletPage() { - const [refreshKey, setRefreshKey] = useState(0) - const params = useParams({ - from: organizationBillingRoute.id, - }) - - return ( - - - - {(organization) => ( -
- {(() => { - const nextMonthSubscriptionAmountInCents = - getNextMonthSubscriptionAmountInCents(organization) - const isWalletShortForNextMonth = - nextMonthSubscriptionAmountInCents > 0 && - organization.walletBalanceInCents < nextMonthSubscriptionAmountInCents - - return ( - <> - - {isWalletShortForNextMonth ? ( -
-
- -
- - Solde insuffisant pour le prochain prélèvement mensuel - - - {formatEuros(organization.walletBalanceInCents)} disponibles - pour {formatEuros(nextMonthSubscriptionAmountInCents)}{" "} - requis au début du mois prochain. - -
-
- setRefreshKey((key) => key + 1)}> - - -
- ) : null} - - {(payments) => ( - - -
-
- setRefreshKey((key) => key + 1)} - > - - - setRefreshKey((key) => key + 1)} - > - - -
-
-
-
- - - Montant disponible - - - {formatEuros(organization.walletBalanceInCents)} - -
-
-
-
-
- )} -
- - - - - - ) - })()} -
- )} -
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/wallet/walletCurrentMonth.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationBilling/wallet/walletCurrentMonth.tsx deleted file mode 100644 index ead0f903..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationBilling/wallet/walletCurrentMonth.tsx +++ /dev/null @@ -1,388 +0,0 @@ -import type { returnedSchemas } from "@arrhes/application-metadata" -import { - readAllInvoicesRouteDefinition, - readAllOrganizationPaymentsRouteDefinition, -} from "@arrhes/application-metadata/routes" -import { css } from "@arrhes/ui/utilities/cn.js" -import type * as v from "valibot" -import { DataWrapper } from "../../../../../components/layouts/dataWrapper.tsx" -import { formatEuros } from "../../../../../utilities/formatEuros.tsx" - -function getPaymentLineType(payment: { - category: string - serviceType: string | null - sequenceType: string | null -}): "subscription" | "storage_gb" | "agent_tokens_million" | "ocr_pages_hundred" | null { - if ( - payment.category === "top_up" || - payment.category === "withdrawal" || - payment.category === "setup" || - payment.sequenceType === "setup" - ) { - return null - } - - if (payment.serviceType === "storage_gb") { - return "storage_gb" - } - - if (payment.serviceType === "agent_tokens_million") { - return "agent_tokens_million" - } - - if (payment.serviceType === "ocr_pages_hundred") { - return "ocr_pages_hundred" - } - - return "subscription" -} - -export function WalletCurrentMonth(props: { organization: v.InferOutput }) { - return ( - - {(invoices) => ( - - {(payments) => { - const now = new Date() - const currentPeriodStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)) - const currentInvoice = invoices.find((inv) => { - const invStart = new Date(inv.startingAt) - return ( - invStart.getUTCFullYear() === currentPeriodStart.getUTCFullYear() && - invStart.getUTCMonth() === currentPeriodStart.getUTCMonth() - ) - }) - - const currentMonthPayments = currentInvoice - ? payments.filter( - (payment) => - payment.idInvoice === currentInvoice.id && - payment.status !== "failed" && - payment.status !== "refunded", - ) - : [] - - const lineItems = [ - { - type: "subscription", - label: "Abonnement", - unit: null, - }, - { - type: "storage_gb", - label: "Stockage", - unit: "1 Go / mois", - }, - { - type: "agent_tokens_million", - label: "Tokens Assistant IA", - unit: "1 million de tokens", - }, - { - type: "ocr_pages_hundred", - label: "Traitement de pages par OCR", - unit: "1 page", - }, - ] as const - - const totalsByType = currentMonthPayments.reduce< - Record< - string, - { - amountHT: number - amountTVA: number - quantity: number - } - > - >((acc, payment) => { - const lineType = getPaymentLineType(payment) - - if (lineType === null) return acc - if (!acc[lineType]) { - acc[lineType] = { - amountHT: 0, - amountTVA: 0, - quantity: 0, - } - } - - acc[lineType].amountHT += payment.amountHTInCents - acc[lineType].amountTVA += payment.amountTVAInCents - acc[lineType].quantity += 1 - - return acc - }, {}) - - // The first 1 Go of storage is always included free - // in the subscription — add it to the display so the - // quantity reflects the actual allocated storage. - if (props.organization.licenceAmount > 0) { - if (!totalsByType.storage_gb) - totalsByType.storage_gb = { - amountHT: 0, - amountTVA: 0, - quantity: 0, - } - totalsByType.storage_gb.quantity += 1 - } - - const totalHTAmount = currentMonthPayments.reduce( - (sum, payment) => - getPaymentLineType(payment) === null ? sum : sum + payment.amountHTInCents, - 0, - ) - - const totalTVAAmount = currentMonthPayments.reduce( - (sum, payment) => - getPaymentLineType(payment) === null ? sum : sum + payment.amountTVAInCents, - 0, - ) - const totalTTCAmount = totalHTAmount + totalTVAAmount - - return ( -
-
-
- - Service - - - Unité - - - Quantité - - - Montant HT - - - TVA - - - Montant TTC - -
- {lineItems.map((line, index) => { - const data = totalsByType[line.type] ?? { - amountHT: 0, - amountTVA: 0, - quantity: 0, - } - const amountTTC = data.amountHT + data.amountTVA - - return ( -
- - {line.label} - - - {line.unit ?? "-"} - - - {data.quantity === 0 ? "-" : data.quantity} - - - {formatEuros(data.amountHT)} - - - {formatEuros(data.amountTVA)} - - - {formatEuros(amountTTC)} - -
- ) - })} -
- - Total - - - - - {formatEuros(totalHTAmount)} - - - {formatEuros(totalTVAAmount)} - - - {formatEuros(totalTTCAmount)} - -
-
-
- ) - }} -
- )} -
- ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationLayout.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationLayout.tsx deleted file mode 100644 index f061c4d5..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationLayout.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { - IconCalendarEvent, - IconPlug, - IconRobot, - IconRosetteDiscountCheck, - IconSettings, - IconUsers, -} from "@tabler/icons-react" -import { Outlet, useParams } from "@tanstack/react-router" -import { Fragment } from "react/jsx-runtime" -import { PageNavigation } from "../../../components/layouts/page/pageNavigation.tsx" -import { organizationLayoutRoute } from "../../../routes/root/dashboard/organizations/$idOrganization/organizationLayoutRoute.tsx" - -export function OrganizationLayout() { - const params = useParams({ - from: organizationLayoutRoute.id, - }) - - return ( - - , - to: "/dashboard/organisations/$idOrganization/exercices", - params: { - idOrganization: params.idOrganization, - }, - }, - { - label: "Assistant", - icon: , - to: "/dashboard/organisations/$idOrganization/agent", - params: { - idOrganization: params.idOrganization, - }, - }, - { - label: "Membres", - icon: , - to: "/dashboard/organisations/$idOrganization/membres", - params: { - idOrganization: params.idOrganization, - }, - }, - { - label: "Facturation", - icon: , - to: "/dashboard/organisations/$idOrganization/facturation", - params: { - idOrganization: params.idOrganization, - }, - }, - { - label: "API", - icon: , - to: "/dashboard/organisations/$idOrganization/api", - params: { - idOrganization: params.idOrganization, - }, - }, - { - label: "Paramètres", - icon: , - to: "/dashboard/organisations/$idOrganization/paramètres", - params: { - idOrganization: params.idOrganization, - }, - }, - ]} - /> - - - ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationSettings/DeleteOneOrganization.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationSettings/DeleteOneOrganization.tsx new file mode 100644 index 00000000..03b69305 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationSettings/DeleteOneOrganization.tsx @@ -0,0 +1,88 @@ +import { + deleteOneOrganizationRouteDefinition, + getAllMyOrganizationsRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, ButtonOutlineContent, ButtonPlainContent, Dialog, toast, useModalStore } from "@arrhes/ui" +import { type ComponentPropsWithRef, type ReactElement, useId } from "react" +import type * as v from "valibot" +import { applicationRouter } from "../../../../routes/applicationRouter.tsx" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../utilities/invalidateData.ts" + +export function DeleteOneOrganization(props: { + idOrganization: v.InferOutput["id"] + children: ReactElement> +}) { + const modalId = useId() + const { open: openModal, close: closeModal } = useModalStore() + + async function onSubmit() { + const deleteResponse = await getResponseBodyFromAPI({ + routeDefinition: deleteOneOrganizationRouteDefinition, + body: {}, + }) + + if (deleteResponse.ok === false) { + toast({ + title: "Erreur lors de la suppression de l'organisation", + variant: "error", + }) + return + } + + await invalidateData({ + routeDefinition: getAllMyOrganizationsRouteDefinition, + body: {}, + }) + + toast({ + title: "Organisation supprimée", + variant: "success", + }) + + applicationRouter.navigate({ + to: "/dashboard/organisations", + }) + } + + return ( + + + + , + ) + } + > + {props.children} + + ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationSettings/OrganizationSecurityPage.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationSettings/OrganizationSecurityPage.tsx new file mode 100644 index 00000000..14e99ed4 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationSettings/OrganizationSecurityPage.tsx @@ -0,0 +1,54 @@ +import { readOneOrganizationRouteDefinition } from "@arrhes/application-metadata/routes" +import { ButtonOutlineContent } from "@arrhes/ui" +import { IconTrash } from "@tabler/icons-react" +import { useParams } from "@tanstack/react-router" +import { DataWrapper } from "../../../../components/layouts/DataWrapper.tsx" +import { Page } from "../../../../components/layouts/page/page.tsx" +import { SettingsSection } from "../../../../components/layouts/settingsSection/settingsSection.tsx" +import { DeleteOneOrganization } from "./DeleteOneOrganization.tsx" + +export function OrganizationSecurityPage({ idOrganization: idOrganizationProp }: { idOrganization?: string } = {}) { + const params = useParams({ + strict: false, + }) as { + idOrganization?: string + } + const idOrganization = idOrganizationProp ?? params.idOrganization ?? "" + + return ( + + + + {(organization) => { + return ( + + + + + } + text="Supprimer" + color="danger" + /> + + + + ) + }} + + + + ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationSettings/OrganizationSettingsLayout.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationSettings/OrganizationSettingsLayout.tsx new file mode 100644 index 00000000..8a7afc08 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationSettings/OrganizationSettingsLayout.tsx @@ -0,0 +1,5 @@ +import { Outlet } from "@tanstack/react-router" + +export function OrganizationSettingsLayout() { + return +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationSettings/OrganizationSettingsPage.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationSettings/OrganizationSettingsPage.tsx new file mode 100644 index 00000000..6bb53812 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationSettings/OrganizationSettingsPage.tsx @@ -0,0 +1,49 @@ +import { readOneOrganizationRouteDefinition } from "@arrhes/application-metadata/routes" +import { ButtonOutlineContent } from "@arrhes/ui" +import { IconPencil } from "@tabler/icons-react" +import { useParams } from "@tanstack/react-router" +import { DataWrapper } from "../../../../components/layouts/DataWrapper.tsx" +import { Page } from "../../../../components/layouts/page/page.tsx" +import { SettingsSection } from "../../../../components/layouts/settingsSection/settingsSection.tsx" +import { UpdateOneOrganization } from "./UpdateOneOrganization.tsx" + +export function OrganizationSettingsPage({ idOrganization: idOrganizationProp }: { idOrganization?: string } = {}) { + const params = useParams({ + strict: false, + }) as { + idOrganization?: string + } + const idOrganization = idOrganizationProp ?? params.idOrganization ?? "" + + return ( + + + + {(organization) => { + return ( + + + + + } + text="Modifier" + /> + + + + ) + }} + + + + ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationSettings/OrganizationSettingsTabContent.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationSettings/OrganizationSettingsTabContent.tsx new file mode 100644 index 00000000..d39a3dc3 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationSettings/OrganizationSettingsTabContent.tsx @@ -0,0 +1,48 @@ +import { IconHome, IconLock } from "@tabler/icons-react" +import { lazy, Suspense } from "react" +import { SubPageContent } from "../../../../components/layouts/SubPageContent.tsx" + +const OrganizationSettingsPage = lazy(() => + import("./OrganizationSettingsPage.js").then((m) => ({ + default: m.OrganizationSettingsPage, + })), +) +const OrganizationSecurityPage = lazy(() => + import("./OrganizationSecurityPage.js").then((m) => ({ + default: m.OrganizationSecurityPage, + })), +) + +export function OrganizationSettingsTabContent(props: { idOrganization: string }) { + return ( + , + content: ( + + + + ), + }, + { + key: "sécurité", + label: "Sécurité", + icon: , + content: ( + + + + ), + }, + ], + }, + }} + /> + ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationSettings/UpdateOneOrganization.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationSettings/UpdateOneOrganization.tsx new file mode 100644 index 00000000..3541fc92 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationSettings/UpdateOneOrganization.tsx @@ -0,0 +1,165 @@ +import { + readOneOrganizationRouteDefinition, + updateOneOrganizationRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, InputText, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPencil } from "@tabler/icons-react" +import type { JSX } from "react" +import { Fragment } from "react/jsx-runtime" +import type * as v from "valibot" +import { FormControl } from "../../../../components/forms/FormControl.tsx" +import { FormError } from "../../../../components/forms/FormError.tsx" +import { FormField } from "../../../../components/forms/FormField.tsx" +import { FormItem } from "../../../../components/forms/FormItem.tsx" +import { FormLabel } from "../../../../components/forms/FormLabel.tsx" +import { FormRoot } from "../../../../components/forms/FormRoot.tsx" +import { useTabs } from "../../../../contexts/tabs/useTabs.tsx" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../utilities/invalidateData.ts" + +export function UpdateOneOrganization(props: { + organization: v.InferOutput + children: JSX.Element +}) { + const { openPanelTab, closeTab } = useTabs() + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationSettings/deleteOneOrganization.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationSettings/deleteOneOrganization.tsx deleted file mode 100644 index 7b9611c3..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationSettings/deleteOneOrganization.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { - deleteOneOrganizationRouteDefinition, - getAllMyOrganizationsRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { toast } from "@arrhes/ui" -import type { ComponentPropsWithRef, ReactElement } from "react" -import type * as v from "valibot" -import { ConfirmationModal } from "../../../../components/overlays/dialog/confirmationModal.tsx" -import { applicationRouter } from "../../../../routes/applicationRouter.tsx" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../utilities/invalidateData.ts" - -export function DeleteOneOrganization(props: { - idOrganization: v.InferOutput["id"] - children: ReactElement> -}) { - return ( - - Cette action supprimera l'organisation et toutes les données associées. -
- Cette action est irréversible. - - } - submitButtonProps={{ - color: "danger", - text: "Supprimer l'organisation", - }} - onSubmit={async () => { - const deleteResponse = await getResponseBodyFromAPI({ - routeDefinition: deleteOneOrganizationRouteDefinition, - body: {}, - }) - - if (deleteResponse.ok === false) { - toast({ - title: "Erreur lors de la suppression de l'organisation", - variant: "error", - }) - return - } - - await invalidateData({ - routeDefinition: getAllMyOrganizationsRouteDefinition, - body: {}, - }) - - toast({ - title: "Organisation supprimée", - variant: "success", - }) - - applicationRouter.navigate({ - to: "/dashboard/organisations", - }) - }} - > - {props.children} -
- ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationSettings/organizationSecurityPage.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationSettings/organizationSecurityPage.tsx deleted file mode 100644 index da7b2e66..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationSettings/organizationSecurityPage.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { readOneOrganizationRouteDefinition } from "@arrhes/application-metadata/routes" -import { ButtonOutlineContent } from "@arrhes/ui" -import { IconTrash } from "@tabler/icons-react" -import { useParams } from "@tanstack/react-router" -import { DataWrapper } from "../../../../components/layouts/dataWrapper.tsx" -import { Page } from "../../../../components/layouts/page/page.tsx" -import { SettingsSection } from "../../../../components/layouts/settingsSection/settingsSection.tsx" -import { organizationSecurityRoute } from "../../../../routes/root/dashboard/organizations/$idOrganization/organizationSettings/organizationSecurityRoute.tsx" -import { DeleteOneOrganization } from "./deleteOneOrganization.tsx" - -export function OrganizationSecurityPage() { - const params = useParams({ - from: organizationSecurityRoute.id, - }) - - return ( - - - - {(organization) => { - return ( - - - - - } - text="Supprimer" - color="danger" - /> - - - - ) - }} - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationSettings/organizationSettingsLayout.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationSettings/organizationSettingsLayout.tsx deleted file mode 100644 index 4708b614..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationSettings/organizationSettingsLayout.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { IconHome, IconLock } from "@tabler/icons-react" -import { useParams } from "@tanstack/react-router" -import { SubPageLayout } from "../../../../components/layouts/subPageLayout.tsx" -import { organizationSettingsLayoutRoute } from "../../../../routes/root/dashboard/organizations/$idOrganization/organizationSettings/organizationSettingsLayoutRoute.tsx" - -export function OrganizationSettingsLayout() { - const params = useParams({ - from: organizationSettingsLayoutRoute.id, - }) - - return ( - , - to: "/dashboard/organisations/$idOrganization/paramètres", - params: { - idOrganization: params.idOrganization, - }, - }, - { - label: "Sécurité", - icon: , - to: "/dashboard/organisations/$idOrganization/paramètres/sécurité", - params: { - idOrganization: params.idOrganization, - }, - }, - ], - }, - }} - /> - ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationSettings/organizationSettingsPage.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationSettings/organizationSettingsPage.tsx deleted file mode 100644 index 3059a551..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationSettings/organizationSettingsPage.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { readOneOrganizationRouteDefinition } from "@arrhes/application-metadata/routes" -import { ButtonOutlineContent } from "@arrhes/ui" -import { IconPencil } from "@tabler/icons-react" -import { useParams } from "@tanstack/react-router" -import { DataWrapper } from "../../../../components/layouts/dataWrapper.tsx" -import { Page } from "../../../../components/layouts/page/page.tsx" -import { SettingsSection } from "../../../../components/layouts/settingsSection/settingsSection.tsx" -import { organizationSettingsRoute } from "../../../../routes/root/dashboard/organizations/$idOrganization/organizationSettings/organizationSettingsRoute.tsx" -import { UpdateOneOrganization } from "./updateOneOrganization.tsx" - -export function OrganizationSettingsPage() { - const params = useParams({ - from: organizationSettingsRoute.id, - }) - - return ( - - - - {(organization) => { - return ( - - - - - } - text="Modifier" - /> - - - - ) - }} - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationSettings/updateOneOrganization.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationSettings/updateOneOrganization.tsx deleted file mode 100644 index 9311e3ec..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationSettings/updateOneOrganization.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { - readOneOrganizationRouteDefinition, - updateOneOrganizationRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { InputText, toast } from "@arrhes/ui" -import { IconPencil } from "@tabler/icons-react" -import { type JSX, useState } from "react" -import { Fragment } from "react/jsx-runtime" -import type * as v from "valibot" -import { FormControl } from "../../../../components/forms/formControl.tsx" -import { FormError } from "../../../../components/forms/formError.tsx" -import { FormField } from "../../../../components/forms/formField.tsx" -import { FormItem } from "../../../../components/forms/formItem.tsx" -import { FormLabel } from "../../../../components/forms/formLabel.tsx" -import { FormRoot } from "../../../../components/forms/formRoot.tsx" -import { Drawer } from "../../../../components/overlays/drawer/drawer.tsx" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../utilities/invalidateData.ts" - -export function UpdateOneOrganization(props: { - organization: v.InferOutput - children: JSX.Element -}) { - const [open, setOpen] = useState(false) - - return ( - - {props.children} - - - - , - text: "Modifier l'organisation", - }} - onSubmit={async (data) => { - const response = await getResponseBodyFromAPI({ - routeDefinition: updateOneOrganizationRouteDefinition, - body: data, - }) - if (!response.ok) { - toast({ - title: "Impossible de modifier l'organisation", - variant: "error", - }) - return false - } - - toast({ - title: "Organisation modifiée avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await invalidateData({ - routeDefinition: readOneOrganizationRouteDefinition, - body: { - idOrganization: props.organization.id, - }, - }) - - setOpen(false) - }} - > - {(form) => ( - - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - - )} - - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationUsers/$idOrganizationUser/DeleteOneOrganizationUser.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationUsers/$idOrganizationUser/DeleteOneOrganizationUser.tsx new file mode 100644 index 00000000..4671198a --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationUsers/$idOrganizationUser/DeleteOneOrganizationUser.tsx @@ -0,0 +1,83 @@ +import { + deleteOneOrganizationUserRouteDefinition, + readOneOrganizationRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, ButtonOutlineContent, ButtonPlainContent, Dialog, toast, useModalStore } from "@arrhes/ui" +import { type ComponentPropsWithRef, type ReactElement, useId } from "react" +import type * as v from "valibot" +import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../utilities/invalidateData.ts" + +export function DeleteOneOrganizationUser(props: { + organizationUser: v.InferOutput + children: ReactElement> +}) { + const modalId = useId() + const { open: openModal, close: closeModal } = useModalStore() + + async function onSubmit() { + const deleteResponse = await getResponseBodyFromAPI({ + routeDefinition: deleteOneOrganizationUserRouteDefinition, + body: { + idOrganizationUser: props.organizationUser.id, + }, + }) + + if (deleteResponse.ok === false) { + toast({ + title: "Erreur lors de la révocation de l'utilisateur", + variant: "error", + }) + return + } + + await invalidateData({ + routeDefinition: readOneOrganizationRouteDefinition, + body: { + idOrganization: props.organizationUser.idOrganization, + }, + }) + toast({ + title: "Utilisateur révoqué de l'organisation", + variant: "success", + }) + } + + return ( + + + + , + ) + } + > + {props.children} + + ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationUsers/$idOrganizationUser/UpdateOneOrganizationUser.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationUsers/$idOrganizationUser/UpdateOneOrganizationUser.tsx new file mode 100644 index 00000000..90aac602 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationUsers/$idOrganizationUser/UpdateOneOrganizationUser.tsx @@ -0,0 +1,130 @@ +import { + readAllOrganizationUsersRouteDefinition, + updateOneOrganizationUserRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, InputToggle, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPencil } from "@tabler/icons-react" +import type { JSX } from "react" +import { Fragment } from "react/jsx-runtime" +import type * as v from "valibot" +import { FormControl } from "../../../../../components/forms/FormControl.tsx" +import { FormError } from "../../../../../components/forms/FormError.tsx" +import { FormField } from "../../../../../components/forms/FormField.tsx" +import { FormItem } from "../../../../../components/forms/FormItem.tsx" +import { FormLabel } from "../../../../../components/forms/FormLabel.tsx" +import { FormRoot } from "../../../../../components/forms/FormRoot.tsx" +import { useTabs } from "../../../../../contexts/tabs/useTabs.tsx" +import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../utilities/invalidateData.ts" + +export function UpdateOneOrganizationUser(props: { + organizationUser: v.InferOutput + children: JSX.Element +}) { + const { openPanelTab, closeTab } = useTabs() + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationUsers/$idOrganizationUser/deleteOneOrganizationUser.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationUsers/$idOrganizationUser/deleteOneOrganizationUser.tsx deleted file mode 100644 index 4127322f..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationUsers/$idOrganizationUser/deleteOneOrganizationUser.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { - deleteOneOrganizationUserRouteDefinition, - readOneOrganizationRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { toast } from "@arrhes/ui" -import type { ComponentPropsWithRef, ReactElement } from "react" -import type * as v from "valibot" -import { ConfirmationModal } from "../../../../../components/overlays/dialog/confirmationModal.tsx" -import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../utilities/invalidateData.ts" - -export function DeleteOneOrganizationUser(props: { - organizationUser: v.InferOutput - children: ReactElement> -}) { - async function onSubmit() { - const deleteResponse = await getResponseBodyFromAPI({ - routeDefinition: deleteOneOrganizationUserRouteDefinition, - body: { - idOrganizationUser: props.organizationUser.id, - }, - }) - - if (deleteResponse.ok === false) { - toast({ - title: "Erreur lors de la révocation de l'utilisateur", - variant: "error", - }) - return - } - - await invalidateData({ - routeDefinition: readOneOrganizationRouteDefinition, - body: { - idOrganization: props.organizationUser.idOrganization, - }, - }) - toast({ - title: "Utilisateur révoqué de l'organisation", - variant: "success", - }) - } - - return ( - - {props.children} - - ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationUsers/$idOrganizationUser/updateOneOrganizationUser.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationUsers/$idOrganizationUser/updateOneOrganizationUser.tsx deleted file mode 100644 index fa22d598..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationUsers/$idOrganizationUser/updateOneOrganizationUser.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { - readAllOrganizationUsersRouteDefinition, - updateOneOrganizationUserRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { InputToggle, toast } from "@arrhes/ui" -import { IconPencil } from "@tabler/icons-react" -import { type JSX, useState } from "react" -import { Fragment } from "react/jsx-runtime" -import type * as v from "valibot" -import { FormControl } from "../../../../../components/forms/formControl.tsx" -import { FormError } from "../../../../../components/forms/formError.tsx" -import { FormField } from "../../../../../components/forms/formField.tsx" -import { FormItem } from "../../../../../components/forms/formItem.tsx" -import { FormLabel } from "../../../../../components/forms/formLabel.tsx" -import { FormRoot } from "../../../../../components/forms/formRoot.tsx" -import { Drawer } from "../../../../../components/overlays/drawer/drawer.tsx" -import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../utilities/invalidateData.ts" - -export function UpdateOneOrganizationUser(props: { - organizationUser: v.InferOutput - children: JSX.Element -}) { - const [open, setOpen] = useState(false) - - return ( - - {props.children} - - - - , - text: "Modifier l'utilisateur", - }} - onSubmit={async (data) => { - const response = await getResponseBodyFromAPI({ - routeDefinition: updateOneOrganizationUserRouteDefinition, - body: data, - }) - if (!response.ok) { - toast({ - title: "Impossible de modifier l'utilisateur", - variant: "error", - }) - return false - } - - toast({ - title: "Utilisateur modifié avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await invalidateData({ - routeDefinition: readAllOrganizationUsersRouteDefinition, - body: {}, - }) - - setOpen(false) - }} - > - {(form) => ( - - ( - - - - - - - - )} - /> - - )} - - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationUsers/CreateOneOrganizationUser.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationUsers/CreateOneOrganizationUser.tsx new file mode 100644 index 00000000..65d7174b --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationUsers/CreateOneOrganizationUser.tsx @@ -0,0 +1,154 @@ +import { + createOneOrganizationUserRouteDefinition, + readAllOrganizationUsersRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, InputText, InputToggle, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPlus } from "@tabler/icons-react" +import type { JSX } from "react" +import { Fragment } from "react/jsx-runtime" +import type * as v from "valibot" +import { FormControl } from "../../../../components/forms/FormControl.tsx" +import { FormError } from "../../../../components/forms/FormError.tsx" +import { FormField } from "../../../../components/forms/FormField.tsx" +import { FormItem } from "../../../../components/forms/FormItem.tsx" +import { FormLabel } from "../../../../components/forms/FormLabel.tsx" +import { FormRoot } from "../../../../components/forms/FormRoot.tsx" +import { useTabs } from "../../../../contexts/tabs/useTabs.tsx" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../utilities/invalidateData.ts" + +export function CreateOneOrganizationUser(props: { + idOrganization: v.InferOutput["id"] + children: JSX.Element +}) { + const { openPanelTab, closeTab } = useTabs() + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationUsers/OrganizationUserListTableRow.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationUsers/OrganizationUserListTableRow.tsx new file mode 100644 index 00000000..4c0236b0 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationUsers/OrganizationUserListTableRow.tsx @@ -0,0 +1,116 @@ +import type { readAllOrganizationUsersRouteDefinition } from "@arrhes/application-metadata/routes" +import { ButtonOutlineContent, Chip } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconUserEdit, IconUserX } from "@tabler/icons-react" +import type * as v from "valibot" +import { ListTable } from "../../../../components/layouts/listTable/listTable.tsx" +import { DeleteOneOrganizationUser } from "./$idOrganizationUser/DeleteOneOrganizationUser.tsx" +import { UpdateOneOrganizationUser } from "./$idOrganizationUser/UpdateOneOrganizationUser.tsx" + +export function OrganizationUserListTableRow(props: { + organizationUser: v.InferOutput[number] +}) { + return ( + +
+
+
+ + {props.organizationUser.user.email} + + {props.organizationUser.user.alias && ( + + {props.organizationUser.user.alias} + + )} +
+
+ {props.organizationUser.isAdmin && ( + + )} + {props.organizationUser.status === "active" && ( + + )} + {props.organizationUser.status === "invited" && ( + + )} + {props.organizationUser.status === "removed" && ( + + )} +
+
+
+ + } + title="Changer les droits" + /> + + + } + title="Retirer l'utilisateur" + color="danger" + /> + +
+
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationUsers/OrganizationUsersListTable.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationUsers/OrganizationUsersListTable.tsx new file mode 100644 index 00000000..753daeeb --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationUsers/OrganizationUsersListTable.tsx @@ -0,0 +1,39 @@ +import { readAllOrganizationUsersRouteDefinition } from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { IconUsers } from "@tabler/icons-react" +import type * as v from "valibot" +import { DataWrapper } from "../../../../components/layouts/DataWrapper.tsx" +import { EmptyState } from "../../../../components/layouts/EmptyState.tsx" +import { ListTable } from "../../../../components/layouts/listTable/listTable.tsx" +import { OrganizationUserListTableRow } from "./OrganizationUserListTableRow.tsx" + +export function OrganizationUsersListTable(_props: { + idOrganization: v.InferOutput["id"] +}) { + return ( + + + {(organizationUsers) => { + if (organizationUsers.length === 0) { + return ( + } + title="Aucun utilisateur" + subtitle="Invitez un utilisateur pour commencer" + /> + ) + } + return organizationUsers.map((organizationUser) => ( + + )) + }} + + + ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationUsers/OrganizationUsersPage.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationUsers/OrganizationUsersPage.tsx new file mode 100644 index 00000000..0b88b667 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/organizationUsers/OrganizationUsersPage.tsx @@ -0,0 +1,44 @@ +import { ButtonOutlineContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconUserPlus } from "@tabler/icons-react" +import { useParams } from "@tanstack/react-router" +import { Page } from "../../../../components/layouts/page/page.tsx" +import { CreateOneOrganizationUser } from "./CreateOneOrganizationUser.tsx" +import { OrganizationUsersListTable } from "./OrganizationUsersListTable.tsx" + +export function OrganizationUsersPage({ idOrganization: idOrganizationProp }: { idOrganization?: string }) { + const params = useParams({ + strict: false, + }) as { + idOrganization?: string + } + const idOrganization = idOrganizationProp ?? params.idOrganization ?? "" + + return ( + + {/* + + Membres + + */} + +
+ + } + text="Inviter un collaborateur" + /> + +
+ +
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationUsers/createOneOrganizationUser.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationUsers/createOneOrganizationUser.tsx deleted file mode 100644 index 8dea24b9..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationUsers/createOneOrganizationUser.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { - createOneOrganizationUserRouteDefinition, - readAllOrganizationUsersRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { InputText, InputToggle, toast } from "@arrhes/ui" -import { IconPlus } from "@tabler/icons-react" -import { type JSX, useState } from "react" -import { Fragment } from "react/jsx-runtime" -import type * as v from "valibot" -import { FormControl } from "../../../../components/forms/formControl.tsx" -import { FormError } from "../../../../components/forms/formError.tsx" -import { FormField } from "../../../../components/forms/formField.tsx" -import { FormItem } from "../../../../components/forms/formItem.tsx" -import { FormLabel } from "../../../../components/forms/formLabel.tsx" -import { FormRoot } from "../../../../components/forms/formRoot.tsx" -import { Drawer } from "../../../../components/overlays/drawer/drawer.tsx" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../utilities/invalidateData.ts" - -export function CreateOneOrganizationUser(props: { - idOrganization: v.InferOutput["id"] - children: JSX.Element -}) { - const [open, setOpen] = useState(false) - - return ( - - {props.children} - - - - , - text: "Ajouter l'utilisateur", - }} - onSubmit={async (data) => { - const response = await getResponseBodyFromAPI({ - routeDefinition: createOneOrganizationUserRouteDefinition, - body: data, - }) - if (!response.ok) { - toast({ - title: "Impossible d'ajouter l'utilisateur", - variant: "error", - }) - return false - } - - toast({ - title: "Utilisateur ajouté avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await invalidateData({ - routeDefinition: readAllOrganizationUsersRouteDefinition, - body: {}, - }) - - setOpen(false) - }} - > - {(form) => ( - - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - - )} - - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationUsers/organizationUserListTableRow.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationUsers/organizationUserListTableRow.tsx deleted file mode 100644 index 4f3d382f..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationUsers/organizationUserListTableRow.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import type { readAllOrganizationUsersRouteDefinition } from "@arrhes/application-metadata/routes" -import { ButtonOutlineContent, Chip } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconUserEdit, IconUserX } from "@tabler/icons-react" -import type * as v from "valibot" -import { ListTable } from "../../../../components/layouts/listTable/listTable.tsx" -import { DeleteOneOrganizationUser } from "./$idOrganizationUser/deleteOneOrganizationUser.tsx" -import { UpdateOneOrganizationUser } from "./$idOrganizationUser/updateOneOrganizationUser.tsx" - -export function OrganizationUserListTableRow(props: { - organizationUser: v.InferOutput[number] -}) { - return ( - -
-
-
- - {props.organizationUser.user.email} - - {props.organizationUser.user.alias && ( - - {props.organizationUser.user.alias} - - )} -
-
- {props.organizationUser.isAdmin && ( - - )} - {props.organizationUser.status === "active" && ( - - )} - {props.organizationUser.status === "invited" && ( - - )} - {props.organizationUser.status === "removed" && ( - - )} -
-
-
- - } - title="Changer les droits" - /> - - - } - title="Retirer l'utilisateur" - color="danger" - /> - -
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationUsers/organizationUsersListTable.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationUsers/organizationUsersListTable.tsx deleted file mode 100644 index f7247ca4..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationUsers/organizationUsersListTable.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { readAllOrganizationUsersRouteDefinition } from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { IconUsers } from "@tabler/icons-react" -import type * as v from "valibot" -import { DataWrapper } from "../../../../components/layouts/dataWrapper.tsx" -import { EmptyState } from "../../../../components/layouts/emptyState.tsx" -import { ListTable } from "../../../../components/layouts/listTable/listTable.tsx" -import { OrganizationUserListTableRow } from "./organizationUserListTableRow.tsx" - -export function OrganizationUsersListTable(_props: { - idOrganization: v.InferOutput["id"] -}) { - return ( - - - {(organizationUsers) => { - if (organizationUsers.length === 0) { - return ( - } - title="Aucun utilisateur" - subtitle="Invitez un utilisateur pour commencer" - /> - ) - } - return organizationUsers.map((organizationUser) => ( - - )) - }} - - - ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/organizationUsers/organizationUsersPage.tsx b/packages/website/src/features/dashboard/$idOrganization/organizationUsers/organizationUsersPage.tsx deleted file mode 100644 index f4a06b2a..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/organizationUsers/organizationUsersPage.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { ButtonOutlineContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconUserPlus } from "@tabler/icons-react" -import { useParams } from "@tanstack/react-router" -import { Page } from "../../../../components/layouts/page/page.tsx" -import { organizationUsersRoute } from "../../../../routes/root/dashboard/organizations/$idOrganization/organizationUsers/organizationUsersRoute.tsx" -import { CreateOneOrganizationUser } from "./createOneOrganizationUser.tsx" -import { OrganizationUsersListTable } from "./organizationUsersListTable.tsx" - -export function OrganizationUsersPage() { - const params = useParams({ - from: organizationUsersRoute.id, - }) - - return ( - - {/* - - Membres - - */} - -
- - } - text="Inviter un collaborateur" - /> - -
- -
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/years/CreateOneYear.tsx b/packages/website/src/features/dashboard/$idOrganization/years/CreateOneYear.tsx new file mode 100644 index 00000000..fb660003 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/years/CreateOneYear.tsx @@ -0,0 +1,191 @@ +import { createOneYearRouteDefinition, readAllYearsRouteDefinition } from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, InputDate, InputText, toast } from "@arrhes/ui" +import { css, cx } from "@arrhes/ui/utilities/cn.js" +import { IconPlus } from "@tabler/icons-react" +import type { JSX } from "react" +import { Fragment } from "react/jsx-runtime" +import type * as v from "valibot" +import { FormControl } from "../../../../components/forms/FormControl.tsx" +import { FormError } from "../../../../components/forms/FormError.tsx" +import { FormField } from "../../../../components/forms/FormField.tsx" +import { FormItem } from "../../../../components/forms/FormItem.tsx" +import { FormLabel } from "../../../../components/forms/FormLabel.tsx" +import { FormRoot } from "../../../../components/forms/FormRoot.tsx" +import { useTabs } from "../../../../contexts/tabs/useTabs.tsx" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../utilities/invalidateData.ts" +import { YearSelect } from "./YearSelect.tsx" + +export function CreateOneYear(props: { + idOrganization: v.InferOutput["id"] + children: JSX.Element + className?: string +}) { + const { openPanelTab, closeTab } = useTabs() + const currentDate = new Date() + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/years/YearListTableRow.tsx b/packages/website/src/features/dashboard/$idOrganization/years/YearListTableRow.tsx new file mode 100644 index 00000000..a76912ea --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/years/YearListTableRow.tsx @@ -0,0 +1,108 @@ +import type { readAllYearsRouteDefinition } from "@arrhes/application-metadata/routes" +import { Button, Chip, formatDate, LinkContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import type * as v from "valibot" +import { ListTable } from "../../../../components/layouts/listTable/listTable.tsx" +import { TabLink } from "../../../../components/layouts/tabBar/TabLink.js" + +export function YearListTableRow(props: { + year: v.InferOutput[number] +}) { + const startDate = formatDate(props.year.startingAt) + const endDate = formatDate(props.year.endingAt) + + return ( + +
+
+
+ + + +
+
+ {props.year.isClosed ? ( + + ) : ( + + )} +
+
+
+ + {startDate} - {endDate} + +
+
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/years/yearSelect.tsx b/packages/website/src/features/dashboard/$idOrganization/years/YearSelect.tsx similarity index 100% rename from packages/website/src/features/dashboard/$idOrganization/years/yearSelect.tsx rename to packages/website/src/features/dashboard/$idOrganization/years/YearSelect.tsx diff --git a/packages/website/src/features/dashboard/$idOrganization/years/YearsListTable.tsx b/packages/website/src/features/dashboard/$idOrganization/years/YearsListTable.tsx new file mode 100644 index 00000000..35557f30 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/years/YearsListTable.tsx @@ -0,0 +1,37 @@ +import { readAllYearsRouteDefinition } from "@arrhes/application-metadata/routes" +import type { schemas } from "@arrhes/application-metadata/schemas" +import { IconCalendarPlus } from "@tabler/icons-react" +import type * as v from "valibot" +import { DataWrapper } from "../../../../components/layouts/DataWrapper.tsx" +import { EmptyState } from "../../../../components/layouts/EmptyState.tsx" +import { ListTable } from "../../../../components/layouts/listTable/listTable.tsx" +import { YearListTableRow } from "./YearListTableRow.tsx" + +export function YearsListTable(_props: { idOrganization: v.InferOutput["id"] }) { + return ( + + + {(years) => { + if (years.length === 0) { + return ( + } + title="Aucun exercice" + subtitle="Créez un exercice pour commencer" + /> + ) + } + return years.map((year) => ( + + )) + }} + + + ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/years/YearsPage.tsx b/packages/website/src/features/dashboard/$idOrganization/years/YearsPage.tsx new file mode 100644 index 00000000..6fa73d38 --- /dev/null +++ b/packages/website/src/features/dashboard/$idOrganization/years/YearsPage.tsx @@ -0,0 +1,44 @@ +import { ButtonOutlineContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconCalendarPlus } from "@tabler/icons-react" +import { useParams } from "@tanstack/react-router" +import { Page } from "../../../../components/layouts/page/page.tsx" +import { CreateOneYear } from "./CreateOneYear.tsx" +import { YearsListTable } from "./YearsListTable.tsx" + +export function YearsPage({ idOrganization: idOrganizationProp }: { idOrganization?: string }) { + const params = useParams({ + strict: false, + }) as { + idOrganization?: string + } + const idOrganization = idOrganizationProp ?? params.idOrganization ?? "" + + return ( + + {/* + + Exercices fiscaux + + */} + +
+ + } + text="Ajouter un exercice" + /> + +
+ +
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idOrganization/years/createOneYear.tsx b/packages/website/src/features/dashboard/$idOrganization/years/createOneYear.tsx deleted file mode 100644 index 64dce4c2..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/years/createOneYear.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import { createOneYearRouteDefinition, readAllYearsRouteDefinition } from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { InputDate, InputText, toast } from "@arrhes/ui" -import { IconPlus } from "@tabler/icons-react" -import { type JSX, useState } from "react" -import { Fragment } from "react/jsx-runtime" -import type * as v from "valibot" -import { FormControl } from "../../../../components/forms/formControl.tsx" -import { FormError } from "../../../../components/forms/formError.tsx" -import { FormField } from "../../../../components/forms/formField.tsx" -import { FormItem } from "../../../../components/forms/formItem.tsx" -import { FormLabel } from "../../../../components/forms/formLabel.tsx" -import { FormRoot } from "../../../../components/forms/formRoot.tsx" -import { Drawer } from "../../../../components/overlays/drawer/drawer.tsx" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../utilities/invalidateData.ts" -import { YearSelect } from "./yearSelect.tsx" - -export function CreateOneYear(props: { - idOrganization: v.InferOutput["id"] - children: JSX.Element -}) { - const [open, setOpen] = useState(false) - const currentDate = new Date() - - return ( - - {props.children} - - - - , - text: "Ajouter l'exercice", - }} - onSubmit={async (data) => { - const response = await getResponseBodyFromAPI({ - routeDefinition: createOneYearRouteDefinition, - body: data, - }) - if (!response.ok) { - toast({ - title: "Impossible de créer l'exercice", - variant: "error", - }) - return false - } - - toast({ - title: "Exercice créé avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await invalidateData({ - routeDefinition: readAllYearsRouteDefinition, - body: {}, - }) - - setOpen(false) - }} - > - {(form) => ( - - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - - )} - - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/years/yearListTableRow.tsx b/packages/website/src/features/dashboard/$idOrganization/years/yearListTableRow.tsx deleted file mode 100644 index 5402bfd1..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/years/yearListTableRow.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import type { readAllYearsRouteDefinition } from "@arrhes/application-metadata/routes" -import { Chip, formatDate, LinkContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import type * as v from "valibot" -import { ListTable } from "../../../../components/layouts/listTable/listTable.tsx" -import { LinkButton } from "../../../../components/linkButton.tsx" - -export function YearListTableRow(props: { - year: v.InferOutput[number] -}) { - const startDate = formatDate(props.year.startingAt) - const endDate = formatDate(props.year.endingAt) - - return ( - -
-
-
- - - {props.year.label} - - -
-
- {props.year.isClosed ? ( - - ) : ( - - )} -
-
-
- - {startDate} - {endDate} - -
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/years/yearsListTable.tsx b/packages/website/src/features/dashboard/$idOrganization/years/yearsListTable.tsx deleted file mode 100644 index d64f12c1..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/years/yearsListTable.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { readAllYearsRouteDefinition } from "@arrhes/application-metadata/routes" -import type { schemas } from "@arrhes/application-metadata/schemas" -import { IconCalendarPlus } from "@tabler/icons-react" -import type * as v from "valibot" -import { DataWrapper } from "../../../../components/layouts/dataWrapper.tsx" -import { EmptyState } from "../../../../components/layouts/emptyState.tsx" -import { ListTable } from "../../../../components/layouts/listTable/listTable.tsx" -import { YearListTableRow } from "./yearListTableRow.tsx" - -export function YearsListTable(_props: { idOrganization: v.InferOutput["id"] }) { - return ( - - - {(years) => { - if (years.length === 0) { - return ( - } - title="Aucun exercice" - subtitle="Créez un exercice pour commencer" - /> - ) - } - return years.map((year) => ( - - )) - }} - - - ) -} diff --git a/packages/website/src/features/dashboard/$idOrganization/years/yearsPage.tsx b/packages/website/src/features/dashboard/$idOrganization/years/yearsPage.tsx deleted file mode 100644 index c3e331fe..00000000 --- a/packages/website/src/features/dashboard/$idOrganization/years/yearsPage.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { ButtonOutlineContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconCalendarPlus } from "@tabler/icons-react" -import { useParams } from "@tanstack/react-router" -import { Page } from "../../../../components/layouts/page/page.tsx" -import { yearsRoute } from "../../../../routes/root/dashboard/organizations/$idOrganization/years/yearsRoute.tsx" -import { CreateOneYear } from "./createOneYear.tsx" -import { YearsListTable } from "./yearsListTable.tsx" - -export function YearsPage() { - const params = useParams({ - from: yearsRoute.id, - }) - - return ( - - {/* - - Exercices fiscaux - - */} - -
- - } - text="Ajouter un exercice" - /> - -
- -
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/YearDataProvider.tsx b/packages/website/src/features/dashboard/$idYear/YearDataProvider.tsx new file mode 100644 index 00000000..1bcec1b7 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/YearDataProvider.tsx @@ -0,0 +1,147 @@ +import { + readAllAccountsRouteDefinition, + readAllBalanceSheetsRouteDefinition, + readAllComputationIncomeStatementsRouteDefinition, + readAllComputationsRouteDefinition, + readAllEntriesRouteDefinition, + readAllEntryLinesRouteDefinition, + readAllEntryTagsRouteDefinition, + readAllFilesRouteDefinition, + readAllFoldersRouteDefinition, + readAllIncomeStatementsRouteDefinition, + readAllJournalsRouteDefinition, + readAllTagsRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { type UseQueryResult, useQuery } from "@tanstack/react-query" +import { createContext, type ReactNode, useMemo } from "react" +import type * as v from "valibot" +import { ClientError } from "../../../utilities/clientError.ts" +import { getResponseBodyFromAPI } from "../../../utilities/getResponseBodyFromAPI.ts" + +const yearQueries = { + accounts: readAllAccountsRouteDefinition, + entries: readAllEntriesRouteDefinition, + entryLines: readAllEntryLinesRouteDefinition, + entryTags: readAllEntryTagsRouteDefinition, + journals: readAllJournalsRouteDefinition, + tags: readAllTagsRouteDefinition, + files: readAllFilesRouteDefinition, + folders: readAllFoldersRouteDefinition, + balanceSheets: readAllBalanceSheetsRouteDefinition, + incomeStatements: readAllIncomeStatementsRouteDefinition, + computations: readAllComputationsRouteDefinition, + computationIncomeStatements: readAllComputationIncomeStatementsRouteDefinition, +} as const + +type YearQueries = typeof yearQueries + +export type YearData = { + [K in keyof YearQueries]: v.InferOutput +} + +export type YearDataKey = keyof YearData + +type YearScopedRouteDefinition = { + method: "GET" | "POST" | "PATCH" | "DELETE" + path: string + schemas: { + body: v.ObjectSchema + return: + | v.ObjectSchema + | v.ArraySchema, undefined> + } +} + +export type YearDataContextValue = { + [K in YearDataKey]: UseQueryResult +} + +export const YearDataContext = createContext(null) + +function useYearQuery( + key: K, + body: { + idYear: string + }, +) { + const routeDefinition = yearQueries[key] as YearScopedRouteDefinition + + return useQuery({ + queryKey: [ + routeDefinition.path, + body, + ], + queryFn: async (context) => { + const response = await getResponseBodyFromAPI({ + routeDefinition, + body, + signal: context.signal, + }) + if (response.ok === false) { + throw new ClientError({ + message: "Error with the data fetching", + rawError: response.error, + }) + } + return response.data + }, + retry: 1, + }) as UseQueryResult +} + +export function YearDataProvider(props: { idYear: string; children: ReactNode }) { + const body = useMemo( + () => ({ + idYear: props.idYear, + }), + [ + props.idYear, + ], + ) + + const accounts = useYearQuery("accounts", body) + const entries = useYearQuery("entries", body) + const entryLines = useYearQuery("entryLines", body) + const entryTags = useYearQuery("entryTags", body) + const journals = useYearQuery("journals", body) + const tags = useYearQuery("tags", body) + const files = useYearQuery("files", body) + const folders = useYearQuery("folders", body) + const balanceSheets = useYearQuery("balanceSheets", body) + const incomeStatements = useYearQuery("incomeStatements", body) + const computations = useYearQuery("computations", body) + const computationIncomeStatements = useYearQuery("computationIncomeStatements", body) + + const value = useMemo( + () => ({ + accounts, + entries, + entryLines, + entryTags, + journals, + tags, + files, + folders, + balanceSheets, + incomeStatements, + computations, + computationIncomeStatements, + }), + [ + accounts, + entries, + entryLines, + entryTags, + journals, + tags, + files, + folders, + balanceSheets, + incomeStatements, + computations, + computationIncomeStatements, + ], + ) + + return {props.children} +} diff --git a/packages/website/src/features/dashboard/$idYear/YearDataWrapper.tsx b/packages/website/src/features/dashboard/$idYear/YearDataWrapper.tsx new file mode 100644 index 00000000..f5135643 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/YearDataWrapper.tsx @@ -0,0 +1,356 @@ +import { + readAllAccountsRouteDefinition, + readAllBalanceSheetsRouteDefinition, + readAllComputationIncomeStatementsRouteDefinition, + readAllComputationsRouteDefinition, + readAllEntriesRouteDefinition, + readAllEntryLinesRouteDefinition, + readAllEntryTagsRouteDefinition, + readAllFilesRouteDefinition, + readAllFoldersRouteDefinition, + readAllIncomeStatementsRouteDefinition, + readAllJournalsRouteDefinition, + readAllTagsRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { CircularLoader, FormatError } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { useQueries } from "@tanstack/react-query" +import type { ReactElement } from "react" +import { useMemo } from "react" +import type * as v from "valibot" +import { ClientError } from "../../../utilities/clientError.ts" +import { getResponseBodyFromAPI } from "../../../utilities/getResponseBodyFromAPI.ts" + +const yearQueries = { + accounts: readAllAccountsRouteDefinition, + entries: readAllEntriesRouteDefinition, + entryLines: readAllEntryLinesRouteDefinition, + entryTags: readAllEntryTagsRouteDefinition, + journals: readAllJournalsRouteDefinition, + tags: readAllTagsRouteDefinition, + files: readAllFilesRouteDefinition, + folders: readAllFoldersRouteDefinition, + balanceSheets: readAllBalanceSheetsRouteDefinition, + incomeStatements: readAllIncomeStatementsRouteDefinition, + computations: readAllComputationsRouteDefinition, + computationIncomeStatements: readAllComputationIncomeStatementsRouteDefinition, +} as const + +type YearQueries = typeof yearQueries + +type YearData = { + [K in keyof YearQueries]: v.InferOutput +} + +export type YearDataKey = keyof YearData + +// Pre-built index maps for O(1) lookups — computed once per data change, shared by all consumers. +export type YearDataMaps = { + entryById: Map + entryLinesByEntryId: Map> + entryTagsByEntryId: Map> + journalById: Map + tagById: Map + fileById: Map + folderById: Map + accountById: Map + accountByNumber: Map + balanceSheetById: Map + incomeStatementById: Map + computationById: Map +} + +type YearScopedRouteDefinition = { + method: "GET" | "POST" | "PATCH" | "DELETE" + path: string + schemas: { + body: v.ObjectSchema + return: + | v.ObjectSchema + | v.ArraySchema, undefined> + } +} + +const yearQueryEntries = Object.entries(yearQueries) as [ + YearDataKey, + YearScopedRouteDefinition, +][] + +// Stable index for dereferencing results by key name (avoids magic numbers). +const KEY_INDEX = Object.fromEntries( + yearQueryEntries.map(([key], i) => [ + key, + i, + ]), +) as Record + +export function YearDataWrapper(props: { + idYear: string + requiredKeys: K + children: (data: Pick & YearDataMaps) => ReactElement | null +}) { + const body = useMemo( + () => ({ + idYear: props.idYear, + }), + [ + props.idYear, + ], + ) + + const results = useQueries({ + queries: yearQueryEntries.map(([_key, routeDef]) => ({ + queryKey: [ + routeDef.path, + body, + ], + queryFn: async (context: { signal: AbortSignal }) => { + const response = await getResponseBodyFromAPI({ + routeDefinition: routeDef, + body, + signal: context.signal, + }) + if (response.ok === false) { + throw new ClientError({ + message: "Error with the data fetching", + rawError: response.error, + }) + } + return response.data + }, + retry: 1, + })), + }) + + const requiredIndices = useMemo( + () => props.requiredKeys.map((key) => KEY_INDEX[key]), + [ + props.requiredKeys, + ], + ) + + const isPending = requiredIndices.some((index) => results[index].isPending) + const isError = requiredIndices.some((index) => results[index].isError) + + // Extract each query's data to individual stable references so downstream useMemo deps + // only invalidate when the actual data changes, not on every YearDataWrapper render. + const accountsData = results[KEY_INDEX.accounts].data as YearData["accounts"] | undefined + const entriesData = results[KEY_INDEX.entries].data as YearData["entries"] | undefined + const entryLinesData = results[KEY_INDEX.entryLines].data as YearData["entryLines"] | undefined + const entryTagsData = results[KEY_INDEX.entryTags].data as YearData["entryTags"] | undefined + const journalsData = results[KEY_INDEX.journals].data as YearData["journals"] | undefined + const tagsData = results[KEY_INDEX.tags].data as YearData["tags"] | undefined + const filesData = results[KEY_INDEX.files].data as YearData["files"] | undefined + const foldersData = results[KEY_INDEX.folders].data as YearData["folders"] | undefined + const balanceSheetsData = results[KEY_INDEX.balanceSheets].data as YearData["balanceSheets"] | undefined + const incomeStatementsData = results[KEY_INDEX.incomeStatements].data as YearData["incomeStatements"] | undefined + const computationsData = results[KEY_INDEX.computations].data as YearData["computations"] | undefined + const computationIncomeStatementsData = results[KEY_INDEX.computationIncomeStatements].data as + | YearData["computationIncomeStatements"] + | undefined + + // Memoized raw-array object — reference stable as long as query data references don't change. + // eslint-disable-next-line react-hooks/exhaustive-deps + const arrays = useMemo( + () => ({ + accounts: accountsData ?? [], + entries: entriesData ?? [], + entryLines: entryLinesData ?? [], + entryTags: entryTagsData ?? [], + journals: journalsData ?? [], + tags: tagsData ?? [], + files: filesData ?? [], + folders: foldersData ?? [], + balanceSheets: balanceSheetsData ?? [], + incomeStatements: incomeStatementsData ?? [], + computations: computationsData ?? [], + computationIncomeStatements: computationIncomeStatementsData ?? [], + }), + // Each individual data reference from React Query cache — stable until data actually changes. + [ + accountsData, + entriesData, + entryLinesData, + entryTagsData, + journalsData, + tagsData, + filesData, + foldersData, + balanceSheetsData, + incomeStatementsData, + computationsData, + computationIncomeStatementsData, + ], + ) + + // ── Pre-built index maps ───────────────────────────────────────────────── + // Each map is memoized independently so only the affected map rebuilds when its source changes. + + const entryById = useMemo(() => { + const m = new Map() + for (const e of arrays.entries) m.set(e.id, e) + return m + }, [ + arrays.entries, + ]) + + const entryLinesByEntryId = useMemo(() => { + const m = new Map>() + for (const line of arrays.entryLines) { + const arr = m.get(line.idEntry) + if (arr) arr.push(line) + else + m.set(line.idEntry, [ + line, + ]) + } + return m + }, [ + arrays.entryLines, + ]) + + const entryTagsByEntryId = useMemo(() => { + const m = new Map>() + for (const et of arrays.entryTags) { + const arr = m.get(et.idEntry) + if (arr) arr.push(et) + else + m.set(et.idEntry, [ + et, + ]) + } + return m + }, [ + arrays.entryTags, + ]) + + const journalById = useMemo(() => { + const m = new Map() + for (const j of arrays.journals) m.set(j.id, j) + return m + }, [ + arrays.journals, + ]) + + const tagById = useMemo(() => { + const m = new Map() + for (const t of arrays.tags) m.set(t.id, t) + return m + }, [ + arrays.tags, + ]) + + const fileById = useMemo(() => { + const m = new Map() + for (const f of arrays.files) m.set(f.id, f) + return m + }, [ + arrays.files, + ]) + + const folderById = useMemo(() => { + const m = new Map() + for (const f of arrays.folders) m.set(f.id, f) + return m + }, [ + arrays.folders, + ]) + + const accountById = useMemo(() => { + const m = new Map() + for (const a of arrays.accounts) m.set(a.id, a) + return m + }, [ + arrays.accounts, + ]) + + const accountByNumber = useMemo(() => { + const m = new Map() + for (const a of arrays.accounts) m.set(a.number, a) + return m + }, [ + arrays.accounts, + ]) + + const balanceSheetById = useMemo(() => { + const m = new Map() + for (const bs of arrays.balanceSheets) m.set(bs.id, bs) + return m + }, [ + arrays.balanceSheets, + ]) + + const incomeStatementById = useMemo(() => { + const m = new Map() + for (const is of arrays.incomeStatements) m.set(is.id, is) + return m + }, [ + arrays.incomeStatements, + ]) + + const computationById = useMemo(() => { + const m = new Map() + for (const c of arrays.computations) m.set(c.id, c) + return m + }, [ + arrays.computations, + ]) + + const maps: YearDataMaps = useMemo( + () => ({ + entryById, + entryLinesByEntryId, + entryTagsByEntryId, + journalById, + tagById, + fileById, + folderById, + accountById, + accountByNumber, + balanceSheetById, + incomeStatementById, + computationById, + }), + [ + entryById, + entryLinesByEntryId, + entryTagsByEntryId, + journalById, + tagById, + fileById, + folderById, + accountById, + accountByNumber, + balanceSheetById, + incomeStatementById, + computationById, + ], + ) + + if (isPending) { + return ( + + ) + } + + if (isError) { + return ( + + ) + } + + return props.children({ + ...(arrays as unknown as Pick), + ...maps, + }) +} diff --git a/packages/website/src/features/dashboard/$idYear/YearLayout.tsx b/packages/website/src/features/dashboard/$idYear/YearLayout.tsx new file mode 100644 index 00000000..2cc1e9ba --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/YearLayout.tsx @@ -0,0 +1,5 @@ +import { Outlet } from "@tanstack/react-router" + +export function YearLayout() { + return +} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/$idEntryLine/DeleteOneEntryLine.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/$idEntryLine/DeleteOneEntryLine.tsx new file mode 100644 index 00000000..891d1dd6 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/$idEntryLine/DeleteOneEntryLine.tsx @@ -0,0 +1,107 @@ +import { + deleteOneEntryLineRouteDefinition, + readAllEntryLinesRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, ButtonOutlineContent, ButtonPlainContent, Dialog, toast, useModalStore } from "@arrhes/ui" +import { type ComponentPropsWithRef, type ReactElement, useId } from "react" +import type * as v from "valibot" +import { applicationRouter } from "../../../../../../routes/applicationRouter.tsx" +import { getResponseBodyFromAPI } from "../../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../../utilities/invalidateData.ts" + +export function DeleteOneEntryLine(props: { + entryLine: v.InferOutput + children: ReactElement> +}) { + const modalId = useId() + const { open: openModal, close: closeModal } = useModalStore() + + async function onSubmit() { + const deleteResponse = await getResponseBodyFromAPI({ + routeDefinition: deleteOneEntryLineRouteDefinition, + body: { + idEntryLine: props.entryLine.id, + idYear: props.entryLine.idYear, + }, + }) + + if (deleteResponse.ok === false) { + toast({ + title: "Erreur lors de la suppression du mouvement", + variant: "error", + }) + return + } + + await Promise.all([ + invalidateData({ + routeDefinition: readAllEntryLinesRouteDefinition, + body: { + idYear: props.entryLine.idYear, + idEntry: props.entryLine.idEntry, + }, + }), + invalidateData({ + routeDefinition: readAllEntryLinesRouteDefinition, + body: { + idYear: props.entryLine.idYear, + }, + }), + ]) + + toast({ + title: "Écriture supprimée", + variant: "success", + }) + + applicationRouter.navigate({ + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/écritures/$idEntry", + params: { + idOrganization: props.entryLine.idOrganization, + idYear: props.entryLine.idYear, + idEntry: props.entryLine.idEntry, + }, + }) + } + + return ( + + + + , + ) + } + > + {props.children} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/$idEntryLine/EntryLineLayout.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/$idEntryLine/EntryLineLayout.tsx new file mode 100644 index 00000000..a5ea07ee --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/$idEntryLine/EntryLineLayout.tsx @@ -0,0 +1,120 @@ +import { readOneEntryLineRouteDefinition } from "@arrhes/application-metadata/routes" +import { ButtonOutlineContent, ButtonPlainContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconChevronLeft, IconDatabase, IconInfoCircle, IconPencil, IconTrash } from "@tabler/icons-react" +import { Outlet, useParams } from "@tanstack/react-router" +import { LinkButton } from "../../../../../../components/LinkButton.tsx" +import { DataWrapper } from "../../../../../../components/layouts/DataWrapper.tsx" +import { Page } from "../../../../../../components/layouts/page/page.tsx" +import { Section } from "../../../../../../components/layouts/section/section.tsx" +import { Tab } from "../../../../../../components/layouts/tab/tab.tsx" + +import { DeleteOneEntryLine } from "./DeleteOneEntryLine.tsx" +import { UpdateOneEntryLine } from "./UpdateOneEntryLine.tsx" + +export function EntryLineLayout() { + const params = useParams({ + strict: false, + }) as { + idYear: string + idEntryLine: string + idOrganization: string + idEntry: string + } + + return ( + + + + {(entryLine) => { + return ( + + +
+ + } + text="Retour" + /> + +
+ + } + text="Modifier" + /> + + + } + color="danger" + /> + +
+
+
+ + , + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/écritures/$idEntry/mouvements/$idEntryLine", + params: { + idOrganization: params.idOrganization, + idYear: params.idYear, + idEntry: params.idEntry, + idEntryLine: params.idEntryLine, + }, + }, + { + label: "Métadonnées", + icon: , + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/écritures/$idEntry/mouvements/$idEntryLine/métadonnées", + params: { + idOrganization: params.idOrganization, + idYear: params.idYear, + idEntry: params.idEntry, + idEntryLine: params.idEntryLine, + }, + }, + ]} + /> + + +
+ ) + }} +
+
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/$idEntryLine/EntryLineMetadataTab.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/$idEntryLine/EntryLineMetadataTab.tsx new file mode 100644 index 00000000..58c71758 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/$idEntryLine/EntryLineMetadataTab.tsx @@ -0,0 +1,51 @@ +import { readOneEntryLineRouteDefinition } from "@arrhes/application-metadata/routes" +import { FormatDateTime, FormatText } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { useParams } from "@tanstack/react-router" +import { DataWrapper } from "../../../../../../components/layouts/DataWrapper.tsx" +import { DataBlock } from "../../../../../../components/layouts/dataBlock/dataBlock.tsx" +import { Section } from "../../../../../../components/layouts/section/section.tsx" + +export function EntryLineMetadataTab() { + const params = useParams({ + strict: false, + }) as { + idYear: string + idEntryLine: string + } + + return ( + + {(entryLine) => { + return ( + + + + + + + + + + + + {entryLine.id} + + + + + ) + }} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/$idEntryLine/EntryLinePage.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/$idEntryLine/EntryLinePage.tsx new file mode 100644 index 00000000..9fc59b20 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/$idEntryLine/EntryLinePage.tsx @@ -0,0 +1,83 @@ +import { readOneAccountRouteDefinition, readOneEntryLineRouteDefinition } from "@arrhes/application-metadata/routes" +import { FormatBoolean, FormatNull, FormatPrice, FormatText } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { useParams } from "@tanstack/react-router" +import { DataWrapper } from "../../../../../../components/layouts/DataWrapper.tsx" +import { DataBlock } from "../../../../../../components/layouts/dataBlock/dataBlock.tsx" +import { Section } from "../../../../../../components/layouts/section/section.tsx" + +export function EntryLinePage() { + const params = useParams({ + strict: false, + }) as { + idYear: string + idEntryLine: string + } + + return ( + + {(entryLine) => { + return ( + + + + + + {entryLine.label} + + + {entryLine.idAccount === null ? ( + + ) : ( + + {(account) => ( + {`${account.number} - ${account.label}`} + )} + + )} + + + + + + + + + + + + + + + + + + + + + + + + + + ) + }} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/$idEntryLine/UpdateOneEntryLine.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/$idEntryLine/UpdateOneEntryLine.tsx new file mode 100644 index 00000000..0db1b3f7 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/$idEntryLine/UpdateOneEntryLine.tsx @@ -0,0 +1,410 @@ +import { + readAllAccountsRouteDefinition, + readAllEntryLinesRouteDefinition, + readOneEntryLineRouteDefinition, + updateOneEntryLineRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, InputPrice, InputText, InputToggle, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPencil } from "@tabler/icons-react" +import type { JSX } from "react" +import { Fragment } from "react/jsx-runtime" +import type * as v from "valibot" +import { FormControl } from "../../../../../../components/forms/FormControl.tsx" +import { FormError } from "../../../../../../components/forms/FormError.tsx" +import { FormField } from "../../../../../../components/forms/FormField.tsx" +import { FormItem } from "../../../../../../components/forms/FormItem.tsx" +import { FormLabel } from "../../../../../../components/forms/FormLabel.tsx" +import { FormRoot } from "../../../../../../components/forms/FormRoot.tsx" +import { InputDataCombobox } from "../../../../../../components/InputDataCombobox.tsx" +import { useTabs } from "../../../../../../contexts/tabs/useTabs.tsx" +import { getResponseBodyFromAPI } from "../../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../../utilities/invalidateData.ts" + +export function UpdateOneEntryLine(props: { + entryLine: v.InferOutput + children: JSX.Element +}) { + const { openPanelTab, closeTab } = useTabs() + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/$idEntryLine/deleteOneEntryLine.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/$idEntryLine/deleteOneEntryLine.tsx deleted file mode 100644 index 542c972a..00000000 --- a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/$idEntryLine/deleteOneEntryLine.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { - deleteOneEntryLineRouteDefinition, - readAllEntryLinesRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { toast } from "@arrhes/ui" -import type { ComponentPropsWithRef, ReactElement } from "react" -import type * as v from "valibot" -import { ConfirmationModal } from "../../../../../../components/overlays/dialog/confirmationModal.tsx" -import { applicationRouter } from "../../../../../../routes/applicationRouter.tsx" -import { getResponseBodyFromAPI } from "../../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../../utilities/invalidateData.ts" - -export function DeleteOneEntryLine(props: { - entryLine: v.InferOutput - children: ReactElement> -}) { - async function onSubmit() { - const deleteResponse = await getResponseBodyFromAPI({ - routeDefinition: deleteOneEntryLineRouteDefinition, - body: { - idEntryLine: props.entryLine.id, - idYear: props.entryLine.idYear, - }, - }) - - if (deleteResponse.ok === false) { - toast({ - title: "Erreur lors de la suppression du mouvement", - variant: "error", - }) - return - } - - await Promise.all([ - invalidateData({ - routeDefinition: readAllEntryLinesRouteDefinition, - body: { - idYear: props.entryLine.idYear, - idEntry: props.entryLine.idEntry, - }, - }), - invalidateData({ - routeDefinition: readAllEntryLinesRouteDefinition, - body: { - idYear: props.entryLine.idYear, - }, - }), - ]) - - toast({ - title: "Écriture supprimée", - variant: "success", - }) - - applicationRouter.navigate({ - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/écritures/$idEntry", - params: { - idOrganization: props.entryLine.idOrganization, - idYear: props.entryLine.idYear, - idEntry: props.entryLine.idEntry, - }, - }) - } - - return ( - - Cette action supprimera le mouvement et toutes les données associées. -
- Cette action est irréversible. - - } - submitButtonProps={{ - color: "danger", - text: "Supprimer le mouvement", - }} - onSubmit={onSubmit} - > - {props.children} -
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/$idEntryLine/entryLineLayout.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/$idEntryLine/entryLineLayout.tsx deleted file mode 100644 index 969ce3f4..00000000 --- a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/$idEntryLine/entryLineLayout.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { readOneEntryLineRouteDefinition } from "@arrhes/application-metadata/routes" -import { ButtonOutlineContent, ButtonPlainContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconChevronLeft, IconDatabase, IconInfoCircle, IconPencil, IconTrash } from "@tabler/icons-react" -import { Outlet, useParams } from "@tanstack/react-router" -import { DataWrapper } from "../../../../../../components/layouts/dataWrapper.tsx" -import { Page } from "../../../../../../components/layouts/page/page.tsx" -import { Section } from "../../../../../../components/layouts/section/section.tsx" -import { Tab } from "../../../../../../components/layouts/tab/tab.tsx" -import { LinkButton } from "../../../../../../components/linkButton.tsx" -import { entryLineLayoutRoute } from "../../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/$idEntryLine/entryLineLayoutRoute.tsx" -import { DeleteOneEntryLine } from "./deleteOneEntryLine.tsx" -import { UpdateOneEntryLine } from "./updateOneEntryLine.tsx" - -export function EntryLineLayout() { - const params = useParams({ - from: entryLineLayoutRoute.id, - }) - - return ( - - - - {(entryLine) => { - return ( - - -
- - } - text="Retour" - /> - -
- - } - text="Modifier" - /> - - - } - color="danger" - /> - -
-
-
- - , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/écritures/$idEntry/mouvements/$idEntryLine", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - idEntry: params.idEntry, - idEntryLine: params.idEntryLine, - }, - }, - { - label: "Métadonnées", - icon: , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/écritures/$idEntry/mouvements/$idEntryLine/métadonnées", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - idEntry: params.idEntry, - idEntryLine: params.idEntryLine, - }, - }, - ]} - /> - - -
- ) - }} -
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/$idEntryLine/entryLineMetadataTab.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/$idEntryLine/entryLineMetadataTab.tsx deleted file mode 100644 index 0017be8a..00000000 --- a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/$idEntryLine/entryLineMetadataTab.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { readOneEntryLineRouteDefinition } from "@arrhes/application-metadata/routes" -import { FormatDateTime, FormatText } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { useParams } from "@tanstack/react-router" -import { DataBlock } from "../../../../../../components/layouts/dataBlock/dataBlock.tsx" -import { DataWrapper } from "../../../../../../components/layouts/dataWrapper.tsx" -import { Section } from "../../../../../../components/layouts/section/section.tsx" -import { entryLineLayoutRoute } from "../../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/$idEntryLine/entryLineLayoutRoute.tsx" - -export function EntryLineMetadataTab() { - const params = useParams({ - from: entryLineLayoutRoute.id, - }) - - return ( - - {(entryLine) => { - return ( - - - - - - - - - - - - {entryLine.id} - - - - - ) - }} - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/$idEntryLine/entryLinePage.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/$idEntryLine/entryLinePage.tsx deleted file mode 100644 index dd6334d8..00000000 --- a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/$idEntryLine/entryLinePage.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { readOneAccountRouteDefinition, readOneEntryLineRouteDefinition } from "@arrhes/application-metadata/routes" -import { FormatBoolean, FormatNull, FormatPrice, FormatText } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { useParams } from "@tanstack/react-router" -import { DataBlock } from "../../../../../../components/layouts/dataBlock/dataBlock.tsx" -import { DataWrapper } from "../../../../../../components/layouts/dataWrapper.tsx" -import { Section } from "../../../../../../components/layouts/section/section.tsx" -import { entryLineLayoutRoute } from "../../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/$idEntryLine/entryLineLayoutRoute.tsx" - -export function EntryLinePage() { - const params = useParams({ - from: entryLineLayoutRoute.id, - }) - - return ( - - {(entryLine) => { - return ( - - - - - - {entryLine.label} - - - {entryLine.idAccount === null ? ( - - ) : ( - - {(account) => ( - {`${account.number} - ${account.label}`} - )} - - )} - - - - - - - - - - - - - - - - - - - - - - - - - - ) - }} - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/$idEntryLine/updateOneEntryLine.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/$idEntryLine/updateOneEntryLine.tsx deleted file mode 100644 index 05ab7526..00000000 --- a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/$idEntryLine/updateOneEntryLine.tsx +++ /dev/null @@ -1,392 +0,0 @@ -import { - readAllAccountsRouteDefinition, - readAllEntryLinesRouteDefinition, - readOneEntryLineRouteDefinition, - updateOneEntryLineRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { InputPrice, InputText, InputToggle, toast } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconPencil } from "@tabler/icons-react" -import { type JSX, useState } from "react" -import { Fragment } from "react/jsx-runtime" -import type * as v from "valibot" -import { FormControl } from "../../../../../../components/forms/formControl.tsx" -import { FormError } from "../../../../../../components/forms/formError.tsx" -import { FormField } from "../../../../../../components/forms/formField.tsx" -import { FormItem } from "../../../../../../components/forms/formItem.tsx" -import { FormLabel } from "../../../../../../components/forms/formLabel.tsx" -import { FormRoot } from "../../../../../../components/forms/formRoot.tsx" -import { InputDataCombobox } from "../../../../../../components/inputDataCombobox.tsx" -import { Drawer } from "../../../../../../components/overlays/drawer/drawer.tsx" -import { getResponseBodyFromAPI } from "../../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../../utilities/invalidateData.ts" - -export function UpdateOneEntryLine(props: { - entryLine: v.InferOutput - children: JSX.Element -}) { - const [open, setOpen] = useState(false) - - return ( - - {props.children} - - - - , - text: "Modifier le mouvement", - }} - onSubmit={async (data) => { - const updateEntryLineResponse = await getResponseBodyFromAPI({ - routeDefinition: updateOneEntryLineRouteDefinition, - body: data, - }) - if (updateEntryLineResponse.ok === false) { - toast({ - title: "Impossible de modifier le mouvement", - variant: "error", - }) - return false - } - - toast({ - title: "Mouvement modifié avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await Promise.all([ - invalidateData({ - routeDefinition: readAllEntryLinesRouteDefinition, - body: { - idYear: props.entryLine.idYear, - idEntry: props.entryLine.idEntry, - }, - }), - invalidateData({ - routeDefinition: readAllEntryLinesRouteDefinition, - body: { - idYear: props.entryLine.idYear, - }, - }), - invalidateData({ - routeDefinition: readOneEntryLineRouteDefinition, - body: { - idYear: props.entryLine.idYear, - idEntryLine: props.entryLine.id, - }, - }), - ]) - - setOpen(false) - }} - > - {(form) => ( - - ( - - - - - - - - )} - /> - ( - - - - ({ - key: journal.id, - label: `${journal.number} - ${journal.label}`, - })} - /> - - - - )} - /> -
- ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> -
-
- -
- ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> -
-
-
- )} -
-
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/CreateOneEntryLine.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/CreateOneEntryLine.tsx new file mode 100644 index 00000000..e0c29993 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/CreateOneEntryLine.tsx @@ -0,0 +1,415 @@ +import { + createOneEntryLineRouteDefinition, + readAllAccountsRouteDefinition, + readAllEntryLinesRouteDefinition, + readOneEntryRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, InputPrice, InputText, InputToggle, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPlus } from "@tabler/icons-react" +import type { JSX } from "react" +import { Fragment } from "react/jsx-runtime" +import type * as v from "valibot" +import { FormControl } from "../../../../../components/forms/FormControl.tsx" +import { FormError } from "../../../../../components/forms/FormError.tsx" +import { FormField } from "../../../../../components/forms/FormField.tsx" +import { FormItem } from "../../../../../components/forms/FormItem.tsx" +import { FormLabel } from "../../../../../components/forms/FormLabel.tsx" +import { FormRoot } from "../../../../../components/forms/FormRoot.tsx" +import { InputDataCombobox } from "../../../../../components/InputDataCombobox.tsx" +import { useTabs } from "../../../../../contexts/tabs/useTabs.tsx" +import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../utilities/invalidateData.ts" + +export function CreateOneEntryLine(props: { + entry: v.InferOutput + children: JSX.Element +}) { + const { openPanelTab, closeTab } = useTabs() + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/DeleteOneEntry.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/DeleteOneEntry.tsx new file mode 100644 index 00000000..7309df7d --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/DeleteOneEntry.tsx @@ -0,0 +1,94 @@ +import { deleteOneEntryRouteDefinition, readAllEntriesRouteDefinition } from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, ButtonOutlineContent, ButtonPlainContent, Dialog, toast, useModalStore } from "@arrhes/ui" +import { type ComponentPropsWithRef, type ReactElement, useId } from "react" +import type * as v from "valibot" +import { applicationRouter } from "../../../../../routes/applicationRouter.tsx" +import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../utilities/invalidateData.ts" + +export function DeleteOneEntry(props: { + entry: v.InferOutput + children: ReactElement> +}) { + const modalId = useId() + const { open: openModal, close: closeModal } = useModalStore() + + async function onSubmit() { + const deleteResponse = await getResponseBodyFromAPI({ + routeDefinition: deleteOneEntryRouteDefinition, + body: { + idEntry: props.entry.id, + idYear: props.entry.idYear, + }, + }) + + if (deleteResponse.ok === false) { + toast({ + title: "Erreur lors de la suppression de l'écriture", + variant: "error", + }) + return + } + + await invalidateData({ + routeDefinition: readAllEntriesRouteDefinition, + body: { + idYear: props.entry.idYear, + }, + }) + + toast({ + title: "Écriture supprimée", + variant: "success", + }) + + applicationRouter.navigate({ + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/écritures", + params: { + idOrganization: props.entry.idOrganization, + idYear: props.entry.idYear, + }, + }) + } + + return ( + + + + , + ) + } + > + {props.children} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/DuplicateOneEntry.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/DuplicateOneEntry.tsx new file mode 100644 index 00000000..c72b7709 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/DuplicateOneEntry.tsx @@ -0,0 +1,116 @@ +import { + duplicateOneEntryRouteDefinition, + readAllEntriesRouteDefinition, + readAllEntryTagsRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, ButtonGhostContent, ButtonPlainContent, Dialog, toast, useModalStore } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconCopyCheck } from "@tabler/icons-react" +import { type ComponentPropsWithRef, type ReactElement, useId } from "react" +import type * as v from "valibot" +import { applicationRouter } from "../../../../../routes/applicationRouter.tsx" +import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../utilities/invalidateData.ts" + +export function DuplicateOneEntry(props: { + entry: v.InferOutput + children: ReactElement> +}) { + const modalId = useId() + const { open: openModal, close: closeModal } = useModalStore() + + async function onSubmit() { + const duplicateResponse = await getResponseBodyFromAPI({ + routeDefinition: duplicateOneEntryRouteDefinition, + body: { + idEntry: props.entry.id, + idYear: props.entry.idYear, + }, + }) + + if (duplicateResponse.ok === false) { + toast({ + title: "Erreur lors de la duplication de l'écriture", + variant: "error", + }) + return + } + + await Promise.all([ + invalidateData({ + routeDefinition: readAllEntriesRouteDefinition, + body: { + idYear: props.entry.idYear, + }, + }), + invalidateData({ + routeDefinition: readAllEntryTagsRouteDefinition, + body: { + idYear: props.entry.idYear, + }, + }), + ]) + + toast({ + title: "Écriture dupliquée", + variant: "success", + }) + + applicationRouter.navigate({ + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/écritures/$idEntry", + params: { + idOrganization: props.entry.idOrganization, + idYear: props.entry.idYear, + idEntry: duplicateResponse.data.id, + }, + }) + + closeModal(modalId) + } + + return ( + + + + , + ) + } + > + {props.children} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/EntryCategoriesTab.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/EntryCategoriesTab.tsx new file mode 100644 index 00000000..de89c93c --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/EntryCategoriesTab.tsx @@ -0,0 +1,224 @@ +import { + addOneEntryTagRouteDefinition, + readAllEntryTagsRouteDefinition, + removeOneEntryTagRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, ButtonGhostContent, FormatNull, FormatText, InputCombobox, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconLinkOff } from "@tabler/icons-react" +import { useParams } from "@tanstack/react-router" +import { useState } from "react" +import type * as v from "valibot" +import { DataBlock } from "../../../../../components/layouts/dataBlock/dataBlock.tsx" +import { Section } from "../../../../../components/layouts/section/section.tsx" +import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.js" +import { invalidateData } from "../../../../../utilities/invalidateData.js" +import type { YearDataKey } from "../../YearDataWrapper.tsx" +import { YearDataWrapper } from "../../YearDataWrapper.tsx" + +const requiredKeys = [ + "entries", + "entryTags", + "tags", +] as const satisfies readonly YearDataKey[] + +export function EntryCategoriesTab(props: { idYear?: string; idEntry?: string } = {}) { + const params = useParams({ + strict: false, + }) as { + idYear?: string + idEntry?: string + } + const idYear = props.idYear ?? params.idYear ?? "" + const idEntry = props.idEntry ?? params.idEntry ?? "" + + return ( + + {({ entries, entryTags, tags }) => { + const entry = entries.find((r) => r.id === idEntry) + if (entry === undefined) return null + + return ( + + ) + }} + + ) +} + +function EntryCategoriesTabContent(props: { + entry: v.InferOutput + entryTags: v.InferOutput[] + tags: v.InferOutput[] + idYear: string +}) { + const [isAdding, setIsAdding] = useState(false) + const [isRemoving, setIsRemoving] = useState(null) + + const currentEntryTags = props.entryTags.filter((et) => et.idEntry === props.entry.id) + const currentTagIds = new Set(currentEntryTags.map((et) => et.idTag)) + const resolvedTags = currentEntryTags + .map((et) => { + const tag = props.tags.find((t) => t.id === et.idTag) + return tag + ? { + entryTagId: et.id, + tagId: tag.id, + label: tag.label, + } + : null + }) + .filter((t): t is NonNullable => t !== null) + + const availableTags = props.tags + .filter((t) => !currentTagIds.has(t.id)) + .map((t) => ({ + key: t.id, + label: t.label, + })) + + const handleAddTag = async (idTag: string | null | undefined) => { + if (!idTag) return + if (currentTagIds.has(idTag)) return + setIsAdding(true) + const response = await getResponseBodyFromAPI({ + routeDefinition: addOneEntryTagRouteDefinition, + body: { + idYear: props.idYear, + idEntry: props.entry.id, + idTag, + }, + }) + if (response.ok === false) { + toast({ + title: "Impossible d'ajouter la catégorie", + variant: "error", + }) + } else { + toast({ + title: "Catégorie ajoutée", + variant: "success", + }) + await invalidateData({ + routeDefinition: readAllEntryTagsRouteDefinition, + body: { + idYear: props.idYear, + }, + }) + } + setIsAdding(false) + } + + const handleRemoveTag = async (entryTagId: string) => { + setIsRemoving(entryTagId) + const response = await getResponseBodyFromAPI({ + routeDefinition: removeOneEntryTagRouteDefinition, + body: { + idYear: props.idYear, + idEntryTag: entryTagId, + }, + }) + if (response.ok === false) { + toast({ + title: "Impossible de retirer la catégorie", + variant: "error", + }) + } else { + toast({ + title: "Catégorie retirée", + variant: "success", + }) + await invalidateData({ + routeDefinition: readAllEntryTagsRouteDefinition, + body: { + idYear: props.idYear, + }, + }) + } + setIsRemoving(null) + } + + return ( + +
+ +
+ + + + {resolvedTags.length === 0 ? ( +
+ +
+ ) : ( + resolvedTags.map((tag) => ( +
+ {tag.label} + +
+ )) + )} +
+
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/EntryInformationsTab.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/EntryInformationsTab.tsx new file mode 100644 index 00000000..b8e0b4f3 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/EntryInformationsTab.tsx @@ -0,0 +1,95 @@ +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, ButtonPlainContent, FormatDate, FormatNull, FormatPrice, FormatText, LinkContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPencil } from "@tabler/icons-react" +import type * as v from "valibot" +import { DataBlock } from "../../../../../components/layouts/dataBlock/dataBlock.tsx" +import { Section } from "../../../../../components/layouts/section/section.tsx" +import { useTabs } from "../../../../../contexts/tabs/useTabs.tsx" +import { UpdateOneEntry } from "./UpdateOneEntry.tsx" + +export function EntryInformationsTab(props: { + entry: v.InferOutput + journal: v.InferOutput | null + entryTagLabels: string[] + file: v.InferOutput | null + totalDebit: number + totalCredit: number +}) { + const { openTab } = useTabs() + + return ( + +
+ + } + text="Modifier" + /> + +
+ + + + + {props.entry.label} + + + + + + {props.entry.idJournal === null ? ( + + ) : props.journal !== null ? ( + {`(${props.journal.code}) ${props.journal.label}`} + ) : ( + + )} + + + {props.entry.idFile === null || props.file === null ? ( + + ) : ( + + )} + + + + + + + + + + + + + + +
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/EntryLayout.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/EntryLayout.tsx new file mode 100644 index 00000000..38a65b20 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/EntryLayout.tsx @@ -0,0 +1,238 @@ +import { Button, ButtonGhostContent, ButtonOutlineContent, FormatError, formatPrice } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { + IconArrowBackUp, + IconChevronLeft, + IconCopyCheck, + IconDatabase, + IconDotsVertical, + IconInfoCircle, + IconList, + IconTag, + IconTrash, +} from "@tabler/icons-react" +import { Outlet, useParams } from "@tanstack/react-router" +import { useState } from "react" +import { LinkButton } from "../../../../../components/LinkButton.js" +import { Banner } from "../../../../../components/layouts/Banner.tsx" +import { Page } from "../../../../../components/layouts/page/page.tsx" +import { Tab } from "../../../../../components/layouts/tab/tab.tsx" +import { Popover } from "../../../../../components/overlays/popover/popover.js" + +import { compareAmounts } from "../../../../../utilities/compareAmounts.ts" +import type { YearDataKey } from "../../YearDataWrapper.tsx" +import { YearDataWrapper } from "../../YearDataWrapper.tsx" +import { ReverseOneEntry } from "../ReverseOneEntry.tsx" +import { DeleteOneEntry } from "./DeleteOneEntry.tsx" +import { DuplicateOneEntry } from "./DuplicateOneEntry.tsx" + +const requiredKeys = [ + "entries", + "entryLines", + "entryTags", + "accounts", + "journals", + "tags", + "files", +] as const satisfies readonly YearDataKey[] + +export function EntryLayout() { + const params = useParams({ + strict: false, + }) as { + idOrganization: string + idYear: string + idEntry: string + } + const [menuOpen, setMenuOpen] = useState(false) + + return ( + + {({ entries, entryLines: allEntryLines }) => { + const entry = entries.find((r) => r.id === params.idEntry) + + if (entry === undefined) { + return ( + + ) + } + + const entryLines = allEntryLines.filter((row) => row.idEntry === params.idEntry) + + let totalDebit = 0 + let totalCredit = 0 + + for (const entryLine of entryLines) { + totalDebit += Number(entryLine.debit) + totalCredit += Number(entryLine.credit) + } + + return ( + + +
+ + } + text="Retour" + /> + +
+ + + + + + +
setMenuOpen(false)} + > + } + text="Extourner" + className={css({ + width: "100%", + justifyContent: "start", + })} + /> +
+
+ +
setMenuOpen(false)} + > + } + text="Dupliquer" + className={css({ + width: "100%", + justifyContent: "start", + })} + /> +
+
+
+
+ + } + title="Supprimer" + color="danger" + /> + +
+
+ {entry.idFile !== null ? null : ( + Il manque une pièce justificative. + )} + {compareAmounts({ + a: totalDebit, + b: totalCredit, + }) ? null : ( + + Les montants au débit et au crédit sont différents, veuillez corriger pour pouvoir + valider. ( + {formatPrice({ + price: totalDebit - totalCredit, + })} + ) + + )} + , + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/écritures/$idEntry", + params: { + idOrganization: params.idOrganization, + idYear: params.idYear, + idEntry: params.idEntry, + }, + }, + { + label: "Mouvements", + icon: , + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/écritures/$idEntry/mouvements", + params: { + idOrganization: params.idOrganization, + idYear: params.idYear, + idEntry: params.idEntry, + }, + }, + { + label: "Catégories", + icon: , + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/écritures/$idEntry/catégories", + params: { + idOrganization: params.idOrganization, + idYear: params.idYear, + idEntry: params.idEntry, + }, + }, + { + label: "Métadonnées", + icon: , + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/écritures/$idEntry/métadonnées", + params: { + idOrganization: params.idOrganization, + idYear: params.idYear, + idEntry: params.idEntry, + }, + }, + ]} + /> + +
+
+ ) + }} +
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/EntryLinesTab.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/EntryLinesTab.tsx new file mode 100644 index 00000000..b1a78f8c --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/EntryLinesTab.tsx @@ -0,0 +1,89 @@ +import type { readAllAccountsRouteDefinition } from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { ButtonOutlineContent, ButtonPlainContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconEdit, IconPlus } from "@tabler/icons-react" +import type * as v from "valibot" +import { Section } from "../../../../../components/layouts/section/section.tsx" +import type { YearDataKey } from "../../YearDataWrapper.tsx" +import { YearDataWrapper } from "../../YearDataWrapper.tsx" +import { CreateOneEntryLine } from "./CreateOneEntryLine.tsx" +import { EntryLinesTable } from "./EntryLinesTable.tsx" +import { UpdateManyEntryLines } from "./UpdateManyEntryLines.tsx" + +const requiredKeys = [ + "entries", + "entryLines", + "accounts", +] as const satisfies readonly YearDataKey[] + +export function EntryLinesTab(props: { idYear: string; idEntry: string }) { + const idYear = props.idYear + const idEntry = props.idEntry + + return ( + + {({ entries, entryLines: allEntryLines, accounts }) => { + const entry = entries.find((r) => r.id === idEntry) + if (entry === undefined) return null + + const entryLines = allEntryLines.filter((row) => row.idEntry === idEntry) + const accountsMap = new Map( + accounts.map((account) => [ + account.id, + account, + ]), + ) + + return ( + + ) + }} + + ) +} + +function EntryLinesTabContent(props: { + entry: v.InferOutput + entryLines: v.InferOutput[] + accounts: Map[number]> +}) { + return ( + +
+ + } + text="Ajouter un mouvement" + /> + + + } + text="Modifier plusieurs mouvements" + /> + +
+ +
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/EntryLinesTable.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/EntryLinesTable.tsx new file mode 100644 index 00000000..0acc68ce --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/EntryLinesTable.tsx @@ -0,0 +1,120 @@ +import type { readAllAccountsRouteDefinition } from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { ButtonGhostContent, FormatDateTime, FormatNull, FormatPrice, FormatText } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconEye, IconPencil } from "@tabler/icons-react" +import type * as v from "valibot" +import { LinkButton } from "../../../../../components/LinkButton.tsx" +import { DataTable } from "../../../../../components/layouts/DataTable.tsx" +import { UpdateOneEntryLine } from "./$idEntryLine/UpdateOneEntryLine.tsx" + +export function EntryLinesTable(props: { + entry: v.InferOutput + entryLines: Array> + accounts: Map[number]> + isLoading?: boolean +}) { + return ( + ( +
+ + } + text={undefined} + /> + + + } + text={undefined} + /> + +
+ ), + enableSorting: false, + enableGlobalFilter: false, + }, + { + accessorKey: "label", + header: "Libellé", + cell: ({ row }) => {row.original.label}, + filterFn: "includesString", + }, + { + accessorKey: "idAccount", + header: "Compte", + cell: ({ row }) => { + const account = props.accounts.get(row.original.idAccount) + if (!account) return + return ( +
+ + {account.number} + + + {account.label} + +
+ ) + }, + filterFn: "includesString", + }, + { + accessorKey: "debit", + header: "Débit", + cell: ({ row }) => , + filterFn: "includesString", + }, + { + accessorKey: "credit", + header: "Crédit", + cell: ({ row }) => , + filterFn: "includesString", + }, + { + accessorKey: "createdAt", + header: "Ajouté le", + cell: ({ row }) => , + filterFn: "includesString", + }, + ]} + /> + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/EntryMetadataTab.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/EntryMetadataTab.tsx new file mode 100644 index 00000000..c7b0b7bf --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/EntryMetadataTab.tsx @@ -0,0 +1,57 @@ +import { FormatDateTime, FormatText } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { useParams } from "@tanstack/react-router" +import { DataBlock } from "../../../../../components/layouts/dataBlock/dataBlock.tsx" +import { Section } from "../../../../../components/layouts/section/section.tsx" +import type { YearDataKey } from "../../YearDataWrapper.tsx" +import { YearDataWrapper } from "../../YearDataWrapper.tsx" + +const requiredKeys = [ + "entries", +] as const satisfies readonly YearDataKey[] + +export function EntryMetadataTab(props: { idYear?: string; idEntry?: string } = {}) { + const params = useParams({ + strict: false, + }) as { + idYear?: string + idEntry?: string + } + const idYear = props.idYear ?? params.idYear ?? "" + const idEntry = props.idEntry ?? params.idEntry ?? "" + + return ( + + {({ entries }) => { + const entry = entries.find((r) => r.id === idEntry) + if (entry === undefined) return null + + return ( + + + + + + + + + + + + {entry.id} + + + + + ) + }} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/EntryRoutePage.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/EntryRoutePage.tsx new file mode 100644 index 00000000..941293ff --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/EntryRoutePage.tsx @@ -0,0 +1,65 @@ +import { useParams } from "@tanstack/react-router" +import type { YearDataKey } from "../../YearDataWrapper.js" +import { YearDataWrapper } from "../../YearDataWrapper.js" +import { EntryInformationsTab } from "./EntryInformationsTab.js" + +const routeRequiredKeys = [ + "entries", + "entryLines", + "entryTags", + "journals", + "tags", + "files", +] as const satisfies readonly YearDataKey[] + +// Used by TanStack Router as a standalone route component — resolves data itself via YearDataWrapper. +export function EntryRoutePage() { + const params = useParams({ + strict: false, + }) as { + idYear?: string + idEntry?: string + } + const idYear = params.idYear ?? "" + const idEntry = params.idEntry ?? "" + + return ( + + {({ entries, entryLines: allEntryLines, entryTags, journals, tags, files }) => { + const entry = entries.find((r) => r.id === idEntry) + if (entry === undefined) return null + + const entryLines = allEntryLines.filter((row) => row.idEntry === idEntry) + const journal = + entry.idJournal !== null ? (journals.find((j) => j.id === entry.idJournal) ?? null) : null + const entryTagIds = entryTags.filter((et) => et.idEntry === entry.id).map((et) => et.idTag) + const entryTagLabels = entryTagIds + .map((id) => tags.find((t) => t.id === id)) + .filter((t): t is NonNullable => Boolean(t)) + .map((t) => t.label) + const file = entry.idFile !== null ? (files.find((f) => f.id === entry.idFile) ?? null) : null + + let totalDebit = 0 + let totalCredit = 0 + for (const entryLine of entryLines) { + totalDebit += Number(entryLine.debit) + totalCredit += Number(entryLine.credit) + } + + return ( + + ) + }} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/EntryTabContent.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/EntryTabContent.tsx new file mode 100644 index 00000000..f320572f --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/EntryTabContent.tsx @@ -0,0 +1,231 @@ +import { Button, ButtonGhostContent, ButtonOutlineContent, FormatError, formatPrice } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { + IconArrowBackUp, + IconCopyCheck, + IconDatabase, + IconDotsVertical, + IconInfoCircle, + IconList, + IconTag, + IconTrash, +} from "@tabler/icons-react" +import { Suspense, useState } from "react" +import { Banner } from "../../../../../components/layouts/Banner.tsx" +import { Page } from "../../../../../components/layouts/page/page.tsx" +import { SubPageContent } from "../../../../../components/layouts/SubPageContent.tsx" +import { Popover } from "../../../../../components/overlays/popover/popover.js" +import { compareAmounts } from "../../../../../utilities/compareAmounts.ts" +import type { YearDataKey } from "../../YearDataWrapper.tsx" +import { YearDataWrapper } from "../../YearDataWrapper.tsx" +import { ReverseOneEntry } from "../ReverseOneEntry.tsx" +import { DeleteOneEntry } from "./DeleteOneEntry.tsx" +import { DuplicateOneEntry } from "./DuplicateOneEntry.tsx" +import { EntryCategoriesTab } from "./EntryCategoriesTab.tsx" +import { EntryInformationsTab } from "./EntryInformationsTab.tsx" +import { EntryLinesTab } from "./EntryLinesTab.tsx" +import { EntryMetadataTab } from "./EntryMetadataTab.tsx" + +const requiredKeys = [ + "entries", + "entryLines", + "entryTags", + "journals", + "tags", + "files", +] as const satisfies readonly YearDataKey[] + +export function EntryTabContent(props: { idOrganization: string; idYear: string; idEntry: string }) { + const [menuOpen, setMenuOpen] = useState(false) + + return ( + + {({ entries, entryLinesByEntryId, entryTagsByEntryId, journalById, tagById, fileById }) => { + const entry = entries.find((r) => r.id === props.idEntry) + + if (entry === undefined) { + return ( + + ) + } + + const entryLines = entryLinesByEntryId.get(props.idEntry) ?? [] + + let totalDebit = 0 + let totalCredit = 0 + + for (const entryLine of entryLines) { + totalDebit += Number(entryLine.debit) + totalCredit += Number(entryLine.credit) + } + + const journal = entry.idJournal !== null ? (journalById.get(entry.idJournal) ?? null) : null + const entryTagLabels = (entryTagsByEntryId.get(entry.id) ?? []) + .map((et) => tagById.get(et.idTag)) + .filter((t): t is NonNullable => t !== undefined) + .map((t) => t.label) + const file = entry.idFile !== null ? (fileById.get(entry.idFile) ?? null) : null + + return ( + + + {entry.idFile !== null ? null : ( + Il manque une pièce justificative. + )} + {compareAmounts({ + a: totalDebit, + b: totalCredit, + }) ? null : ( + + Les montants au débit et au crédit sont différents, veuillez corriger pour pouvoir + valider. ( + {formatPrice({ + price: totalDebit - totalCredit, + })} + ) + + )} + + + , + content: ( + + + + ), + }, + { + key: "mouvements", + label: "Mouvements", + icon: , + content: ( + + + + ), + }, + { + key: "catégories", + label: "Catégories", + icon: , + content: ( + + + + ), + }, + { + key: "métadonnées", + label: "Métadonnées", + icon: , + content: ( + + + + ), + }, + ], + }, + }} + > + + + + + + +
setMenuOpen(false)} + > + } + text="Extourner" + className={css({ + width: "100%", + justifyContent: "start", + })} + /> +
+
+ +
setMenuOpen(false)} + > + } + text="Dupliquer" + className={css({ + width: "100%", + justifyContent: "start", + })} + /> +
+
+
+
+ +
+ } + title="Supprimer" + color="danger" + /> +
+
+
+
+
+ ) + }} +
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/UpdateManyEntryLines.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/UpdateManyEntryLines.tsx new file mode 100644 index 00000000..30ae3829 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/UpdateManyEntryLines.tsx @@ -0,0 +1,325 @@ +import { + readAllEntryLinesRouteDefinition, + updateManyEntryLinesRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, InputText, InputToggle, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPencil } from "@tabler/icons-react" +import type { JSX } from "react" +import { Fragment } from "react/jsx-runtime" +import type * as v from "valibot" +import { FormControl } from "../../../../../components/forms/FormControl.tsx" +import { FormError } from "../../../../../components/forms/FormError.tsx" +import { FormField } from "../../../../../components/forms/FormField.tsx" +import { FormItem } from "../../../../../components/forms/FormItem.tsx" +import { FormLabel } from "../../../../../components/forms/FormLabel.tsx" +import { FormRoot } from "../../../../../components/forms/FormRoot.tsx" +import { useTabs } from "../../../../../contexts/tabs/useTabs.tsx" +import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../utilities/invalidateData.ts" + +export function UpdateManyEntryLines(props: { + entry: v.InferOutput + children: JSX.Element +}) { + const { openPanelTab, closeTab } = useTabs() + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/UpdateOneEntry.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/UpdateOneEntry.tsx new file mode 100644 index 00000000..d1847114 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/UpdateOneEntry.tsx @@ -0,0 +1,209 @@ +import { + readAllEntriesRouteDefinition, + readAllFilesRouteDefinition, + readAllJournalsRouteDefinition, + readOneEntryRouteDefinition, + updateOneEntryRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, InputDate, InputText, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPencil } from "@tabler/icons-react" +import type { JSX } from "react" +import { Fragment } from "react/jsx-runtime" +import type * as v from "valibot" +import { FormControl } from "../../../../../components/forms/FormControl.tsx" +import { FormError } from "../../../../../components/forms/FormError.tsx" +import { FormField } from "../../../../../components/forms/FormField.tsx" +import { FormItem } from "../../../../../components/forms/FormItem.tsx" +import { FormLabel } from "../../../../../components/forms/FormLabel.tsx" +import { FormRoot } from "../../../../../components/forms/FormRoot.tsx" +import { InputDataCombobox } from "../../../../../components/InputDataCombobox.tsx" +import { useTabs } from "../../../../../contexts/tabs/useTabs.tsx" +import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../utilities/invalidateData.ts" + +export function UpdateOneEntry(props: { entry: v.InferOutput; children: JSX.Element }) { + const { openPanelTab, closeTab } = useTabs() + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/createOneEntryLine.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/createOneEntryLine.tsx deleted file mode 100644 index f666a077..00000000 --- a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/createOneEntryLine.tsx +++ /dev/null @@ -1,397 +0,0 @@ -import { - createOneEntryLineRouteDefinition, - readAllAccountsRouteDefinition, - readAllEntryLinesRouteDefinition, - readOneEntryRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { InputPrice, InputText, InputToggle, toast } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconPlus } from "@tabler/icons-react" -import { type JSX, useState } from "react" -import { Fragment } from "react/jsx-runtime" -import type * as v from "valibot" -import { FormControl } from "../../../../../components/forms/formControl.tsx" -import { FormError } from "../../../../../components/forms/formError.tsx" -import { FormField } from "../../../../../components/forms/formField.tsx" -import { FormItem } from "../../../../../components/forms/formItem.tsx" -import { FormLabel } from "../../../../../components/forms/formLabel.tsx" -import { FormRoot } from "../../../../../components/forms/formRoot.tsx" -import { InputDataCombobox } from "../../../../../components/inputDataCombobox.tsx" -import { Drawer } from "../../../../../components/overlays/drawer/drawer.tsx" -import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../utilities/invalidateData.ts" - -export function CreateOneEntryLine(props: { - entry: v.InferOutput - children: JSX.Element -}) { - const [open, setOpen] = useState(false) - - return ( - - {props.children} - - - - , - text: "Ajouter le mouvement", - }} - onSubmit={async (data) => { - const createEntryLineResponse = await getResponseBodyFromAPI({ - routeDefinition: createOneEntryLineRouteDefinition, - body: data, - }) - if (createEntryLineResponse.ok === false) { - toast({ - title: "Impossible d'ajouter le mouvement", - variant: "error", - }) - return false - } - - toast({ - title: "Mouvement ajouté avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await Promise.all([ - invalidateData({ - routeDefinition: readOneEntryRouteDefinition, - body: { - idYear: props.entry.idYear, - idEntry: props.entry.id, - }, - }), - invalidateData({ - routeDefinition: readAllEntryLinesRouteDefinition, - body: { - idYear: props.entry.idYear, - idEntry: props.entry.id, - }, - }), - invalidateData({ - routeDefinition: readAllEntryLinesRouteDefinition, - body: { - idYear: props.entry.idYear, - }, - }), - ]) - - setOpen(false) - }} - > - {(form) => ( - - ( - - - - - - - - )} - /> - ( - - - - ({ - key: journal.id, - label: `${journal.number} - ${journal.label}`, - })} - /> - - - - )} - /> -
- ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> -
-
- -
- ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> -
-
-
- )} -
-
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/deleteOneEntry.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/deleteOneEntry.tsx deleted file mode 100644 index 36d97645..00000000 --- a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/deleteOneEntry.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { deleteOneEntryRouteDefinition, readAllEntriesRouteDefinition } from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { toast } from "@arrhes/ui" -import type { ComponentPropsWithRef, ReactElement } from "react" -import type * as v from "valibot" -import { ConfirmationModal } from "../../../../../components/overlays/dialog/confirmationModal.tsx" -import { applicationRouter } from "../../../../../routes/applicationRouter.tsx" -import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../utilities/invalidateData.ts" - -export function DeleteOneEntry(props: { - entry: v.InferOutput - children: ReactElement> -}) { - async function onSubmit() { - const deleteResponse = await getResponseBodyFromAPI({ - routeDefinition: deleteOneEntryRouteDefinition, - body: { - idEntry: props.entry.id, - idYear: props.entry.idYear, - }, - }) - - if (deleteResponse.ok === false) { - toast({ - title: "Erreur lors de la suppression de l'écriture", - variant: "error", - }) - return - } - - await invalidateData({ - routeDefinition: readAllEntriesRouteDefinition, - body: { - idYear: props.entry.idYear, - }, - }) - - toast({ - title: "Écriture supprimée", - variant: "success", - }) - - applicationRouter.navigate({ - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/écritures", - params: { - idOrganization: props.entry.idOrganization, - idYear: props.entry.idYear, - }, - }) - } - - return ( - - Cette action supprimera l'écriture et toutes les données associées. -
- Cette action est irréversible. - - } - submitButtonProps={{ - color: "danger", - text: "Supprimer l'écriture", - }} - onSubmit={onSubmit} - > - {props.children} -
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/duplicateOneEntry.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/duplicateOneEntry.tsx deleted file mode 100644 index aa6f3807..00000000 --- a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/duplicateOneEntry.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { - duplicateOneEntryRouteDefinition, - readAllEntriesRouteDefinition, - readAllEntryTagsRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { Button, ButtonGhostContent, ButtonPlainContent, toast } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconCopyCheck } from "@tabler/icons-react" -import { type ComponentPropsWithRef, type ReactElement, useState } from "react" -import type * as v from "valibot" -import { Dialog } from "../../../../../components/overlays/dialog/dialog.tsx" -import { applicationRouter } from "../../../../../routes/applicationRouter.tsx" -import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../utilities/invalidateData.ts" - -export function DuplicateOneEntry(props: { - entry: v.InferOutput - children: ReactElement> -}) { - const [open, setOpen] = useState(false) - - async function onSubmit() { - const duplicateResponse = await getResponseBodyFromAPI({ - routeDefinition: duplicateOneEntryRouteDefinition, - body: { - idEntry: props.entry.id, - idYear: props.entry.idYear, - }, - }) - - if (duplicateResponse.ok === false) { - toast({ - title: "Erreur lors de la duplication de l'écriture", - variant: "error", - }) - return - } - - await Promise.all([ - invalidateData({ - routeDefinition: readAllEntriesRouteDefinition, - body: { - idYear: props.entry.idYear, - }, - }), - invalidateData({ - routeDefinition: readAllEntryTagsRouteDefinition, - body: { - idYear: props.entry.idYear, - }, - }), - ]) - - toast({ - title: "Écriture dupliquée", - variant: "success", - }) - - applicationRouter.navigate({ - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/écritures/$idEntry", - params: { - idOrganization: props.entry.idOrganization, - idYear: props.entry.idYear, - idEntry: duplicateResponse.data.id, - }, - }) - - setOpen(false) - } - - async function onCancel() { - setOpen(false) - } - - return ( - setOpen(value)} - > - { - setOpen(true) - event.preventDefault() - }} - > - {props.children} - - {open === false ? null : ( - - -
- Voulez-vous dupliquer cette écriture ? - - Cette action dupliquera l'écriture et toutes les données associées. - -
- - - - -
- )} -
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/entryCategoriesTab.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/entryCategoriesTab.tsx deleted file mode 100644 index 0da46d8a..00000000 --- a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/entryCategoriesTab.tsx +++ /dev/null @@ -1,220 +0,0 @@ -import { - addOneEntryTagRouteDefinition, - readAllEntryTagsRouteDefinition, - removeOneEntryTagRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { Button, ButtonGhostContent, FormatNull, FormatText, InputCombobox, toast } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconLinkOff } from "@tabler/icons-react" -import { useParams } from "@tanstack/react-router" -import { useState } from "react" -import type * as v from "valibot" -import { DataBlock } from "../../../../../components/layouts/dataBlock/dataBlock.tsx" -import { Section } from "../../../../../components/layouts/section/section.tsx" -import { entryLayoutRoute } from "../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLayoutRoute.tsx" -import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.js" -import { invalidateData } from "../../../../../utilities/invalidateData.js" -import type { YearDataKey } from "../../yearDataWrapper.tsx" -import { YearDataWrapper } from "../../yearDataWrapper.tsx" - -const requiredKeys = [ - "entries", - "entryTags", - "tags", -] as const satisfies readonly YearDataKey[] - -export function EntryCategoriesTab() { - const params = useParams({ - from: entryLayoutRoute.id, - }) - - return ( - - {({ entries, entryTags, tags }) => { - const entry = entries.find((r) => r.id === params.idEntry) - if (entry === undefined) return null - - return ( - - ) - }} - - ) -} - -function EntryCategoriesTabContent(props: { - entry: v.InferOutput - entryTags: v.InferOutput[] - tags: v.InferOutput[] - idYear: string -}) { - const [isAdding, setIsAdding] = useState(false) - const [isRemoving, setIsRemoving] = useState(null) - - const currentEntryTags = props.entryTags.filter((et) => et.idEntry === props.entry.id) - const currentTagIds = new Set(currentEntryTags.map((et) => et.idTag)) - const resolvedTags = currentEntryTags - .map((et) => { - const tag = props.tags.find((t) => t.id === et.idTag) - return tag - ? { - entryTagId: et.id, - tagId: tag.id, - label: tag.label, - } - : null - }) - .filter((t): t is NonNullable => t !== null) - - const availableTags = props.tags - .filter((t) => !currentTagIds.has(t.id)) - .map((t) => ({ - key: t.id, - label: t.label, - })) - - const handleAddTag = async (idTag: string | null | undefined) => { - if (!idTag) return - if (currentTagIds.has(idTag)) return - setIsAdding(true) - const response = await getResponseBodyFromAPI({ - routeDefinition: addOneEntryTagRouteDefinition, - body: { - idYear: props.idYear, - idEntry: props.entry.id, - idTag, - }, - }) - if (response.ok === false) { - toast({ - title: "Impossible d'ajouter la catégorie", - variant: "error", - }) - } else { - toast({ - title: "Catégorie ajoutée", - variant: "success", - }) - await invalidateData({ - routeDefinition: readAllEntryTagsRouteDefinition, - body: { - idYear: props.idYear, - }, - }) - } - setIsAdding(false) - } - - const handleRemoveTag = async (entryTagId: string) => { - setIsRemoving(entryTagId) - const response = await getResponseBodyFromAPI({ - routeDefinition: removeOneEntryTagRouteDefinition, - body: { - idYear: props.idYear, - idEntryTag: entryTagId, - }, - }) - if (response.ok === false) { - toast({ - title: "Impossible de retirer la catégorie", - variant: "error", - }) - } else { - toast({ - title: "Catégorie retirée", - variant: "success", - }) - await invalidateData({ - routeDefinition: readAllEntryTagsRouteDefinition, - body: { - idYear: props.idYear, - }, - }) - } - setIsRemoving(null) - } - - return ( - -
- -
- - - - {resolvedTags.length === 0 ? ( -
- -
- ) : ( - resolvedTags.map((tag) => ( -
- {tag.label} - -
- )) - )} -
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/entryLayout.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/entryLayout.tsx deleted file mode 100644 index 9c955562..00000000 --- a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/entryLayout.tsx +++ /dev/null @@ -1,234 +0,0 @@ -import { Button, ButtonGhostContent, ButtonOutlineContent, FormatError, formatPrice } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { - IconArrowBackUp, - IconChevronLeft, - IconCopyCheck, - IconDatabase, - IconDotsVertical, - IconInfoCircle, - IconList, - IconTag, - IconTrash, -} from "@tabler/icons-react" -import { Outlet, useParams } from "@tanstack/react-router" -import { useState } from "react" -import { Banner } from "../../../../../components/layouts/banner.tsx" -import { Page } from "../../../../../components/layouts/page/page.tsx" -import { Tab } from "../../../../../components/layouts/tab/tab.tsx" -import { LinkButton } from "../../../../../components/linkButton.js" -import { Popover } from "../../../../../components/overlays/popover/popover.js" -import { entryLayoutRoute } from "../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLayoutRoute.tsx" -import { compareAmounts } from "../../../../../utilities/compareAmounts.ts" -import type { YearDataKey } from "../../yearDataWrapper.tsx" -import { YearDataWrapper } from "../../yearDataWrapper.tsx" -import { ReverseOneEntry } from "../reverseOneEntry.tsx" -import { DeleteOneEntry } from "./deleteOneEntry.tsx" -import { DuplicateOneEntry } from "./duplicateOneEntry.tsx" - -const requiredKeys = [ - "entries", - "entryLines", - "entryTags", - "accounts", - "journals", - "tags", - "files", -] as const satisfies readonly YearDataKey[] - -export function EntryLayout() { - const params = useParams({ - from: entryLayoutRoute.id, - }) - const [menuOpen, setMenuOpen] = useState(false) - - return ( - - {({ entries, entryLines: allEntryLines }) => { - const entry = entries.find((r) => r.id === params.idEntry) - - if (entry === undefined) { - return ( - - ) - } - - const entryLines = allEntryLines.filter((row) => row.idEntry === params.idEntry) - - let totalDebit = 0 - let totalCredit = 0 - - for (const entryLine of entryLines) { - totalDebit += Number(entryLine.debit) - totalCredit += Number(entryLine.credit) - } - - return ( - - -
- - } - text="Retour" - /> - -
- - - - - - -
setMenuOpen(false)} - > - } - text="Extourner" - className={css({ - width: "100%", - justifyContent: "start", - })} - /> -
-
- -
setMenuOpen(false)} - > - } - text="Dupliquer" - className={css({ - width: "100%", - justifyContent: "start", - })} - /> -
-
-
-
- - } - title="Supprimer" - color="danger" - /> - -
-
- {entry.idFile !== null ? null : ( - Il manque une pièce justificative. - )} - {compareAmounts({ - a: totalDebit, - b: totalCredit, - }) ? null : ( - - Les montants au débit et au crédit sont différents, veuillez corriger pour pouvoir - valider. ( - {formatPrice({ - price: totalDebit - totalCredit, - })} - ) - - )} - , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/écritures/$idEntry", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - idEntry: params.idEntry, - }, - }, - { - label: "Mouvements", - icon: , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/écritures/$idEntry/mouvements", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - idEntry: params.idEntry, - }, - }, - { - label: "Catégories", - icon: , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/écritures/$idEntry/catégories", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - idEntry: params.idEntry, - }, - }, - { - label: "Métadonnées", - icon: , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/écritures/$idEntry/métadonnées", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - idEntry: params.idEntry, - }, - }, - ]} - /> - -
-
- ) - }} -
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/entryLinesTab.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/entryLinesTab.tsx deleted file mode 100644 index 95146fdc..00000000 --- a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/entryLinesTab.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import type { readAllAccountsRouteDefinition } from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { ButtonOutlineContent, ButtonPlainContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconEdit, IconPlus } from "@tabler/icons-react" -import { useParams } from "@tanstack/react-router" -import type * as v from "valibot" -import { Section } from "../../../../../components/layouts/section/section.tsx" -import { entryLayoutRoute } from "../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLayoutRoute.tsx" -import type { YearDataKey } from "../../yearDataWrapper.tsx" -import { YearDataWrapper } from "../../yearDataWrapper.tsx" -import { CreateOneEntryLine } from "./createOneEntryLine.tsx" -import { EntryLinesTable } from "./entryLinesTable.tsx" -import { UpdateManyEntryLines } from "./updateManyEntryLines.tsx" - -const requiredKeys = [ - "entries", - "entryLines", - "accounts", -] as const satisfies readonly YearDataKey[] - -export function EntryLinesTab() { - const params = useParams({ - from: entryLayoutRoute.id, - }) - - return ( - - {({ entries, entryLines: allEntryLines, accounts }) => { - const entry = entries.find((r) => r.id === params.idEntry) - if (entry === undefined) return null - - const entryLines = allEntryLines.filter((row) => row.idEntry === params.idEntry) - const accountsMap = new Map( - accounts.map((account) => [ - account.id, - account, - ]), - ) - - return ( - - ) - }} - - ) -} - -function EntryLinesTabContent(props: { - entry: v.InferOutput - entryLines: v.InferOutput[] - accounts: Map[number]> -}) { - return ( - -
- - } - text="Ajouter un mouvement" - /> - - - } - text="Modifier plusieurs mouvements" - /> - -
- -
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/entryLinesTable.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/entryLinesTable.tsx deleted file mode 100644 index 9ede0c9c..00000000 --- a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/entryLinesTable.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import type { readAllAccountsRouteDefinition } from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { ButtonGhostContent, FormatDateTime, FormatNull, FormatPrice, FormatText } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconEye, IconPencil } from "@tabler/icons-react" -import type * as v from "valibot" -import { DataTable } from "../../../../../components/layouts/dataTable.tsx" -import { LinkButton } from "../../../../../components/linkButton.tsx" -import { UpdateOneEntryLine } from "./$idEntryLine/updateOneEntryLine.tsx" - -export function EntryLinesTable(props: { - entry: v.InferOutput - entryLines: Array> - accounts: Map[number]> - isLoading?: boolean -}) { - return ( - ( -
- - } - text={undefined} - /> - - - } - text={undefined} - /> - -
- ), - enableSorting: false, - enableGlobalFilter: false, - }, - { - accessorKey: "label", - header: "Libellé", - cell: ({ row }) => {row.original.label}, - filterFn: "includesString", - }, - { - accessorKey: "idAccount", - header: "Compte", - cell: ({ row }) => { - const account = props.accounts.get(row.original.idAccount) - if (!account) return - return ( -
- - {account.number} - - - {account.label} - -
- ) - }, - filterFn: "includesString", - }, - { - accessorKey: "debit", - header: "Débit", - cell: ({ row }) => , - filterFn: "includesString", - }, - { - accessorKey: "credit", - header: "Crédit", - cell: ({ row }) => , - filterFn: "includesString", - }, - { - accessorKey: "createdAt", - header: "Ajouté le", - cell: ({ row }) => , - filterFn: "includesString", - }, - ]} - /> - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/entryMetadataTab.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/entryMetadataTab.tsx deleted file mode 100644 index 8c51e84d..00000000 --- a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/entryMetadataTab.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { FormatDateTime, FormatText } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { useParams } from "@tanstack/react-router" -import { DataBlock } from "../../../../../components/layouts/dataBlock/dataBlock.tsx" -import { Section } from "../../../../../components/layouts/section/section.tsx" -import { entryLayoutRoute } from "../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLayoutRoute.tsx" -import type { YearDataKey } from "../../yearDataWrapper.tsx" -import { YearDataWrapper } from "../../yearDataWrapper.tsx" - -const requiredKeys = [ - "entries", -] as const satisfies readonly YearDataKey[] - -export function EntryMetadataTab() { - const params = useParams({ - from: entryLayoutRoute.id, - }) - - return ( - - {({ entries }) => { - const entry = entries.find((r) => r.id === params.idEntry) - if (entry === undefined) return null - - return ( - - - - - - - - - - - - {entry.id} - - - - - ) - }} - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/entryPage.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/entryPage.tsx deleted file mode 100644 index c2b0c3c1..00000000 --- a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/entryPage.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { ButtonPlainContent, FormatDate, FormatNull, FormatPrice, FormatText } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconPencil } from "@tabler/icons-react" -import { useParams } from "@tanstack/react-router" -import type * as v from "valibot" -import { DataBlock } from "../../../../../components/layouts/dataBlock/dataBlock.tsx" -import { Section } from "../../../../../components/layouts/section/section.tsx" -import { entryLayoutRoute } from "../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLayoutRoute.tsx" -import type { YearDataKey } from "../../yearDataWrapper.tsx" -import { YearDataWrapper } from "../../yearDataWrapper.tsx" -import { UpdateOneEntry } from "./updateOneEntry.tsx" - -const requiredKeys = [ - "entries", - "entryLines", - "entryTags", - "journals", - "tags", - "files", -] as const satisfies readonly YearDataKey[] - -export function EntryPage() { - const params = useParams({ - from: entryLayoutRoute.id, - }) - - return ( - - {({ entries, entryLines: allEntryLines, entryTags, journals, tags, files }) => { - const entry = entries.find((r) => r.id === params.idEntry) - if (entry === undefined) return null - - const entryLines = allEntryLines.filter((row) => row.idEntry === params.idEntry) - const journal = - entry.idJournal !== null ? (journals.find((j) => j.id === entry.idJournal) ?? null) : null - const entryTagIds = entryTags.filter((et) => et.idEntry === entry.id).map((et) => et.idTag) - const entryTagLabels = entryTagIds - .map((id) => tags.find((t) => t.id === id)) - .filter((t): t is NonNullable => Boolean(t)) - .map((t) => t.label) - const file = entry.idFile !== null ? (files.find((f) => f.id === entry.idFile) ?? null) : null - - let totalDebit = 0 - let totalCredit = 0 - for (const entryLine of entryLines) { - totalDebit += Number(entryLine.debit) - totalCredit += Number(entryLine.credit) - } - - return ( - - ) - }} - - ) -} - -function EntryInformationsTab(props: { - entry: v.InferOutput - journal: v.InferOutput | null - entryTagLabels: string[] - file: v.InferOutput | null - totalDebit: number - totalCredit: number -}) { - return ( - -
- - } - text="Modifier" - /> - -
- - - - - {props.entry.label} - - - - - - {props.entry.idJournal === null ? ( - - ) : props.journal !== null ? ( - {`(${props.journal.code}) ${props.journal.label}`} - ) : ( - - )} - - - {props.entry.idFile === null ? ( - - ) : props.file !== null ? ( - {props.file.reference} - ) : ( - - )} - - - - - - - - - - - - - - -
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/updateManyEntryLines.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/updateManyEntryLines.tsx deleted file mode 100644 index 8d9dc177..00000000 --- a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/updateManyEntryLines.tsx +++ /dev/null @@ -1,307 +0,0 @@ -import { - readAllEntryLinesRouteDefinition, - updateManyEntryLinesRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { InputText, InputToggle, toast } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconPencil } from "@tabler/icons-react" -import { type JSX, useState } from "react" -import { Fragment } from "react/jsx-runtime" -import type * as v from "valibot" -import { FormControl } from "../../../../../components/forms/formControl.tsx" -import { FormError } from "../../../../../components/forms/formError.tsx" -import { FormField } from "../../../../../components/forms/formField.tsx" -import { FormItem } from "../../../../../components/forms/formItem.tsx" -import { FormLabel } from "../../../../../components/forms/formLabel.tsx" -import { FormRoot } from "../../../../../components/forms/formRoot.tsx" -import { Drawer } from "../../../../../components/overlays/drawer/drawer.tsx" -import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../utilities/invalidateData.ts" - -export function UpdateManyEntryLines(props: { - entry: v.InferOutput - children: JSX.Element -}) { - const [open, setOpen] = useState(false) - - return ( - - {props.children} - - - - , - text: "Modifier les mouvements", - }} - onSubmit={async (data) => { - const updateManyEntryLinesResponse = await getResponseBodyFromAPI({ - routeDefinition: updateManyEntryLinesRouteDefinition, - body: data, - }) - if (updateManyEntryLinesResponse.ok === false) { - toast({ - title: "Impossible de modifier les mouvements", - variant: "error", - }) - return false - } - - toast({ - title: "Mouvements modifiés avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await Promise.all([ - invalidateData({ - routeDefinition: readAllEntryLinesRouteDefinition, - body: { - idYear: props.entry.idYear, - idEntry: props.entry.id, - }, - }), - invalidateData({ - routeDefinition: readAllEntryLinesRouteDefinition, - body: { - idYear: props.entry.idYear, - }, - }), - ]) - - setOpen(false) - }} - > - {(form) => ( - - ( - - - - - - - - )} - /> -
- -
- ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> -
-
-
- )} -
-
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/updateOneEntry.tsx b/packages/website/src/features/dashboard/$idYear/entries/$idEntry/updateOneEntry.tsx deleted file mode 100644 index 09ced696..00000000 --- a/packages/website/src/features/dashboard/$idYear/entries/$idEntry/updateOneEntry.tsx +++ /dev/null @@ -1,191 +0,0 @@ -import { - readAllEntriesRouteDefinition, - readAllFilesRouteDefinition, - readAllJournalsRouteDefinition, - readOneEntryRouteDefinition, - updateOneEntryRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { InputDate, InputText, toast } from "@arrhes/ui" -import { IconPencil } from "@tabler/icons-react" -import { type JSX, useState } from "react" -import { Fragment } from "react/jsx-runtime" -import type * as v from "valibot" -import { FormControl } from "../../../../../components/forms/formControl.tsx" -import { FormError } from "../../../../../components/forms/formError.tsx" -import { FormField } from "../../../../../components/forms/formField.tsx" -import { FormItem } from "../../../../../components/forms/formItem.tsx" -import { FormLabel } from "../../../../../components/forms/formLabel.tsx" -import { FormRoot } from "../../../../../components/forms/formRoot.tsx" -import { InputDataCombobox } from "../../../../../components/inputDataCombobox.tsx" -import { Drawer } from "../../../../../components/overlays/drawer/drawer.tsx" -import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../utilities/invalidateData.ts" - -export function UpdateOneEntry(props: { entry: v.InferOutput; children: JSX.Element }) { - const [open, setOpen] = useState(false) - - return ( - - {props.children} - - - - , - text: "Modifier l'écriture", - }} - onSubmit={async (data) => { - const updateEntryResponse = await getResponseBodyFromAPI({ - routeDefinition: updateOneEntryRouteDefinition, - body: data, - }) - if (updateEntryResponse.ok === false) { - toast({ - title: "Impossible de modifier l'écriture", - variant: "error", - }) - return false - } - - toast({ - title: "Écriture modifiée avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await Promise.all([ - invalidateData({ - routeDefinition: readAllEntriesRouteDefinition, - body: { - idYear: props.entry.idYear, - }, - }), - invalidateData({ - routeDefinition: readOneEntryRouteDefinition, - body: { - idYear: props.entry.idYear, - idEntry: props.entry.id, - }, - }), - ]) - - setOpen(false) - }} - > - {(form) => ( - - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - ({ - key: journal.id, - label: `(${journal.code}) ${journal.label}`, - })} - /> - - - - )} - /> - ( - - - - ({ - key: file.id, - label: file.reference ?? "", - })} - /> - - - - )} - /> - - )} - - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/entries/CreateOneEntry.tsx b/packages/website/src/features/dashboard/$idYear/entries/CreateOneEntry.tsx new file mode 100644 index 00000000..06c4d12d --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/entries/CreateOneEntry.tsx @@ -0,0 +1,344 @@ +import { + addOneEntryTagRouteDefinition, + createOneEntryFromTemplateRouteDefinition, + readAllEntriesRouteDefinition, + readAllEntryTagsRouteDefinition, + readAllFilesRouteDefinition, + readAllJournalsRouteDefinition, + readAllTagsRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, InputComboboxMultiple, InputDate, InputSelect, InputText, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPlus } from "@tabler/icons-react" +import { type JSX, useState } from "react" +import { Fragment } from "react/jsx-runtime" +import type * as v from "valibot" +import { FormControl } from "../../../../components/forms/FormControl.js" +import { FormError } from "../../../../components/forms/FormError.js" +import { FormField } from "../../../../components/forms/FormField.js" +import { FormGroup } from "../../../../components/forms/FormGroup.js" +import { FormItem } from "../../../../components/forms/FormItem.js" +import { FormLabel } from "../../../../components/forms/FormLabel.js" +import { FormRoot } from "../../../../components/forms/FormRoot.js" +import { InputDataCombobox } from "../../../../components/InputDataCombobox.js" +import { useTabs } from "../../../../contexts/tabs/useTabs.js" +import { applicationRouter } from "../../../../routes/applicationRouter.js" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" +import { invalidateData } from "../../../../utilities/invalidateData.js" +import { useDataFromAPI } from "../../../../utilities/useHTTPData.js" +import { type EntryTemplateKey, entryTemplates } from "./entryTemplates/entryTemplates.js" + +function CreateOneEntryPanel(props: { + idOrganization: v.InferOutput["id"] + idYear: v.InferOutput["id"] + onClose: () => void +}) { + const [selectedTemplate, setSelectedTemplate] = useState("empty") + const [isTemplateReady, setIsTemplateReady] = useState(false) + const [selectedTags, setSelectedTags] = useState< + Array<{ + key: string + label: string + }> + >([]) + + const tagsResponse = useDataFromAPI({ + routeDefinition: readAllTagsRouteDefinition, + body: { + idYear: props.idYear, + }, + }) + + const activeTemplate = entryTemplates.find((t) => t.key === selectedTemplate) + const isSubmitDisabled = activeTemplate?.hasActionButton === true && isTemplateReady === false + + return ( +
+ , + text: "Ajouter l'écriture", + isDisabled: isSubmitDisabled, + }} + onSubmit={async (data) => { + const createEntryResponse = await getResponseBodyFromAPI({ + routeDefinition: createOneEntryFromTemplateRouteDefinition, + body: data, + }) + if (createEntryResponse.ok === false) { + toast({ + title: "Impossible d'ajouter l'écriture", + variant: "error", + }) + return false + } + + if (selectedTags.length > 0) { + await Promise.all( + selectedTags.map((tag) => + getResponseBodyFromAPI({ + routeDefinition: addOneEntryTagRouteDefinition, + body: { + idYear: props.idYear, + idEntry: createEntryResponse.data.id, + idTag: tag.key, + }, + }), + ), + ) + } + + toast({ + title: "Écriture ajoutée avec succès", + variant: "success", + }) + applicationRouter.navigate({ + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/écritures/$idEntry", + params: { + idOrganization: props.idOrganization, + idYear: props.idYear, + idEntry: createEntryResponse.data.id, + }, + }) + return true + }} + onCancel={undefined} + onSuccess={async () => { + await Promise.all([ + invalidateData({ + routeDefinition: readAllEntriesRouteDefinition, + body: { + idYear: props.idYear, + }, + }), + invalidateData({ + routeDefinition: readAllEntryTagsRouteDefinition, + body: { + idYear: props.idYear, + }, + }), + ]) + + props.onClose() + }} + > + {(form) => ( + + ( + + + + + + + + )} + /> + ( + + + + + + + + )} + /> + ( + + + + ({ + key: journal.id, + label: `(${journal.code}) ${journal.label}`, + })} + /> + + + + )} + /> + ( + + + + ({ + key: file.id, + label: file.reference ? `${file.name} (${file.reference})` : file.name, + })} + /> + + + + )} + /> + + + ({ + key: tag.id, + label: tag.label, + })) + } + selectedOptions={selectedTags} + onChange={setSelectedTags} + loading={tagsResponse.isPending} + /> + + + + + Choisir un modèle + + { + const newValue = value ?? "empty" + setSelectedTemplate(newValue) + setIsTemplateReady(false) + form.setValue("entryLines", []) + }} + options={entryTemplates.map((template) => ({ + key: template.key, + label: template.label, + }))} + placeholder="Sélectionner un modèle" + /> + + {activeTemplate === undefined || activeTemplate.key === "empty" ? null : ( +
+ {activeTemplate?.formComponent({ + form, + idOrganization: props.idOrganization, + idYear: props.idYear, + onTemplateReadyChange: setIsTemplateReady, + })} +
+ )} +
+
+ )} +
+
+ ) +} + +export function CreateOneEntry(props: { + idOrganization: v.InferOutput["id"] + idYear: v.InferOutput["id"] + children: JSX.Element +}) { + const { openPanelTab, closeTab } = useTabs() + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/entries/EntriesPage.tsx b/packages/website/src/features/dashboard/$idYear/entries/EntriesPage.tsx new file mode 100644 index 00000000..580c24b3 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/entries/EntriesPage.tsx @@ -0,0 +1,198 @@ +import { Button, ButtonGhostContent, ButtonPlainContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconDotsVertical, IconDownload, IconFileExport, IconFileImport, IconPlus } from "@tabler/icons-react" +import { useParams } from "@tanstack/react-router" +import { Fragment } from "react" +import { Page } from "../../../../components/layouts/page/page.js" +import { Popover } from "../../../../components/overlays/popover/popover.js" +import { useTabs } from "../../../../contexts/tabs/useTabs.js" +import { YearDataWrapper } from "../YearDataWrapper.tsx" +import { CreateOneEntry } from "./CreateOneEntry.js" +import { EntriesTable } from "./EntriesTable.js" +import { ExportEntryLines } from "./ExportEntryLines.js" +import { ExportFecFile } from "./ExportFecFile.js" +import { ImportFecFile } from "./ImportFecFile.js" + +export function EntriesPage({ + idOrganization: idOrganizationProp, + idYear: idYearProp, +}: { + idOrganization?: string + idYear?: string +}) { + const params = useParams({ + strict: false, + }) as { + idOrganization?: string + idYear?: string + } + const idOrganization = idOrganizationProp ?? params.idOrganization ?? "" + const idYear = idYearProp ?? params.idYear ?? "" + const { openPanelTab, closeTab } = useTabs() + + return ( + + + + {(data) => ( + +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ )} +
+
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/entries/EntriesTable.tsx b/packages/website/src/features/dashboard/$idYear/entries/EntriesTable.tsx new file mode 100644 index 00000000..e5f23acf --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/entries/EntriesTable.tsx @@ -0,0 +1,307 @@ +import type { readAllEntriesRouteDefinition } from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, FormatDate, FormatDateTime, FormatNull, FormatPrice, FormatText, LinkContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPencil } from "@tabler/icons-react" +import { useMemo } from "react" +import type * as v from "valibot" +import { DataTable } from "../../../../components/layouts/DataTable.js" +import { TabLink } from "../../../../components/layouts/tabBar/TabLink.js" +import type { YearDataMaps } from "../YearDataWrapper.tsx" +import { EntriesTableSelectionActions } from "./EntriesTableSelectionActions.js" + +export function EntriesTable(props: { + idOrganization: v.InferOutput["id"] + idYear: v.InferOutput["id"] + entries: v.InferOutput + accountById: YearDataMaps["accountById"] + entryLinesByEntryId: YearDataMaps["entryLinesByEntryId"] + entryTagsByEntryId: YearDataMaps["entryTagsByEntryId"] + journalById: YearDataMaps["journalById"] + tagById: YearDataMaps["tagById"] + fileById: YearDataMaps["fileById"] +}) { + const entriesData = useMemo( + () => + [ + ...props.entries, + ].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()), + [ + props.entries, + ], + ) + + const linesByEntry = props.entryLinesByEntryId + const journalsMap = props.journalById + const tagsMap = props.tagById + const tagsByEntry = useMemo(() => { + const m = new Map() + for (const [entryId, ets] of props.entryTagsByEntryId) { + m.set( + entryId, + ets.map((et) => et.idTag), + ) + } + return m + }, [ + props.entryTagsByEntryId, + ]) + const filesMap = props.fileById + const accountsMap = props.accountById + + return ( + row.id} + selectionActions={(selectedRows) => ( + + )} + emptyStateProps={{ + icon: , + title: "Aucune écriture", + subtitle: "Les écritures de votre exercice apparaîtront ici.", + }} + columns={[ + { + accessorKey: "label", + header: "Libellé", + cell: ({ row }) => ( + + + + ), + filterFn: "includesString", + }, + { + accessorKey: "date", + header: "Date", + cell: ({ row }) => , + filterFn: "includesString", + }, + { + accessorKey: "idJournal", + header: "Journal", + cell: ({ row }) => { + if (row.original.idJournal === null) return + const journal = journalsMap.get(row.original.idJournal) + if (!journal) return + return {journal.code} + }, + filterFn: "includesString", + }, + { + accessorKey: "id", + id: "tags", + header: "Catégorie", + cell: ({ row }) => { + const tagIds = tagsByEntry.get(row.original.id) + if (!tagIds || tagIds.length === 0) return + const tagLabels = tagIds + .map((id) => tagsMap.get(id)) + .filter((tag): tag is NonNullable => Boolean(tag)) + .map((tag) => tag.label) + if (tagLabels.length === 0) return + return {tagLabels.join(", ")} + }, + filterFn: "includesString", + }, + { + accessorKey: "idFile", + header: "Pièce justificative", + cell: ({ row }) => { + if (row.original.idFile === null) return + const file = filesMap.get(row.original.idFile) + if (!file) return + return ( + + + + ) + }, + filterFn: "includesString", + }, + { + accessorKey: "createdAt", + header: "Ajouté le", + cell: ({ row }) => , + filterFn: "includesString", + }, + { + accessorKey: "lastUpdatedAt", + header: "Dernière mise à jour le", + cell: ({ row }) => , + filterFn: "includesString", + }, + ]} + renderSubComponent={({ row }) => { + const rows = linesByEntry.get(row.original.id) + if (!rows || rows.length === 0) { + return ( + + ) + } + return ( + + + + + + + + + + + {rows.map((entryLine) => { + const account = accountsMap.get(entryLine.idAccount) + return ( + + + + + + + ) + })} + +
+ Compte + + Libellé + + Débit + + Crédit +
+ {account ? ( +
+ + {account.number} + + + {account.label} + +
+ ) : ( + + )} +
+ {entryLine.label} + + + + +
+ ) + }} + /> + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/entries/EntriesTableSelectionActions.tsx b/packages/website/src/features/dashboard/$idYear/entries/EntriesTableSelectionActions.tsx new file mode 100644 index 00000000..b4399c40 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/entries/EntriesTableSelectionActions.tsx @@ -0,0 +1,129 @@ +import { deleteOneEntryRouteDefinition, readAllEntriesRouteDefinition } from "@arrhes/application-metadata/routes" +import { + Button, + ButtonGhostContent, + ButtonOutlineContent, + ButtonPlainContent, + Dialog, + toast, + useModalStore, +} from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconChevronDown, IconTrash } from "@tabler/icons-react" +import type { Row } from "@tanstack/react-table" +import { useId } from "react" +import type * as v from "valibot" +import { Popover } from "../../../../components/overlays/popover/popover.js" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" +import { invalidateData } from "../../../../utilities/invalidateData.js" + +type EntryRow = v.InferOutput[number] + +export function EntriesTableSelectionActions(props: { selectedRows: Array>; idYear: string }) { + const deleteModalId = useId() + const { open: openModal, close: closeModal } = useModalStore() + + async function handleDelete() { + const results = await Promise.all( + props.selectedRows.map((row) => + getResponseBodyFromAPI({ + routeDefinition: deleteOneEntryRouteDefinition, + body: { + idEntry: row.original.id, + idYear: props.idYear, + }, + }), + ), + ) + await invalidateData({ + routeDefinition: readAllEntriesRouteDefinition, + body: { + idYear: props.idYear, + }, + }) + if (results.some((r) => r.ok === false)) { + toast({ + title: "Certaines écritures n'ont pas pu être supprimées", + variant: "error", + }) + } else { + toast({ + title: "Écritures supprimées", + variant: "success", + }) + } + } + + return ( + + + + + + + + + + , + ) + } + > + } + text="Supprimer" + color="danger" + className={css({ + width: "100%", + justifyContent: "start", + })} + /> + + + + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/entries/ExportEntryLines.tsx b/packages/website/src/features/dashboard/$idYear/entries/ExportEntryLines.tsx new file mode 100644 index 00000000..1797cda9 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/entries/ExportEntryLines.tsx @@ -0,0 +1,311 @@ +import { + readAllAccountsRouteDefinition, + type readAllEntriesRouteDefinition, + type readAllEntryLinesRouteDefinition, + readAllJournalsRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { formatDate, formatPrice, InputDate, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconDownload } from "@tabler/icons-react" +import { useMemo } from "react" +import * as v from "valibot" +import { FormControl } from "../../../../components/forms/FormControl.js" +import { FormError } from "../../../../components/forms/FormError.js" +import { FormField } from "../../../../components/forms/FormField.js" +import { FormGroup } from "../../../../components/forms/FormGroup.js" +import { FormItem } from "../../../../components/forms/FormItem.js" +import { FormLabel } from "../../../../components/forms/FormLabel.js" +import { FormRoot } from "../../../../components/forms/FormRoot.js" +import { InputDataCombobox } from "../../../../components/InputDataCombobox.js" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" + +export function ExportEntryLines(props: { + idOrganization: v.InferOutput["id"] + idYear: v.InferOutput["id"] + entries: v.InferOutput + entryLines: v.InferOutput + onClose: () => void +}) { + const entriesMap = useMemo(() => { + return new Map( + props.entries.map((r) => [ + r.id, + r, + ]), + ) + }, [ + props.entries, + ]) + + function escapeCsvValue(value: string): string { + if (value.includes(";") || value.includes('"') || value.includes("\n")) { + return `"${value.replace(/"/g, '""')}"` + } + return value + } + + return ( +
+ , + text: "Exporter en CSV", + }} + onSubmit={async (data) => { + const filteredRows = props.entryLines.filter((row) => { + const entry = entriesMap.get(row.idEntry) + if (!entry) return false + + if (data.idJournal && entry.idJournal !== data.idJournal) return false + if (data.idAccount && row.idAccount !== data.idAccount) return false + if (data.dateFrom && entry.date < data.dateFrom) return false + if (data.dateTo && entry.date > data.dateTo) return false + + return true + }) + + if (filteredRows.length === 0) { + toast({ + title: "Aucun mouvement à exporter", + variant: "warning", + }) + return false + } + + const accountsResponse = await getResponseBodyFromAPI({ + routeDefinition: readAllAccountsRouteDefinition, + body: { + idYear: props.idYear, + }, + }) + const journalsResponse = await getResponseBodyFromAPI({ + routeDefinition: readAllJournalsRouteDefinition, + body: { + idYear: props.idYear, + }, + }) + + if (!accountsResponse.ok || !journalsResponse.ok) { + toast({ + title: "Impossible de charger les données", + variant: "error", + }) + return false + } + + const accountsMap = new Map( + accountsResponse.data.map((a) => [ + a.id, + { + number: a.number, + label: a.label, + }, + ]), + ) + const journalsMap = new Map( + journalsResponse.data.map((j) => [ + j.id, + { + code: j.code, + label: j.label, + }, + ]), + ) + + const headers = [ + "Date", + "Code journal", + "Libellé journal", + "Libellé écriture", + "N° compte", + "Libellé compte", + "Libellé mouvement", + "Débit", + "Crédit", + ] + + const rows = filteredRows + .map((row) => { + const entry = entriesMap.get(row.idEntry) + if (!entry) return null + + const account = accountsMap.get(row.idAccount) + const journal = entry.idJournal ? journalsMap.get(entry.idJournal) : null + + return [ + formatDate(entry.date) ?? "", + journal?.code ?? "", + journal?.label ?? "", + entry.label, + account?.number ?? "", + account?.label ?? "", + row.label ?? "", + formatPrice({ + price: row.debit, + }), + formatPrice({ + price: row.credit, + }), + ].map(escapeCsvValue) + }) + .filter((row) => row !== null) + + const csvContent = [ + headers.map(escapeCsvValue).join(";"), + ...rows.map((r) => r.join(";")), + ].join("\n") + + const BOM = "\uFEFF" + const blob = new Blob( + [ + BOM + csvContent, + ], + { + type: "text/csv;charset=utf-8;", + }, + ) + const url = URL.createObjectURL(blob) + const link = document.createElement("a") + link.href = url + link.download = `ecritures-${new Date().toISOString().slice(0, 10)}.csv` + link.click() + URL.revokeObjectURL(url) + + toast({ + title: `${filteredRows.length} mouvements exportés`, + variant: "success", + }) + return true + }} + onCancel={undefined} + onSuccess={async () => { + props.onClose() + }} + > + {(form) => ( + + ( + + + + ({ + key: journal.id, + label: `(${journal.code}) ${journal.label ?? ""}`, + })} + /> + + + + )} + /> + ( + + + + ({ + key: account.id, + label: `${account.number} - ${account.label}`, + })} + /> + + + + )} + /> + ( + + + + + + + + )} + /> + ( + + + + + + + + )} + /> + + )} + +
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/entries/ExportFecFile.tsx b/packages/website/src/features/dashboard/$idYear/entries/ExportFecFile.tsx new file mode 100644 index 00000000..1052381a --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/entries/ExportFecFile.tsx @@ -0,0 +1,123 @@ +import { + generateFecRouteDefinition, + type readAllEntriesRouteDefinition, + type readAllEntryLinesRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, ButtonPlainContent, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconFileExport } from "@tabler/icons-react" +import type * as v from "valibot" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" + +export function ExportFecFile(props: { + idOrganization: v.InferOutput["id"] + idYear: v.InferOutput["id"] + entries: v.InferOutput + entryLines: v.InferOutput + onClose: () => void +}) { + async function handleExport() { + if (props.entryLines.length === 0) { + toast({ + title: "Aucun mouvement à exporter", + variant: "warning", + }) + return + } + + const result = await getResponseBodyFromAPI({ + routeDefinition: generateFecRouteDefinition, + body: { + idYear: props.idYear, + }, + }) + + if (!result.ok) { + toast({ + title: "Erreur lors de la génération du FEC", + variant: "error", + }) + return + } + + const link = document.createElement("a") + link.href = result.data.url + link.click() + + toast({ + title: `${props.entryLines.length} mouvement${props.entryLines.length > 1 ? "s" : ""} exporté${props.entryLines.length > 1 ? "s" : ""} au format FEC`, + variant: "success", + }) + props.onClose() + } + + return ( +
+

+ Le Fichier des Écritures Comptables (FEC) est un export normé de toutes les écritures de l'exercice, au + format requis par l'administration fiscale. +

+

+ Nous avons créé également un outil de validation de conformité du FEC, disponible gratuitement en ligne + sur{" "} + + fec.arrhes.com + + . +

+

+ {props.entries.length} écriture{props.entries.length > 1 ? "s" : ""} - {props.entryLines.length}{" "} + mouvement{props.entryLines.length > 1 ? "s" : ""} +

+ +
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/entries/ImportFecFile.tsx b/packages/website/src/features/dashboard/$idYear/entries/ImportFecFile.tsx new file mode 100644 index 00000000..3b79c4db --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/entries/ImportFecFile.tsx @@ -0,0 +1,527 @@ +import { + createOneAccountRouteDefinition, + createOneEntryFromTemplateRouteDefinition, + createOneJournalRouteDefinition, + readAllAccountsRouteDefinition, + readAllEntriesRouteDefinition, + readAllEntryLinesRouteDefinition, + readAllJournalsRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, ButtonPlainContent, InputFile, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconFileImport } from "@tabler/icons-react" +import { useMemo, useState } from "react" +import type * as v from "valibot" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" +import { invalidateData } from "../../../../utilities/invalidateData.js" + +type FecLine = { + accountLabel: string + accountNumber: string + credit: number + debit: number + entryDate: string + entryLabel: string + journalCode: string + journalLabel: string +} + +type FecEntry = { + date: string + journalCode: string + journalLabel: string + key: string + label: string + lines: FecLine[] +} + +const REQUIRED_HEADERS = [ + "JournalCode", + "JournalLib", + "EcritureNum", + "EcritureDate", + "CompteNum", + "CompteLib", + "EcritureLib", + "Debit", + "Credit", +] as const + +function parseFecDate(rawValue: string, lineIndex: number): string { + const value = rawValue.trim() + if (/^\d{8}$/.test(value)) { + const year = value.slice(0, 4) + const month = value.slice(4, 6) + const day = value.slice(6, 8) + return `${year}-${month}-${day}T00:00:00.000Z` + } + + const date = new Date(value) + if (Number.isNaN(date.getTime())) { + throw new Error(`Date invalide sur la ligne ${lineIndex}`) + } + + return date.toISOString() +} + +function parseFecAmount(rawValue: string, lineIndex: number): number { + const normalized = rawValue.replaceAll(" ", "").replaceAll(",", ".").trim() + if (normalized === "") { + return 0 + } + + const parsed = Number(normalized) + if (Number.isNaN(parsed)) { + throw new Error(`Montant invalide sur la ligne ${lineIndex}`) + } + + return parsed +} + +function detectSeparator(headerLine: string): "\t" | "|" { + if (headerLine.includes("\t")) { + return "\t" + } + if (headerLine.includes("|")) { + return "|" + } + throw new Error("Séparateur FEC non reconnu (attendu: tabulation ou '|')") +} + +function getAccountType(accountNumber: string): "balance-sheet" | "income-statement" | "special" { + const firstDigit = accountNumber.trim().slice(0, 1) + if (firstDigit === "6" || firstDigit === "7") { + return "income-statement" + } + if (firstDigit === "8") { + return "special" + } + return "balance-sheet" +} + +function getAccountParentId(parameters: { + accountNumber: string + accountsByNumber: Map> +}): string | null { + const number = parameters.accountNumber.trim() + + for (let i = number.length - 1; i > 0; i--) { + const parentNumber = number.slice(0, i) + const parent = parameters.accountsByNumber.get(parentNumber) + if (parent !== undefined) { + return parent.id + } + } + + return null +} + +function parseFecFile(content: string): FecEntry[] { + const normalizedContent = content.replace(/^\uFEFF/, "") + const lines = normalizedContent + .split(/\r?\n/) + .map((line) => line.trimEnd()) + .filter((line) => line !== "") + + if (lines.length < 2) { + throw new Error("Le fichier FEC ne contient aucune écriture") + } + + const separator = detectSeparator(lines[0]) + const headers = lines[0].split(separator).map((value) => value.trim()) + const indexByHeader = new Map( + headers.map((header, index) => [ + header, + index, + ]), + ) + + for (const requiredHeader of REQUIRED_HEADERS) { + if (!indexByHeader.has(requiredHeader)) { + throw new Error(`Colonne manquante: ${requiredHeader}`) + } + } + + const entriesByKey = new Map() + + for (let rowIndex = 1; rowIndex < lines.length; rowIndex++) { + const line = lines[rowIndex] + const cells = line.split(separator) + const lineNumber = rowIndex + 1 + + const getCell = (header: string) => { + const index = indexByHeader.get(header) + if (index === undefined) { + return "" + } + return (cells[index] ?? "").trim() + } + + const journalCode = getCell("JournalCode").slice(0, 32) + const journalLabel = getCell("JournalLib").slice(0, 256) + const entryNumber = getCell("EcritureNum") + const entryDate = parseFecDate(getCell("EcritureDate"), lineNumber) + const accountNumber = getCell("CompteNum").slice(0, 32) + const accountLabel = getCell("CompteLib").slice(0, 256) + const pieceReference = getCell("PieceRef") + const entryLabel = (pieceReference || getCell("EcritureLib") || `Ecriture ${entryNumber}`).slice(0, 256) + const lineLabel = (getCell("EcritureLib") || entryLabel).slice(0, 256) + const debit = parseFecAmount(getCell("Debit"), lineNumber) + const credit = parseFecAmount(getCell("Credit"), lineNumber) + + if (journalCode === "" || entryNumber === "" || accountNumber === "") { + throw new Error(`Ligne ${lineNumber} invalide: champs obligatoires manquants`) + } + + if (debit < 0 || credit < 0) { + throw new Error(`Ligne ${lineNumber} invalide: montants négatifs non supportés`) + } + + const key = `${journalCode}__${entryNumber}__${entryDate}` + const existingEntry = entriesByKey.get(key) + const fecLine: FecLine = { + accountLabel, + accountNumber, + credit, + debit, + entryDate, + entryLabel: lineLabel, + journalCode, + journalLabel, + } + + if (existingEntry === undefined) { + entriesByKey.set(key, { + date: entryDate, + journalCode, + journalLabel, + key, + label: entryLabel, + lines: [ + fecLine, + ], + }) + continue + } + + existingEntry.lines.push(fecLine) + } + + const entries = [ + ...entriesByKey.values(), + ].sort((left, right) => { + const dateCompare = left.date.localeCompare(right.date) + if (dateCompare !== 0) { + return dateCompare + } + return left.key.localeCompare(right.key) + }) + + for (const entry of entries) { + const totalDebit = entry.lines.reduce((sum, line) => sum + line.debit, 0) + const totalCredit = entry.lines.reduce((sum, line) => sum + line.credit, 0) + const delta = Math.abs(totalDebit - totalCredit) + + if (delta > 0.01) { + throw new Error(`Ecriture non équilibrée: ${entry.label}`) + } + } + + return entries +} + +export function ImportFecFile(props: { + idYear: v.InferOutput["id"] + journals: v.InferOutput + accounts: v.InferOutput + onClose: () => void +}) { + const [selectedFile, setSelectedFile] = useState(null) + + const selectedFileLabel = useMemo(() => { + if (selectedFile === null) { + return "Aucun fichier sélectionné" + } + return selectedFile.name + }, [ + selectedFile, + ]) + + async function handleImport() { + if (selectedFile === null) { + toast({ + title: "Sélectionnez un fichier FEC", + variant: "warning", + }) + return + } + + const fileContent = await selectedFile.text() + + let entries: FecEntry[] + try { + entries = parseFecFile(fileContent) + } catch (error) { + const message = error instanceof Error ? error.message : "Fichier FEC invalide" + toast({ + title: message, + variant: "error", + }) + return + } + + const journalsByCode = new Map( + props.journals.map((journal) => [ + journal.code, + journal, + ]), + ) + const accountsByNumber = new Map( + props.accounts.map((account) => [ + account.number, + account, + ]), + ) + + let createdJournalsCount = 0 + let createdAccountsCount = 0 + let createdEntriesCount = 0 + let createdEntryLinesCount = 0 + + for (const entry of entries) { + let journal = journalsByCode.get(entry.journalCode) + if (journal === undefined) { + const createJournalResponse = await getResponseBodyFromAPI({ + routeDefinition: createOneJournalRouteDefinition, + body: { + idYear: props.idYear, + code: entry.journalCode, + label: entry.journalLabel || entry.journalCode, + }, + }) + + if (createJournalResponse.ok === false) { + toast({ + title: `Impossible de créer le journal ${entry.journalCode}`, + variant: "error", + }) + return + } + + journal = createJournalResponse.data + journalsByCode.set(journal.code, journal) + createdJournalsCount += 1 + } + + const entryLinesPayload: Array<{ + credit: string + debit: string + idAccount: string + isComputedForBalanceReport: boolean + isComputedForBalanceSheetReport: boolean + isComputedForIncomeStatementReport: boolean + isComputedForJournalReport: boolean + isComputedForLedgerReport: boolean + label: string + }> = [] + + for (const line of entry.lines) { + let account = accountsByNumber.get(line.accountNumber) + if (account === undefined) { + const createAccountResponse = await getResponseBodyFromAPI({ + routeDefinition: createOneAccountRouteDefinition, + body: { + idYear: props.idYear, + idAccountParent: getAccountParentId({ + accountNumber: line.accountNumber, + accountsByNumber, + }), + isSelectable: true, + label: line.accountLabel || `Compte ${line.accountNumber}`, + number: line.accountNumber, + type: getAccountType(line.accountNumber), + }, + }) + + if (createAccountResponse.ok === false) { + toast({ + title: `Impossible de créer le compte ${line.accountNumber}`, + variant: "error", + }) + return + } + + account = createAccountResponse.data + accountsByNumber.set(account.number, account) + createdAccountsCount += 1 + } + + entryLinesPayload.push({ + credit: line.credit.toFixed(2), + debit: line.debit.toFixed(2), + idAccount: account.id, + isComputedForBalanceReport: true, + isComputedForBalanceSheetReport: true, + isComputedForIncomeStatementReport: true, + isComputedForJournalReport: true, + isComputedForLedgerReport: true, + label: line.entryLabel, + }) + } + + const createEntryResponse = await getResponseBodyFromAPI({ + routeDefinition: createOneEntryFromTemplateRouteDefinition, + body: { + date: entry.date, + entryLines: entryLinesPayload, + idFile: null, + idJournal: journal.id, + idYear: props.idYear, + label: entry.label, + }, + }) + + if (createEntryResponse.ok === false) { + toast({ + title: `Impossible de créer l'écriture ${entry.label}`, + variant: "error", + }) + return + } + + createdEntriesCount += 1 + createdEntryLinesCount += entryLinesPayload.length + } + + await Promise.all([ + invalidateData({ + routeDefinition: readAllEntriesRouteDefinition, + body: { + idYear: props.idYear, + }, + }), + invalidateData({ + routeDefinition: readAllEntryLinesRouteDefinition, + body: { + idYear: props.idYear, + }, + }), + invalidateData({ + routeDefinition: readAllJournalsRouteDefinition, + body: { + idYear: props.idYear, + }, + }), + invalidateData({ + routeDefinition: readAllAccountsRouteDefinition, + body: { + idYear: props.idYear, + }, + }), + ]) + + toast({ + title: `${createdEntriesCount} écritures importées (${createdEntryLinesCount} mouvements, ${createdJournalsCount} journaux, ${createdAccountsCount} comptes créés)`, + variant: "success", + }) + + setSelectedFile(null) + props.onClose() + } + + return ( +
+

+ Importez un FEC pour créer automatiquement les journaux, comptes, écritures et mouvements manquants de + l'exercice. +

+

+ Nous avons créé également un outil de validation de conformité du FEC, disponible gratuitement en ligne + sur{" "} + + fec.arrhes.com + + . +

+ { + if (file === undefined) { + setSelectedFile(null) + return + } + setSelectedFile(file) + }} + /> +

+ Format supporté: fichier texte FEC avec séparateur tabulation ou "|". +

+

+ {selectedFileLabel} +

+ +

+ Le contrôle de compatibilité vérifie les colonnes requises et l'équilibre débit/crédit de chaque + écriture. +

+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/entries/ReverseOneEntry.tsx b/packages/website/src/features/dashboard/$idYear/entries/ReverseOneEntry.tsx new file mode 100644 index 00000000..8af98a01 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/entries/ReverseOneEntry.tsx @@ -0,0 +1,111 @@ +import { reverseOneEntryRouteDefinition } from "@arrhes/application-metadata" +import { + readAllEntriesRouteDefinition, + readAllEntryLinesRouteDefinition, + readAllEntryTagsRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, ButtonOutlineContent, ButtonPlainContent, Dialog, toast, useModalStore } from "@arrhes/ui" +import { type ComponentPropsWithRef, type ReactElement, useId } from "react" +import type * as v from "valibot" +import { applicationRouter } from "../../../../routes/applicationRouter.tsx" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../utilities/invalidateData.ts" + +export function ReverseOneEntry(props: { + entry: v.InferOutput + children: ReactElement> +}) { + const modalId = useId() + const { open: openModal, close: closeModal } = useModalStore() + + async function onSubmit() { + const reverseResponse = await getResponseBodyFromAPI({ + routeDefinition: reverseOneEntryRouteDefinition, + body: { + idEntry: props.entry.id, + idYear: props.entry.idYear, + }, + }) + + if (reverseResponse.ok === false) { + toast({ + title: "Erreur lors de l'extourne de l'écriture", + variant: "error", + }) + return + } + + await Promise.all([ + invalidateData({ + routeDefinition: readAllEntriesRouteDefinition, + body: { + idYear: props.entry.idYear, + }, + }), + invalidateData({ + routeDefinition: readAllEntryLinesRouteDefinition, + body: { + idYear: props.entry.idYear, + }, + }), + invalidateData({ + routeDefinition: readAllEntryTagsRouteDefinition, + body: { + idYear: props.entry.idYear, + }, + }), + ]) + + toast({ + title: "Écriture extournée", + variant: "success", + }) + + applicationRouter.navigate({ + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/écritures/$idEntry", + params: { + idOrganization: props.entry.idOrganization, + idYear: props.entry.idYear, + idEntry: reverseResponse.data.id, + }, + }) + } + + return ( + + + + , + ) + } + > + {props.children} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/entries/createOneEntry.tsx b/packages/website/src/features/dashboard/$idYear/entries/createOneEntry.tsx deleted file mode 100644 index 645fd5c4..00000000 --- a/packages/website/src/features/dashboard/$idYear/entries/createOneEntry.tsx +++ /dev/null @@ -1,319 +0,0 @@ -import { - addOneEntryTagRouteDefinition, - createOneEntryFromTemplateRouteDefinition, - readAllEntriesRouteDefinition, - readAllEntryTagsRouteDefinition, - readAllFilesRouteDefinition, - readAllJournalsRouteDefinition, - readAllTagsRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { InputComboboxMultiple, InputDate, InputSelect, InputText, toast } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconPlus } from "@tabler/icons-react" -import { type JSX, useState } from "react" -import { Fragment } from "react/jsx-runtime" -import type * as v from "valibot" -import { FormControl } from "../../../../components/forms/formControl.js" -import { FormError } from "../../../../components/forms/formError.js" -import { FormField } from "../../../../components/forms/formField.js" -import { FormGroup } from "../../../../components/forms/formGroup.js" -import { FormItem } from "../../../../components/forms/formItem.js" -import { FormLabel } from "../../../../components/forms/formLabel.js" -import { FormRoot } from "../../../../components/forms/formRoot.js" -import { InputDataCombobox } from "../../../../components/inputDataCombobox.js" -import { Drawer } from "../../../../components/overlays/drawer/drawer.js" -import { applicationRouter } from "../../../../routes/applicationRouter.js" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" -import { invalidateData } from "../../../../utilities/invalidateData.js" -import { useDataFromAPI } from "../../../../utilities/useHTTPData.js" -import { type EntryTemplateKey, entryTemplates } from "./entryTemplates/entryTemplates.js" - -export function CreateOneEntry(props: { - idOrganization: v.InferOutput["id"] - idYear: v.InferOutput["id"] - children: JSX.Element -}) { - const [open, setOpen] = useState(false) - const [selectedTemplate, setSelectedTemplate] = useState("empty") - const [isTemplateReady, setIsTemplateReady] = useState(false) - const [selectedTags, setSelectedTags] = useState< - Array<{ - key: string - label: string - }> - >([]) - - const tagsResponse = useDataFromAPI({ - routeDefinition: readAllTagsRouteDefinition, - body: { - idYear: props.idYear, - }, - }) - - const activeTemplate = entryTemplates.find((t) => t.key === selectedTemplate) - const isSubmitDisabled = activeTemplate?.hasActionButton === true && isTemplateReady === false - - return ( - { - setOpen(value) - if (value === false) { - setSelectedTemplate("empty") - setIsTemplateReady(false) - setSelectedTags([]) - } - }} - > - {props.children} - - - - , - text: "Ajouter l'écriture", - isDisabled: isSubmitDisabled, - }} - onSubmit={async (data) => { - const createEntryResponse = await getResponseBodyFromAPI({ - routeDefinition: createOneEntryFromTemplateRouteDefinition, - body: data, - }) - if (createEntryResponse.ok === false) { - toast({ - title: "Impossible d'ajouter l'écriture", - variant: "error", - }) - return false - } - - if (selectedTags.length > 0) { - await Promise.all( - selectedTags.map((tag) => - getResponseBodyFromAPI({ - routeDefinition: addOneEntryTagRouteDefinition, - body: { - idYear: props.idYear, - idEntry: createEntryResponse.data.id, - idTag: tag.key, - }, - }), - ), - ) - } - - toast({ - title: "Écriture ajoutée avec succès", - variant: "success", - }) - applicationRouter.navigate({ - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/écritures/$idEntry", - params: { - idOrganization: props.idOrganization, - idYear: props.idYear, - idEntry: createEntryResponse.data.id, - }, - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await Promise.all([ - invalidateData({ - routeDefinition: readAllEntriesRouteDefinition, - body: { - idYear: props.idYear, - }, - }), - invalidateData({ - routeDefinition: readAllEntryTagsRouteDefinition, - body: { - idYear: props.idYear, - }, - }), - ]) - - setOpen(false) - }} - > - {(form) => ( - - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - ({ - key: journal.id, - label: `(${journal.code}) ${journal.label}`, - })} - /> - - - - )} - /> - ( - - - - ({ - key: file.id, - label: file.reference ?? "", - })} - /> - - - - )} - /> - - - ({ - key: tag.id, - label: tag.label, - })) - } - selectedOptions={selectedTags} - onChange={setSelectedTags} - loading={tagsResponse.isPending} - /> - - - - - Choisir un modèle - - { - const newValue = value ?? "empty" - setSelectedTemplate(newValue) - setIsTemplateReady(false) - form.setValue("entryLines", []) - }} - options={entryTemplates.map((template) => ({ - key: template.key, - label: template.label, - }))} - placeholder="Sélectionner un modèle" - /> - - {activeTemplate === undefined || activeTemplate.key === "empty" ? null : ( -
- {activeTemplate?.formComponent({ - form, - idOrganization: props.idOrganization, - idYear: props.idYear, - onTemplateReadyChange: setIsTemplateReady, - })} -
- )} -
-
- )} -
-
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/entries/entriesPage.tsx b/packages/website/src/features/dashboard/$idYear/entries/entriesPage.tsx deleted file mode 100644 index 0ffecbd4..00000000 --- a/packages/website/src/features/dashboard/$idYear/entries/entriesPage.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import { Button, ButtonGhostContent, ButtonPlainContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconDotsVertical, IconDownload, IconFileExport, IconFileImport, IconPlus } from "@tabler/icons-react" -import { useParams } from "@tanstack/react-router" -import { Fragment, useState } from "react" -import { Page } from "../../../../components/layouts/page/page.js" -import { Popover } from "../../../../components/overlays/popover/popover.js" -import { entriesRoute } from "../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/entriesRoute.js" -import { YearDataWrapper } from "../yearDataWrapper.tsx" -import { CreateOneEntry } from "./createOneEntry.js" -import { EntriesTable } from "./entriesTable.js" -import { ExportEntryLines } from "./exportEntryLines.js" -import { ExportFecFile } from "./exportFecFile.js" -import { ImportFecFile } from "./importFecFile.js" - -export function EntriesPage() { - const params = useParams({ - from: entriesRoute.id, - }) - const [exportOpen, setExportOpen] = useState(false) - const [fecOpen, setFecOpen] = useState(false) - const [importFecOpen, setImportFecOpen] = useState(false) - - return ( - - - - {(data) => ( - -
- - - - - - - - - - - - - - - - - - - -
- - - - -
- )} -
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/entries/entriesTable.tsx b/packages/website/src/features/dashboard/$idYear/entries/entriesTable.tsx deleted file mode 100644 index e311fc19..00000000 --- a/packages/website/src/features/dashboard/$idYear/entries/entriesTable.tsx +++ /dev/null @@ -1,351 +0,0 @@ -import type { - readAllAccountsRouteDefinition, - readAllEntriesRouteDefinition, - readAllEntryLinesRouteDefinition, - readAllEntryTagsRouteDefinition, - readAllFilesRouteDefinition, - readAllJournalsRouteDefinition, - readAllTagsRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { FormatDate, FormatDateTime, FormatNull, FormatPrice, FormatText, LinkContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconPencil } from "@tabler/icons-react" -import { useMemo } from "react" -import type * as v from "valibot" -import { DataTable } from "../../../../components/layouts/dataTable.js" -import { LinkButton } from "../../../../components/linkButton.js" -import { EntriesTableSelectionActions } from "./entriesTableSelectionActions.js" - -export function EntriesTable(props: { - idOrganization: v.InferOutput["id"] - idYear: v.InferOutput["id"] - entries: v.InferOutput - entryLines: v.InferOutput - entryTags: v.InferOutput - journals: v.InferOutput - tags: v.InferOutput - files: v.InferOutput - accounts: v.InferOutput -}) { - const entriesData = useMemo( - () => - [ - ...props.entries, - ].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()), - [ - props.entries, - ], - ) - - const linesByEntry = useMemo(() => { - const map = new Map() - for (const row of props.entryLines) { - const existing = map.get(row.idEntry) - if (existing) { - existing.push(row) - } else { - map.set(row.idEntry, [ - row, - ]) - } - } - return map - }, [ - props.entryLines, - ]) - - const journalsMap = useMemo(() => { - const map = new Map() - for (const journal of props.journals) { - map.set(journal.id, journal) - } - return map - }, [ - props.journals, - ]) - - const tagsMap = useMemo(() => { - const map = new Map() - for (const tag of props.tags) { - map.set(tag.id, tag) - } - return map - }, [ - props.tags, - ]) - - const tagsByEntry = useMemo(() => { - const map = new Map() - for (const entryTag of props.entryTags) { - const existing = map.get(entryTag.idEntry) - if (existing) { - existing.push(entryTag.idTag) - } else { - map.set(entryTag.idEntry, [ - entryTag.idTag, - ]) - } - } - return map - }, [ - props.entryTags, - ]) - - const filesMap = useMemo(() => { - const map = new Map() - for (const file of props.files) { - map.set(file.id, file) - } - return map - }, [ - props.files, - ]) - - const accountsMap = useMemo(() => { - const map = new Map() - for (const account of props.accounts) { - map.set(account.id, account) - } - return map - }, [ - props.accounts, - ]) - - return ( - row.id} - selectionActions={(selectedRows) => ( - - )} - emptyStateProps={{ - icon: , - title: "Aucune écriture", - subtitle: "Les écritures de votre exercice apparaîtront ici.", - }} - columns={[ - { - accessorKey: "label", - header: "Libellé", - cell: ({ row }) => ( - - {row.original.label} - - ), - filterFn: "includesString", - }, - { - accessorKey: "date", - header: "Date", - cell: ({ row }) => , - filterFn: "includesString", - }, - { - accessorKey: "idJournal", - header: "Journal", - cell: ({ row }) => { - if (row.original.idJournal === null) return - const journal = journalsMap.get(row.original.idJournal) - if (!journal) return - return {journal.code} - }, - filterFn: "includesString", - }, - { - accessorKey: "id", - id: "tags", - header: "Catégorie", - cell: ({ row }) => { - const tagIds = tagsByEntry.get(row.original.id) - if (!tagIds || tagIds.length === 0) return - const tagLabels = tagIds - .map((id) => tagsMap.get(id)) - .filter((tag): tag is NonNullable => Boolean(tag)) - .map((tag) => tag.label) - if (tagLabels.length === 0) return - return {tagLabels.join(", ")} - }, - filterFn: "includesString", - }, - { - accessorKey: "idFile", - header: "Pièce justificative", - cell: ({ row }) => { - if (row.original.idFile === null) return - const file = filesMap.get(row.original.idFile) - if (!file) return - return {file.reference} - }, - filterFn: "includesString", - }, - { - accessorKey: "createdAt", - header: "Ajouté le", - cell: ({ row }) => , - filterFn: "includesString", - }, - { - accessorKey: "lastUpdatedAt", - header: "Dernière mise à jour le", - cell: ({ row }) => , - filterFn: "includesString", - }, - ]} - renderSubComponent={({ row }) => { - const rows = linesByEntry.get(row.original.id) - if (!rows || rows.length === 0) { - return ( - - ) - } - return ( - - - - - - - - - - - {rows.map((entryLine) => { - const account = accountsMap.get(entryLine.idAccount) - return ( - - - - - - - ) - })} - -
- Compte - - Libellé - - Débit - - Crédit -
- {account ? ( -
- - {account.number} - - - {account.label} - -
- ) : ( - - )} -
- {entryLine.label} - - - - -
- ) - }} - /> - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/entries/entriesTableSelectionActions.tsx b/packages/website/src/features/dashboard/$idYear/entries/entriesTableSelectionActions.tsx deleted file mode 100644 index 5e87272f..00000000 --- a/packages/website/src/features/dashboard/$idYear/entries/entriesTableSelectionActions.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { deleteOneEntryRouteDefinition, readAllEntriesRouteDefinition } from "@arrhes/application-metadata/routes" -import { Button, ButtonGhostContent, toast } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconChevronDown, IconTrash } from "@tabler/icons-react" -import type { Row } from "@tanstack/react-table" -import { useState } from "react" -import type * as v from "valibot" -import { ConfirmationModal } from "../../../../components/overlays/dialog/confirmationModal.js" -import { Popover } from "../../../../components/overlays/popover/popover.js" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" -import { invalidateData } from "../../../../utilities/invalidateData.js" - -type EntryRow = v.InferOutput[number] - -export function EntriesTableSelectionActions(props: { selectedRows: Array>; idYear: string }) { - const [deleteOpen, setDeleteOpen] = useState(false) - - async function handleDelete() { - const results = await Promise.all( - props.selectedRows.map((row) => - getResponseBodyFromAPI({ - routeDefinition: deleteOneEntryRouteDefinition, - body: { - idEntry: row.original.id, - idYear: props.idYear, - }, - }), - ), - ) - await invalidateData({ - routeDefinition: readAllEntriesRouteDefinition, - body: { - idYear: props.idYear, - }, - }) - if (results.some((r) => r.ok === false)) { - toast({ - title: "Certaines écritures n'ont pas pu être supprimées", - variant: "error", - }) - } else { - toast({ - title: "Écritures supprimées", - variant: "success", - }) - } - } - - return ( - <> - - - - - - - - - - - 1 ? "s" : "" - } ? Cette action est irréversible.`} - submitButtonProps={{ - text: "Supprimer", - leftIcon: , - color: "danger", - }} - onSubmit={handleDelete} - /> - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/entries/entryTemplates/AmortizationTemplateForm.tsx b/packages/website/src/features/dashboard/$idYear/entries/entryTemplates/AmortizationTemplateForm.tsx new file mode 100644 index 00000000..7f8e2047 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/entries/entryTemplates/AmortizationTemplateForm.tsx @@ -0,0 +1,329 @@ +import { amortizationTemplate, amortizationTemplateSchema } from "@arrhes/application-metadata/entryTemplates" +import { readAllAccountsRouteDefinition } from "@arrhes/application-metadata/routes" +import { Button, ButtonOutlineContent, InputPrice, InputText } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { valibotResolver } from "@hookform/resolvers/valibot" +import { IconCalculator } from "@tabler/icons-react" +import { useEffect, useRef } from "react" +import { FormProvider, useForm } from "react-hook-form" +import type * as v from "valibot" +import { FormControl } from "../../../../../components/forms/FormControl.tsx" +import { FormError } from "../../../../../components/forms/FormError.tsx" +import { FormField } from "../../../../../components/forms/FormField.tsx" +import { FormItem } from "../../../../../components/forms/FormItem.tsx" +import { FormLabel } from "../../../../../components/forms/FormLabel.tsx" +import { InputDataCombobox } from "../../../../../components/InputDataCombobox.tsx" +import { useDataFromAPI } from "../../../../../utilities/useHTTPData.ts" +import type { EntryTemplateFormProps } from "./entryTemplates.tsx" + +const DEFAULT_TOTAL_YEARS = "3" + +type AmortizationTemplateFormValues = v.InferOutput + +export function AmortizationTemplateForm(props: EntryTemplateFormProps) { + const { form: parentForm, onTemplateReadyChange } = props + + const templateForm = useForm({ + mode: "onSubmit", + criteriaMode: "all", + shouldFocusError: true, + defaultValues: { + assetLabel: "", + originalPrice: "", + currentYear: DEFAULT_TOTAL_YEARS, + totalYears: DEFAULT_TOTAL_YEARS, + idDotationAccount: "", + idAmortizationAccount: "", + }, + resolver: valibotResolver( + amortizationTemplateSchema, + ), + }) + + const accountsResponse = useDataFromAPI({ + routeDefinition: readAllAccountsRouteDefinition, + body: { + idYear: props.idYear, + }, + }) + + const accounts = accountsResponse.data ?? [] + + const defaultsAppliedRef = useRef(false) + + useEffect(() => { + if (defaultsAppliedRef.current || accounts.length === 0) return + defaultsAppliedRef.current = true + + const dotationAccount = accounts.find((account) => account.number === "68112") + const amortizationAccount = accounts.find((account) => account.number === "2818") + + if (dotationAccount) templateForm.setValue("idDotationAccount", dotationAccount.id) + if (amortizationAccount) templateForm.setValue("idAmortizationAccount", amortizationAccount.id) + }, [ + accounts, + templateForm.setValue, + ]) + + const isReadyRef = useRef(false) + + function invalidateRows() { + if (isReadyRef.current) { + isReadyRef.current = false + parentForm.setValue("entryLines", []) + onTemplateReadyChange(false) + } + } + + async function computeAndSetRows() { + const isValid = await templateForm.trigger() + if (!isValid) { + parentForm.setValue("entryLines", []) + isReadyRef.current = false + onTemplateReadyChange(false) + return + } + + const data = templateForm.getValues() + const result = amortizationTemplate.createEntries(data) + + parentForm.setValue("label", result.label) + parentForm.setValue("entryLines", result.entryLines) + isReadyRef.current = true + onTemplateReadyChange(true) + } + + return ( + +
+ ( + + + + { + const label = value ?? "" + field.onChange(label) + parentForm.setValue("label", `Dotation aux amortissements - ${label}`) + invalidateRows() + }} + placeholder="Ex : Ordinateur portable" + autoFocus={false} + /> + + + + )} + /> + ( + + + + { + field.onChange(value ?? "") + invalidateRows() + }} + /> + + + + )} + /> + ( + + + + { + field.onChange(value ?? "") + invalidateRows() + }} + placeholder="Ex : 2" + /> + + + + )} + /> + ( + + + + { + field.onChange(value ?? "") + invalidateRows() + }} + placeholder="Ex : 3" + /> + + + + )} + /> + ( + + + + { + field.onChange(value ?? "") + invalidateRows() + }} + routeDefinition={readAllAccountsRouteDefinition} + body={{ + idYear: props.idYear, + }} + placeholder="Ex : 68112" + getOption={(account) => ({ + key: account.id, + label: `${account.number} - ${account.label}`, + })} + /> + + + + )} + /> + ( + + + + { + field.onChange(value ?? "") + invalidateRows() + }} + routeDefinition={readAllAccountsRouteDefinition} + body={{ + idYear: props.idYear, + }} + placeholder="Ex : 2818" + getOption={(account) => ({ + key: account.id, + label: `${account.number} - ${account.label}`, + })} + /> + + + + )} + /> + + {parentForm.watch("entryLines")?.length > 0 ? ( +
+ + Mouvements générés + + {( + parentForm.watch("entryLines") as Array<{ + label?: string + debit?: string + credit?: string + }> + ).map((row, index) => ( +
+ + {row.label} + + + {Number(row.debit) > 0 ? `${row.debit} (débit)` : `${row.credit} (crédit)`} + +
+ ))} +
+ ) : null} +
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/entries/entryTemplates/amortizationTemplate.tsx b/packages/website/src/features/dashboard/$idYear/entries/entryTemplates/amortizationTemplate.tsx deleted file mode 100644 index 805849a7..00000000 --- a/packages/website/src/features/dashboard/$idYear/entries/entryTemplates/amortizationTemplate.tsx +++ /dev/null @@ -1,329 +0,0 @@ -import { amortizationTemplate, amortizationTemplateSchema } from "@arrhes/application-metadata/entryTemplates" -import { readAllAccountsRouteDefinition } from "@arrhes/application-metadata/routes" -import { Button, ButtonOutlineContent, InputPrice, InputText } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { valibotResolver } from "@hookform/resolvers/valibot" -import { IconCalculator } from "@tabler/icons-react" -import { useEffect, useRef } from "react" -import { FormProvider, useForm } from "react-hook-form" -import type * as v from "valibot" -import { FormControl } from "../../../../../components/forms/formControl.tsx" -import { FormError } from "../../../../../components/forms/formError.tsx" -import { FormField } from "../../../../../components/forms/formField.tsx" -import { FormItem } from "../../../../../components/forms/formItem.tsx" -import { FormLabel } from "../../../../../components/forms/formLabel.tsx" -import { InputDataCombobox } from "../../../../../components/inputDataCombobox.tsx" -import { useDataFromAPI } from "../../../../../utilities/useHTTPData.ts" -import type { EntryTemplateFormProps } from "./entryTemplates.tsx" - -const DEFAULT_TOTAL_YEARS = "3" - -type AmortizationTemplateFormValues = v.InferOutput - -export function AmortizationTemplateForm(props: EntryTemplateFormProps) { - const { form: parentForm, onTemplateReadyChange } = props - - const templateForm = useForm({ - mode: "onSubmit", - criteriaMode: "all", - shouldFocusError: true, - defaultValues: { - assetLabel: "", - originalPrice: "", - currentYear: DEFAULT_TOTAL_YEARS, - totalYears: DEFAULT_TOTAL_YEARS, - idDotationAccount: "", - idAmortizationAccount: "", - }, - resolver: valibotResolver( - amortizationTemplateSchema, - ), - }) - - const accountsResponse = useDataFromAPI({ - routeDefinition: readAllAccountsRouteDefinition, - body: { - idYear: props.idYear, - }, - }) - - const accounts = accountsResponse.data ?? [] - - const defaultsAppliedRef = useRef(false) - - useEffect(() => { - if (defaultsAppliedRef.current || accounts.length === 0) return - defaultsAppliedRef.current = true - - const dotationAccount = accounts.find((account) => account.number === "68112") - const amortizationAccount = accounts.find((account) => account.number === "2818") - - if (dotationAccount) templateForm.setValue("idDotationAccount", dotationAccount.id) - if (amortizationAccount) templateForm.setValue("idAmortizationAccount", amortizationAccount.id) - }, [ - accounts, - templateForm.setValue, - ]) - - const isReadyRef = useRef(false) - - function invalidateRows() { - if (isReadyRef.current) { - isReadyRef.current = false - parentForm.setValue("entryLines", []) - onTemplateReadyChange(false) - } - } - - async function computeAndSetRows() { - const isValid = await templateForm.trigger() - if (!isValid) { - parentForm.setValue("entryLines", []) - isReadyRef.current = false - onTemplateReadyChange(false) - return - } - - const data = templateForm.getValues() - const result = amortizationTemplate.createEntries(data) - - parentForm.setValue("label", result.label) - parentForm.setValue("entryLines", result.entryLines) - isReadyRef.current = true - onTemplateReadyChange(true) - } - - return ( - -
- ( - - - - { - const label = value ?? "" - field.onChange(label) - parentForm.setValue("label", `Dotation aux amortissements - ${label}`) - invalidateRows() - }} - placeholder="Ex : Ordinateur portable" - autoFocus={false} - /> - - - - )} - /> - ( - - - - { - field.onChange(value ?? "") - invalidateRows() - }} - /> - - - - )} - /> - ( - - - - { - field.onChange(value ?? "") - invalidateRows() - }} - placeholder="Ex : 2" - /> - - - - )} - /> - ( - - - - { - field.onChange(value ?? "") - invalidateRows() - }} - placeholder="Ex : 3" - /> - - - - )} - /> - ( - - - - { - field.onChange(value ?? "") - invalidateRows() - }} - routeDefinition={readAllAccountsRouteDefinition} - body={{ - idYear: props.idYear, - }} - placeholder="Ex : 68112" - getOption={(account) => ({ - key: account.id, - label: `${account.number} - ${account.label}`, - })} - /> - - - - )} - /> - ( - - - - { - field.onChange(value ?? "") - invalidateRows() - }} - routeDefinition={readAllAccountsRouteDefinition} - body={{ - idYear: props.idYear, - }} - placeholder="Ex : 2818" - getOption={(account) => ({ - key: account.id, - label: `${account.number} - ${account.label}`, - })} - /> - - - - )} - /> - - {parentForm.watch("entryLines")?.length > 0 ? ( -
- - Mouvements générés - - {( - parentForm.watch("entryLines") as Array<{ - label?: string - debit?: string - credit?: string - }> - ).map((row, index) => ( -
- - {row.label} - - - {Number(row.debit) > 0 ? `${row.debit} (débit)` : `${row.credit} (crédit)`} - -
- ))} -
- ) : null} -
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/entries/entryTemplates/entryTemplates.tsx b/packages/website/src/features/dashboard/$idYear/entries/entryTemplates/entryTemplates.tsx index 70d32a54..7e22c260 100644 --- a/packages/website/src/features/dashboard/$idYear/entries/entryTemplates/entryTemplates.tsx +++ b/packages/website/src/features/dashboard/$idYear/entries/entryTemplates/entryTemplates.tsx @@ -1,6 +1,6 @@ import type { ReactElement } from "react" import type { UseFormReturn } from "react-hook-form" -import { AmortizationTemplateForm } from "./amortizationTemplate.tsx" +import { AmortizationTemplateForm } from "./AmortizationTemplateForm.tsx" export type EntryTemplateKey = "empty" | "amortization" diff --git a/packages/website/src/features/dashboard/$idYear/entries/exportEntryLines.tsx b/packages/website/src/features/dashboard/$idYear/entries/exportEntryLines.tsx deleted file mode 100644 index 8b592d55..00000000 --- a/packages/website/src/features/dashboard/$idYear/entries/exportEntryLines.tsx +++ /dev/null @@ -1,316 +0,0 @@ -import { - readAllAccountsRouteDefinition, - type readAllEntriesRouteDefinition, - type readAllEntryLinesRouteDefinition, - readAllJournalsRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { formatDate, formatPrice, InputDate, toast } from "@arrhes/ui" -import { IconDownload } from "@tabler/icons-react" -import { useMemo } from "react" -import * as v from "valibot" -import { FormControl } from "../../../../components/forms/formControl.js" -import { FormError } from "../../../../components/forms/formError.js" -import { FormField } from "../../../../components/forms/formField.js" -import { FormGroup } from "../../../../components/forms/formGroup.js" -import { FormItem } from "../../../../components/forms/formItem.js" -import { FormLabel } from "../../../../components/forms/formLabel.js" -import { FormRoot } from "../../../../components/forms/formRoot.js" -import { InputDataCombobox } from "../../../../components/inputDataCombobox.js" -import { Drawer } from "../../../../components/overlays/drawer/drawer.js" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" - -export function ExportEntryLines(props: { - idOrganization: v.InferOutput["id"] - idYear: v.InferOutput["id"] - entries: v.InferOutput - entryLines: v.InferOutput - open: boolean - onOpenChange: (open: boolean) => void -}) { - const entriesMap = useMemo(() => { - return new Map( - props.entries.map((r) => [ - r.id, - r, - ]), - ) - }, [ - props.entries, - ]) - - function escapeCsvValue(value: string): string { - if (value.includes(";") || value.includes('"') || value.includes("\n")) { - return `"${value.replace(/"/g, '""')}"` - } - return value - } - - return ( - - - - - , - text: "Exporter en CSV", - }} - onSubmit={async (data) => { - const filteredRows = props.entryLines.filter((row) => { - const entry = entriesMap.get(row.idEntry) - if (!entry) return false - - if (data.idJournal && entry.idJournal !== data.idJournal) return false - if (data.idAccount && row.idAccount !== data.idAccount) return false - if (data.dateFrom && entry.date < data.dateFrom) return false - if (data.dateTo && entry.date > data.dateTo) return false - - return true - }) - - if (filteredRows.length === 0) { - toast({ - title: "Aucun mouvement à exporter", - variant: "warning", - }) - return false - } - - const accountsResponse = await getResponseBodyFromAPI({ - routeDefinition: readAllAccountsRouteDefinition, - body: { - idYear: props.idYear, - }, - }) - const journalsResponse = await getResponseBodyFromAPI({ - routeDefinition: readAllJournalsRouteDefinition, - body: { - idYear: props.idYear, - }, - }) - - if (!accountsResponse.ok || !journalsResponse.ok) { - toast({ - title: "Impossible de charger les données", - variant: "error", - }) - return false - } - - const accountsMap = new Map( - accountsResponse.data.map((a) => [ - a.id, - { - number: a.number, - label: a.label, - }, - ]), - ) - const journalsMap = new Map( - journalsResponse.data.map((j) => [ - j.id, - { - code: j.code, - label: j.label, - }, - ]), - ) - - const headers = [ - "Date", - "Code journal", - "Libellé journal", - "Libellé écriture", - "N° compte", - "Libellé compte", - "Libellé mouvement", - "Débit", - "Crédit", - ] - - const rows = filteredRows - .map((row) => { - const entry = entriesMap.get(row.idEntry) - if (!entry) return null - - const account = accountsMap.get(row.idAccount) - const journal = entry.idJournal ? journalsMap.get(entry.idJournal) : null - - return [ - formatDate(entry.date) ?? "", - journal?.code ?? "", - journal?.label ?? "", - entry.label, - account?.number ?? "", - account?.label ?? "", - row.label ?? "", - formatPrice({ - price: row.debit, - }), - formatPrice({ - price: row.credit, - }), - ].map(escapeCsvValue) - }) - .filter((row) => row !== null) - - const csvContent = [ - headers.map(escapeCsvValue).join(";"), - ...rows.map((r) => r.join(";")), - ].join("\n") - - const BOM = "\uFEFF" - const blob = new Blob( - [ - BOM + csvContent, - ], - { - type: "text/csv;charset=utf-8;", - }, - ) - const url = URL.createObjectURL(blob) - const link = document.createElement("a") - link.href = url - link.download = `ecritures-${new Date().toISOString().slice(0, 10)}.csv` - link.click() - URL.revokeObjectURL(url) - - toast({ - title: `${filteredRows.length} mouvements exportés`, - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - props.onOpenChange(false) - }} - > - {(form) => ( - - ( - - - - ({ - key: journal.id, - label: `(${journal.code}) ${journal.label ?? ""}`, - })} - /> - - - - )} - /> - ( - - - - ({ - key: account.id, - label: `${account.number} - ${account.label}`, - })} - /> - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - - )} - - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/entries/exportFecFile.tsx b/packages/website/src/features/dashboard/$idYear/entries/exportFecFile.tsx deleted file mode 100644 index 686a3380..00000000 --- a/packages/website/src/features/dashboard/$idYear/entries/exportFecFile.tsx +++ /dev/null @@ -1,345 +0,0 @@ -import { - readAllAccountsRouteDefinition, - type readAllEntriesRouteDefinition, - type readAllEntryLinesRouteDefinition, - readAllJournalsRouteDefinition, - readOneOrganizationRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { Button, ButtonPlainContent, LinkContent, toast } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconFileExport } from "@tabler/icons-react" -import { useEffect, useMemo, useState } from "react" -import type * as v from "valibot" -import { Banner } from "../../../../components/layouts/banner.js" -import { LinkButton } from "../../../../components/linkButton.js" -import { Drawer } from "../../../../components/overlays/drawer/drawer.js" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" - -/** - * Format a date string as YYYYMMDD for FEC compliance. - */ -function formatFecDate(rawDate: string | Date): string { - const date = new Date(rawDate) - const year = date.getFullYear() - const month = String(date.getMonth() + 1).padStart(2, "0") - const day = String(date.getDate()).padStart(2, "0") - return `${year}${month}${day}` -} - -/** - * Format a numeric amount for FEC: comma as decimal separator, no thousands separator, 2 decimals. - * e.g. 1234.5 -> "1234,50" - */ -function formatFecAmount(value: number | string): string { - const num = Number(value) - const abs = Math.abs(num) < 0.009 ? 0 : num - return abs.toFixed(2).replace(".", ",") -} - -const FEC_HEADERS = [ - "JournalCode", - "JournalLib", - "EcritureNum", - "EcritureDate", - "CompteNum", - "CompteLib", - "CompAuxNum", - "CompAuxLib", - "PieceRef", - "PieceDate", - "EcritureLib", - "Debit", - "Credit", - "EcritureLet", - "DateLet", - "ValidDate", - "Montantdevise", - "Idevise", -] as const - -export function ExportFecFile(props: { - idOrganization: v.InferOutput["id"] - idYear: v.InferOutput["id"] - entries: v.InferOutput - entryLines: v.InferOutput - open: boolean - onOpenChange: (open: boolean) => void -}) { - const [siren, setSiren] = useState(null) - - useEffect(() => { - if (!props.open) return - getResponseBodyFromAPI({ - routeDefinition: readOneOrganizationRouteDefinition, - body: { - idOrganization: props.idOrganization, - }, - }).then((response) => { - if (response.ok) { - setSiren(response.data.siren ?? "") - } - }) - }, [ - props.idOrganization, - props.open, - ]) - - const entriesMap = useMemo(() => { - return new Map( - props.entries.map((r) => [ - r.id, - r, - ]), - ) - }, [ - props.entries, - ]) - - async function handleExport() { - if (props.entryLines.length === 0) { - toast({ - title: "Aucun mouvement à exporter", - variant: "warning", - }) - return - } - - const [accountsResponse, journalsResponse] = await Promise.all([ - getResponseBodyFromAPI({ - routeDefinition: readAllAccountsRouteDefinition, - body: { - idYear: props.idYear, - }, - }), - getResponseBodyFromAPI({ - routeDefinition: readAllJournalsRouteDefinition, - body: { - idYear: props.idYear, - }, - }), - ]) - - if (!accountsResponse.ok || !journalsResponse.ok) { - toast({ - title: "Impossible de charger les données", - variant: "error", - }) - return - } - - const accountsMap = new Map( - accountsResponse.data.map((a) => [ - a.id, - { - number: a.number, - label: a.label, - }, - ]), - ) - const journalsMap = new Map( - journalsResponse.data.map((j) => [ - j.id, - { - code: j.code, - label: j.label, - }, - ]), - ) - - // Build a sequential EcritureNum per entry, sorted by date then id - const sortedEntries = [ - ...props.entries, - ].sort((a, b) => { - const dateCompare = a.date.localeCompare(b.date) - if (dateCompare !== 0) return dateCompare - return a.id.localeCompare(b.id) - }) - const ecritureNumMap = new Map() - for (let i = 0; i < sortedEntries.length; i++) { - ecritureNumMap.set(sortedEntries[i].id, String(i + 1)) - } - - // Build rows sorted by entry date, then EcritureNum, then row order - const sortedRows = [ - ...props.entryLines, - ].sort((a, b) => { - const entryA = entriesMap.get(a.idEntry) - const entryB = entriesMap.get(b.idEntry) - if (!entryA || !entryB) return 0 - const dateCompare = entryA.date.localeCompare(entryB.date) - if (dateCompare !== 0) return dateCompare - const numCompare = (ecritureNumMap.get(a.idEntry) ?? "").localeCompare( - ecritureNumMap.get(b.idEntry) ?? "", - undefined, - { - numeric: true, - }, - ) - if (numCompare !== 0) return numCompare - return a.id.localeCompare(b.id) - }) - - const rows = sortedRows - .map((row) => { - const entry = entriesMap.get(row.idEntry) - if (!entry) return null - - const account = accountsMap.get(row.idAccount) - const journal = entry.idJournal ? journalsMap.get(entry.idJournal) : null - - const ecritureDate = formatFecDate(entry.date) - const pieceDate = formatFecDate(entry.date) - const validDate = formatFecDate(entry.createdAt) - - return [ - journal?.code ?? "", - journal?.label ?? "", - ecritureNumMap.get(entry.id) ?? "", - ecritureDate, - account?.number ?? "", - account?.label ?? "", - "", // CompAuxNum - not supported - "", // CompAuxLib - not supported - entry.label, // PieceRef - pieceDate, - row.label ?? entry.label, // EcritureLib - formatFecAmount(row.debit), - formatFecAmount(row.credit), - "", // EcritureLet - not supported - "", // DateLet - not supported - validDate, - "", // Montantdevise - "", // Idevise - ].join("\t") - }) - .filter((row) => row !== null) - - const fecContent = [ - FEC_HEADERS.join("\t"), - ...rows, - ].join("\n") - - const BOM = "\uFEFF" - const blob = new Blob( - [ - BOM + fecContent, - ], - { - type: "text/plain;charset=utf-8;", - }, - ) - const url = URL.createObjectURL(blob) - const link = document.createElement("a") - link.href = url - link.download = `${siren ?? ""}FEC${formatFecDate(new Date())}.txt` - link.click() - URL.revokeObjectURL(url) - - toast({ - title: `${sortedRows.length} mouvements exportés au format FEC`, - variant: "success", - }) - props.onOpenChange(false) - } - - return ( - - - - -
-

- Le Fichier des Écritures Comptables (FEC) est un export normé de toutes les écritures de - l'exercice, au format requis par l'administration fiscale. -

-

- Nous avons créé également un outil de validation de conformité du FEC, disponible - gratuitement en ligne sur{" "} - - fec.arrhes.com - - . -

- {siren === "" && ( - - Le numéro SIREN de l'organisation n'est pas renseigné. Le nom du fichier FEC ne sera pas - conforme. Vous pouvez l'ajouter dans les{" "} - props.onOpenChange(false)} - > - - paramètres de l'organisation - - - . - - )} -

- {props.entries.length} écriture{props.entries.length > 1 ? "s" : ""} -{" "} - {props.entryLines.length} mouvement{props.entryLines.length > 1 ? "s" : ""} -

- -
-
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/entries/importFecFile.tsx b/packages/website/src/features/dashboard/$idYear/entries/importFecFile.tsx deleted file mode 100644 index 002d80ed..00000000 --- a/packages/website/src/features/dashboard/$idYear/entries/importFecFile.tsx +++ /dev/null @@ -1,543 +0,0 @@ -import { - createOneAccountRouteDefinition, - createOneEntryFromTemplateRouteDefinition, - createOneJournalRouteDefinition, - readAllAccountsRouteDefinition, - readAllEntriesRouteDefinition, - readAllEntryLinesRouteDefinition, - readAllJournalsRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { Button, ButtonPlainContent, InputFile, toast } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconFileImport } from "@tabler/icons-react" -import { useMemo, useState } from "react" -import type * as v from "valibot" -import { Drawer } from "../../../../components/overlays/drawer/drawer.js" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" -import { invalidateData } from "../../../../utilities/invalidateData.js" - -type FecLine = { - accountLabel: string - accountNumber: string - credit: number - debit: number - entryDate: string - entryLabel: string - journalCode: string - journalLabel: string -} - -type FecEntry = { - date: string - journalCode: string - journalLabel: string - key: string - label: string - lines: FecLine[] -} - -const REQUIRED_HEADERS = [ - "JournalCode", - "JournalLib", - "EcritureNum", - "EcritureDate", - "CompteNum", - "CompteLib", - "EcritureLib", - "Debit", - "Credit", -] as const - -function parseFecDate(rawValue: string, lineIndex: number): string { - const value = rawValue.trim() - if (/^\d{8}$/.test(value)) { - const year = value.slice(0, 4) - const month = value.slice(4, 6) - const day = value.slice(6, 8) - return `${year}-${month}-${day}T00:00:00.000Z` - } - - const date = new Date(value) - if (Number.isNaN(date.getTime())) { - throw new Error(`Date invalide sur la ligne ${lineIndex}`) - } - - return date.toISOString() -} - -function parseFecAmount(rawValue: string, lineIndex: number): number { - const normalized = rawValue.replaceAll(" ", "").replaceAll(",", ".").trim() - if (normalized === "") { - return 0 - } - - const parsed = Number(normalized) - if (Number.isNaN(parsed)) { - throw new Error(`Montant invalide sur la ligne ${lineIndex}`) - } - - return parsed -} - -function detectSeparator(headerLine: string): "\t" | "|" { - if (headerLine.includes("\t")) { - return "\t" - } - if (headerLine.includes("|")) { - return "|" - } - throw new Error("Séparateur FEC non reconnu (attendu: tabulation ou '|')") -} - -function getAccountType(accountNumber: string): "balance-sheet" | "income-statement" | "special" { - const firstDigit = accountNumber.trim().slice(0, 1) - if (firstDigit === "6" || firstDigit === "7") { - return "income-statement" - } - if (firstDigit === "8") { - return "special" - } - return "balance-sheet" -} - -function getAccountParentId(parameters: { - accountNumber: string - accountsByNumber: Map> -}): string | null { - const number = parameters.accountNumber.trim() - - for (let i = number.length - 1; i > 0; i--) { - const parentNumber = number.slice(0, i) - const parent = parameters.accountsByNumber.get(parentNumber) - if (parent !== undefined) { - return parent.id - } - } - - return null -} - -function parseFecFile(content: string): FecEntry[] { - const normalizedContent = content.replace(/^\uFEFF/, "") - const lines = normalizedContent - .split(/\r?\n/) - .map((line) => line.trimEnd()) - .filter((line) => line !== "") - - if (lines.length < 2) { - throw new Error("Le fichier FEC ne contient aucune écriture") - } - - const separator = detectSeparator(lines[0]) - const headers = lines[0].split(separator).map((value) => value.trim()) - const indexByHeader = new Map( - headers.map((header, index) => [ - header, - index, - ]), - ) - - for (const requiredHeader of REQUIRED_HEADERS) { - if (!indexByHeader.has(requiredHeader)) { - throw new Error(`Colonne manquante: ${requiredHeader}`) - } - } - - const entriesByKey = new Map() - - for (let rowIndex = 1; rowIndex < lines.length; rowIndex++) { - const line = lines[rowIndex] - const cells = line.split(separator) - const lineNumber = rowIndex + 1 - - const getCell = (header: string) => { - const index = indexByHeader.get(header) - if (index === undefined) { - return "" - } - return (cells[index] ?? "").trim() - } - - const journalCode = getCell("JournalCode").slice(0, 32) - const journalLabel = getCell("JournalLib").slice(0, 256) - const entryNumber = getCell("EcritureNum") - const entryDate = parseFecDate(getCell("EcritureDate"), lineNumber) - const accountNumber = getCell("CompteNum").slice(0, 32) - const accountLabel = getCell("CompteLib").slice(0, 256) - const pieceReference = getCell("PieceRef") - const entryLabel = (pieceReference || getCell("EcritureLib") || `Ecriture ${entryNumber}`).slice(0, 256) - const lineLabel = (getCell("EcritureLib") || entryLabel).slice(0, 256) - const debit = parseFecAmount(getCell("Debit"), lineNumber) - const credit = parseFecAmount(getCell("Credit"), lineNumber) - - if (journalCode === "" || entryNumber === "" || accountNumber === "") { - throw new Error(`Ligne ${lineNumber} invalide: champs obligatoires manquants`) - } - - if (debit < 0 || credit < 0) { - throw new Error(`Ligne ${lineNumber} invalide: montants négatifs non supportés`) - } - - const key = `${journalCode}__${entryNumber}__${entryDate}` - const existingEntry = entriesByKey.get(key) - const fecLine: FecLine = { - accountLabel, - accountNumber, - credit, - debit, - entryDate, - entryLabel: lineLabel, - journalCode, - journalLabel, - } - - if (existingEntry === undefined) { - entriesByKey.set(key, { - date: entryDate, - journalCode, - journalLabel, - key, - label: entryLabel, - lines: [ - fecLine, - ], - }) - continue - } - - existingEntry.lines.push(fecLine) - } - - const entries = [ - ...entriesByKey.values(), - ].sort((left, right) => { - const dateCompare = left.date.localeCompare(right.date) - if (dateCompare !== 0) { - return dateCompare - } - return left.key.localeCompare(right.key) - }) - - for (const entry of entries) { - const totalDebit = entry.lines.reduce((sum, line) => sum + line.debit, 0) - const totalCredit = entry.lines.reduce((sum, line) => sum + line.credit, 0) - const delta = Math.abs(totalDebit - totalCredit) - - if (delta > 0.01) { - throw new Error(`Ecriture non équilibrée: ${entry.label}`) - } - } - - return entries -} - -export function ImportFecFile(props: { - idYear: v.InferOutput["id"] - journals: v.InferOutput - accounts: v.InferOutput - open: boolean - onOpenChange: (open: boolean) => void -}) { - const [selectedFile, setSelectedFile] = useState(null) - - const selectedFileLabel = useMemo(() => { - if (selectedFile === null) { - return "Aucun fichier sélectionné" - } - return selectedFile.name - }, [ - selectedFile, - ]) - - async function handleImport() { - if (selectedFile === null) { - toast({ - title: "Sélectionnez un fichier FEC", - variant: "warning", - }) - return - } - - const fileContent = await selectedFile.text() - - let entries: FecEntry[] - try { - entries = parseFecFile(fileContent) - } catch (error) { - const message = error instanceof Error ? error.message : "Fichier FEC invalide" - toast({ - title: message, - variant: "error", - }) - return - } - - const journalsByCode = new Map( - props.journals.map((journal) => [ - journal.code, - journal, - ]), - ) - const accountsByNumber = new Map( - props.accounts.map((account) => [ - account.number, - account, - ]), - ) - - let createdJournalsCount = 0 - let createdAccountsCount = 0 - let createdEntriesCount = 0 - let createdEntryLinesCount = 0 - - for (const entry of entries) { - let journal = journalsByCode.get(entry.journalCode) - if (journal === undefined) { - const createJournalResponse = await getResponseBodyFromAPI({ - routeDefinition: createOneJournalRouteDefinition, - body: { - idYear: props.idYear, - code: entry.journalCode, - label: entry.journalLabel || entry.journalCode, - }, - }) - - if (createJournalResponse.ok === false) { - toast({ - title: `Impossible de créer le journal ${entry.journalCode}`, - variant: "error", - }) - return - } - - journal = createJournalResponse.data - journalsByCode.set(journal.code, journal) - createdJournalsCount += 1 - } - - const entryLinesPayload: Array<{ - credit: string - debit: string - idAccount: string - isComputedForBalanceReport: boolean - isComputedForBalanceSheetReport: boolean - isComputedForIncomeStatementReport: boolean - isComputedForJournalReport: boolean - isComputedForLedgerReport: boolean - label: string - }> = [] - - for (const line of entry.lines) { - let account = accountsByNumber.get(line.accountNumber) - if (account === undefined) { - const createAccountResponse = await getResponseBodyFromAPI({ - routeDefinition: createOneAccountRouteDefinition, - body: { - idYear: props.idYear, - idAccountParent: getAccountParentId({ - accountNumber: line.accountNumber, - accountsByNumber, - }), - isSelectable: true, - label: line.accountLabel || `Compte ${line.accountNumber}`, - number: line.accountNumber, - type: getAccountType(line.accountNumber), - }, - }) - - if (createAccountResponse.ok === false) { - toast({ - title: `Impossible de créer le compte ${line.accountNumber}`, - variant: "error", - }) - return - } - - account = createAccountResponse.data - accountsByNumber.set(account.number, account) - createdAccountsCount += 1 - } - - entryLinesPayload.push({ - credit: line.credit.toFixed(2), - debit: line.debit.toFixed(2), - idAccount: account.id, - isComputedForBalanceReport: true, - isComputedForBalanceSheetReport: true, - isComputedForIncomeStatementReport: true, - isComputedForJournalReport: true, - isComputedForLedgerReport: true, - label: line.entryLabel, - }) - } - - const createEntryResponse = await getResponseBodyFromAPI({ - routeDefinition: createOneEntryFromTemplateRouteDefinition, - body: { - date: entry.date, - entryLines: entryLinesPayload, - idFile: null, - idJournal: journal.id, - idYear: props.idYear, - label: entry.label, - }, - }) - - if (createEntryResponse.ok === false) { - toast({ - title: `Impossible de créer l'écriture ${entry.label}`, - variant: "error", - }) - return - } - - createdEntriesCount += 1 - createdEntryLinesCount += entryLinesPayload.length - } - - await Promise.all([ - invalidateData({ - routeDefinition: readAllEntriesRouteDefinition, - body: { - idYear: props.idYear, - }, - }), - invalidateData({ - routeDefinition: readAllEntryLinesRouteDefinition, - body: { - idYear: props.idYear, - }, - }), - invalidateData({ - routeDefinition: readAllJournalsRouteDefinition, - body: { - idYear: props.idYear, - }, - }), - invalidateData({ - routeDefinition: readAllAccountsRouteDefinition, - body: { - idYear: props.idYear, - }, - }), - ]) - - toast({ - title: `${createdEntriesCount} écritures importées (${createdEntryLinesCount} mouvements, ${createdJournalsCount} journaux, ${createdAccountsCount} comptes créés)`, - variant: "success", - }) - - setSelectedFile(null) - props.onOpenChange(false) - } - - return ( - { - props.onOpenChange(open) - if (open === false) { - setSelectedFile(null) - } - }} - > - - - -
-

- Importez un FEC pour créer automatiquement les journaux, comptes, écritures et mouvements - manquants de l'exercice. -

-

- Nous avons créé également un outil de validation de conformité du FEC, disponible - gratuitement en ligne sur{" "} - - fec.arrhes.com - - . -

- { - if (file === undefined) { - setSelectedFile(null) - return - } - setSelectedFile(file) - }} - /> -

- Format supporté: fichier texte FEC avec séparateur tabulation ou "|". -

-

- {selectedFileLabel} -

- -

- Le contrôle de compatibilité vérifie les colonnes requises et l'équilibre débit/crédit de - chaque écriture. -

-
-
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/entries/reverseOneEntry.tsx b/packages/website/src/features/dashboard/$idYear/entries/reverseOneEntry.tsx deleted file mode 100644 index 41cde234..00000000 --- a/packages/website/src/features/dashboard/$idYear/entries/reverseOneEntry.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { reverseOneEntryRouteDefinition } from "@arrhes/application-metadata" -import { - readAllEntriesRouteDefinition, - readAllEntryLinesRouteDefinition, - readAllEntryTagsRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { toast } from "@arrhes/ui" -import type { ComponentPropsWithRef, ReactElement } from "react" -import type * as v from "valibot" -import { ConfirmationModal } from "../../../../components/overlays/dialog/confirmationModal.tsx" -import { applicationRouter } from "../../../../routes/applicationRouter.tsx" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../utilities/invalidateData.ts" - -export function ReverseOneEntry(props: { - entry: v.InferOutput - children: ReactElement> -}) { - async function onSubmit() { - const reverseResponse = await getResponseBodyFromAPI({ - routeDefinition: reverseOneEntryRouteDefinition, - body: { - idEntry: props.entry.id, - idYear: props.entry.idYear, - }, - }) - - if (reverseResponse.ok === false) { - toast({ - title: "Erreur lors de l'extourne de l'écriture", - variant: "error", - }) - return - } - - await Promise.all([ - invalidateData({ - routeDefinition: readAllEntriesRouteDefinition, - body: { - idYear: props.entry.idYear, - }, - }), - invalidateData({ - routeDefinition: readAllEntryLinesRouteDefinition, - body: { - idYear: props.entry.idYear, - }, - }), - invalidateData({ - routeDefinition: readAllEntryTagsRouteDefinition, - body: { - idYear: props.entry.idYear, - }, - }), - ]) - - toast({ - title: "Écriture extournée", - variant: "success", - }) - - applicationRouter.navigate({ - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/écritures/$idEntry", - params: { - idOrganization: props.entry.idOrganization, - idYear: props.entry.idYear, - idEntry: reverseResponse.data.id, - }, - }) - } - - return ( - - Cette action créera une écriture d'extourne avec les mêmes mouvements inversés. -
- L'écriture originale ne sera pas modifiée. - - } - submitButtonProps={{ - text: "Extourner l'écriture", - }} - onSubmit={onSubmit} - > - {props.children} -
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/files/$idFile/DeleteOneFile.tsx b/packages/website/src/features/dashboard/$idYear/files/$idFile/DeleteOneFile.tsx new file mode 100644 index 00000000..907b09df --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/files/$idFile/DeleteOneFile.tsx @@ -0,0 +1,90 @@ +import { readAllFilesRouteDefinition } from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, ButtonOutlineContent, ButtonPlainContent, Dialog, toast, useModalStore } from "@arrhes/ui" +import { type ComponentPropsWithRef, type ReactElement, useId } from "react" +import type * as v from "valibot" +import { applicationRouter } from "../../../../../routes/applicationRouter.tsx" +import { invalidateData } from "../../../../../utilities/invalidateData.ts" +import { deleteFileWithSignedUrl } from "../deleteFileWithSignedUrl.ts" + +export function DeleteOneFile(props: { + file: v.InferOutput + children: ReactElement> +}) { + const modalId = useId() + const { open: openModal, close: closeModal } = useModalStore() + + async function onSubmit() { + const isDeleted = await deleteFileWithSignedUrl({ + idFile: props.file.id, + }) + + if (isDeleted === false) { + toast({ + title: "Erreur lors de la suppression du fichier", + variant: "error", + }) + return + } + + await invalidateData({ + routeDefinition: readAllFilesRouteDefinition, + body: {}, + }) + + toast({ + title: "Fichier supprimé", + variant: "success", + }) + + applicationRouter.navigate({ + to: "/dashboard/organisations/$idOrganization/stockage", + params: { + idOrganization: props.file.idOrganization, + }, + search: { + idFolder: undefined, + }, + }) + } + + return ( + + + + , + ) + } + > + {props.children} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/files/$idFile/FileData.tsx b/packages/website/src/features/dashboard/$idYear/files/$idFile/FileData.tsx new file mode 100644 index 00000000..8e6492c0 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/files/$idFile/FileData.tsx @@ -0,0 +1,30 @@ +import { readOneFileRouteDefinition } from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { CircularLoader, FormatError } from "@arrhes/ui" +import type { ReactElement } from "react" +import type * as v from "valibot" +import { useDataFromAPI } from "../../../../../utilities/useHTTPData.ts" + +export function FileData(props: { + idFile: v.InferOutput["id"] + idOrganization: v.InferOutput["id"] + children: ( + data: v.InferOutput, + ) => ReactElement | Array | null +}) { + const fileResponse = useDataFromAPI({ + routeDefinition: readOneFileRouteDefinition, + body: { + idFile: props.idFile, + }, + }) + + if (fileResponse.data === undefined) { + if (fileResponse.isPending) { + return + } + return + } + + return props.children(fileResponse.data) +} diff --git a/packages/website/src/features/dashboard/$idYear/files/$idFile/FileFile.tsx b/packages/website/src/features/dashboard/$idYear/files/$idFile/FileFile.tsx new file mode 100644 index 00000000..ce6ad129 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/files/$idFile/FileFile.tsx @@ -0,0 +1,120 @@ +import { generateFileGetSignedUrlRouteDefinition } from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { CircularLoader, FormatError } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { useEffect, useState } from "react" +import type * as v from "valibot" +import { useDataFromAPI } from "../../../../../utilities/useHTTPData.ts" + +export function FileFile(props: { file: v.InferOutput }) { + const [markdownContent, setMarkdownContent] = useState(null) + const [markdownLoading, setMarkdownLoading] = useState(false) + const [markdownError, setMarkdownError] = useState(null) + const fileSignedUrlResponse = useDataFromAPI({ + routeDefinition: generateFileGetSignedUrlRouteDefinition, + body: { + idFile: props.file.id, + }, + }) + + const isMarkdownFile = props.file.type?.startsWith("text/markdown") ?? false + + useEffect(() => { + if (!isMarkdownFile || !fileSignedUrlResponse.data?.url) { + return + } + + let canceled = false + + async function loadMarkdown() { + setMarkdownLoading(true) + setMarkdownError(null) + + try { + const response = await fetch(fileSignedUrlResponse.data!.url) + if (!response.ok) { + throw new Error("Impossible de récupérer le fichier markdown") + } + + const bytes = await response.arrayBuffer() + const text = new TextDecoder("utf-8").decode(bytes) + if (!canceled) { + setMarkdownContent(text) + } + } catch { + if (!canceled) { + setMarkdownError("Impossible d'afficher le contenu markdown.") + } + } finally { + if (!canceled) { + setMarkdownLoading(false) + } + } + } + + void loadMarkdown() + + return () => { + canceled = true + } + }, [ + isMarkdownFile, + fileSignedUrlResponse.data?.url, + ]) + + if (fileSignedUrlResponse.data === undefined) { + if (fileSignedUrlResponse.isPending) { + return + } + return + } + + if (isMarkdownFile) { + if (markdownLoading) { + return + } + + if (markdownError) { + return + } + + return ( +
+                {markdownContent ?? ""}
+            
+ ) + } + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/files/$idFile/FileLayout.tsx b/packages/website/src/features/dashboard/$idYear/files/$idFile/FileLayout.tsx new file mode 100644 index 00000000..7d991552 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/files/$idFile/FileLayout.tsx @@ -0,0 +1,117 @@ +import { ButtonOutlineContent, ButtonPlainContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconChevronLeft, IconDatabase, IconEye, IconInfoCircle, IconPencil, IconTrash } from "@tabler/icons-react" +import { Outlet, useParams } from "@tanstack/react-router" +import { LinkButton } from "../../../../../components/LinkButton.tsx" +import { Page } from "../../../../../components/layouts/page/page.tsx" +import { Section } from "../../../../../components/layouts/section/section.tsx" +import { Tab } from "../../../../../components/layouts/tab/tab.tsx" + +import { DeleteOneFile } from "./DeleteOneFile.tsx" +import { FileData } from "./FileData.tsx" +import { UpdateOneFile } from "./UpdateOneFile.tsx" + +export function FileLayout() { + const params = useParams({ + strict: false, + }) as { + idOrganization: string + idFile: string + } + + return ( + + + + {(file) => { + return ( + + +
+ + } + text="Retour" + /> + +
+
+ + } + text="Modifier" + /> + + + } + color="danger" + /> + +
+
+ + , + to: "/dashboard/organisations/$idOrganization/stockage/$idFile", + params: { + idOrganization: params.idOrganization, + idFile: params.idFile, + }, + }, + { + label: "Métadonnées", + icon: , + to: "/dashboard/organisations/$idOrganization/stockage/$idFile/métadonnées", + params: { + idOrganization: params.idOrganization, + idFile: params.idFile, + }, + }, + { + label: "Visualisation", + icon: , + to: "/dashboard/organisations/$idOrganization/stockage/$idFile/visualisation", + params: { + idOrganization: params.idOrganization, + idFile: params.idFile, + }, + }, + ]} + /> + + +
+ ) + }} +
+
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/files/$idFile/FileMetadataTab.tsx b/packages/website/src/features/dashboard/$idYear/files/$idFile/FileMetadataTab.tsx new file mode 100644 index 00000000..10265888 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/files/$idFile/FileMetadataTab.tsx @@ -0,0 +1,46 @@ +import { FormatDateTime, FormatText } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { useParams } from "@tanstack/react-router" +import { DataBlock } from "../../../../../components/layouts/dataBlock/dataBlock.tsx" +import { Section } from "../../../../../components/layouts/section/section.tsx" +import { FileData } from "./FileData.tsx" + +export function FileMetadataTab(props: { idOrganization?: string; idFile?: string } = {}) { + const params = useParams({ + strict: false, + }) + const idOrganization = props.idOrganization ?? params.idOrganization ?? "" + const idFile = props.idFile ?? params.idFile ?? "" + + return ( + + {(file) => { + return ( + + + + + + + + + + + + {file.id} + + + + + ) + }} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/files/$idFile/FilePage.tsx b/packages/website/src/features/dashboard/$idYear/files/$idFile/FilePage.tsx new file mode 100644 index 00000000..d973e0a6 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/files/$idFile/FilePage.tsx @@ -0,0 +1,54 @@ +import { FormatDateTime, FormatFileSize, FormatNull, FormatText } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { useParams } from "@tanstack/react-router" +import { DataBlock } from "../../../../../components/layouts/dataBlock/dataBlock.tsx" +import { Section } from "../../../../../components/layouts/section/section.tsx" +import { FileData } from "./FileData.tsx" + +export function FilePage(props: { idOrganization?: string; idFile?: string } = {}) { + const params = useParams({ + strict: false, + }) + const idOrganization = props.idOrganization ?? params.idOrganization ?? "" + const idFile = props.idFile ?? params.idFile ?? "" + + return ( + + {(file) => ( + + + + + + {file.reference} + + + {file.name} + + + {file.date !== null && file.date !== undefined ? ( + + ) : ( + + )} + + + {file.type?.split("/").at(1)} + + + + + + + + )} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/files/$idFile/FileTabContent.tsx b/packages/website/src/features/dashboard/$idYear/files/$idFile/FileTabContent.tsx new file mode 100644 index 00000000..096ae891 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/files/$idFile/FileTabContent.tsx @@ -0,0 +1,105 @@ +import { ButtonOutlineContent, ButtonPlainContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconDatabase, IconEye, IconInfoCircle, IconPencil, IconTrash } from "@tabler/icons-react" +import { Suspense } from "react" +import { Page } from "../../../../../components/layouts/page/page.tsx" +import { SubPageContent } from "../../../../../components/layouts/SubPageContent.tsx" +import { DeleteOneFile } from "./DeleteOneFile.tsx" +import { FileData } from "./FileData.tsx" +import { FileMetadataTab } from "./FileMetadataTab.tsx" +import { FilePage } from "./FilePage.tsx" +import { FileVisualisationTab } from "./FileVisualisationTab.tsx" +import { UpdateOneFile } from "./UpdateOneFile.tsx" + +export function FileTabContent(props: { idOrganization: string; idFile: string }) { + return ( + + + + {(file) => ( + <> +
+ +
+ } + text="Modifier" + /> +
+
+ +
+ } + title="Supprimer" + color="danger" + /> +
+
+
+ , + content: ( + + + + ), + }, + { + key: "métadonnées", + label: "Métadonnées", + icon: , + content: ( + + + + ), + }, + { + key: "visualisation", + label: "Visualisation", + icon: , + content: ( + + + + ), + }, + ], + }, + }} + /> + + )} +
+
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/files/$idFile/FileVisualisationTab.tsx b/packages/website/src/features/dashboard/$idYear/files/$idFile/FileVisualisationTab.tsx new file mode 100644 index 00000000..04d6616e --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/files/$idFile/FileVisualisationTab.tsx @@ -0,0 +1,32 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import { useParams } from "@tanstack/react-router" +import { Section } from "../../../../../components/layouts/section/section.tsx" +import { FileData } from "./FileData.tsx" +import { FileFile } from "./FileFile.tsx" + +export function FileVisualisationTab(props: { idOrganization?: string; idFile?: string } = {}) { + const params = useParams({ + strict: false, + }) + const idOrganization = props.idOrganization ?? params.idOrganization ?? "" + const idFile = props.idFile ?? params.idFile ?? "" + + return ( + + {(file) => { + return ( + + + + ) + }} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/files/$idFile/UpdateOneFile.tsx b/packages/website/src/features/dashboard/$idYear/files/$idFile/UpdateOneFile.tsx new file mode 100644 index 00000000..f645eb50 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/files/$idFile/UpdateOneFile.tsx @@ -0,0 +1,44 @@ +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import type { JSX } from "react" +import type * as v from "valibot" +import { useTabs } from "../../../../../contexts/tabs/useTabs.js" +import { UpdateOneFileForm } from "./UpdateOneFileForm.js" + +export function UpdateOneFile(props: { file: v.InferOutput; children: JSX.Element }) { + const { openPanelTab, closeTab } = useTabs() + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/files/$idFile/UpdateOneFileForm.tsx b/packages/website/src/features/dashboard/$idYear/files/$idFile/UpdateOneFileForm.tsx new file mode 100644 index 00000000..def9d57b --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/files/$idFile/UpdateOneFileForm.tsx @@ -0,0 +1,210 @@ +import { fileSchema } from "@arrhes/application-metadata/components" +import { + finalizeFileUploadRouteDefinition, + generateFilePutSignedUrlRouteDefinition, + readAllFilesRouteDefinition, + readOneFileRouteDefinition, + updateOneFileRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { InputDate, InputFile, InputText, toast } from "@arrhes/ui" +import { IconPencil } from "@tabler/icons-react" +import { Fragment } from "react/jsx-runtime" +import * as v from "valibot" +import { FormControl } from "../../../../../components/forms/FormControl.js" +import { FormError } from "../../../../../components/forms/FormError.js" +import { FormField } from "../../../../../components/forms/FormField.js" +import { FormItem } from "../../../../../components/forms/FormItem.js" +import { FormLabel } from "../../../../../components/forms/FormLabel.js" +import { FormRoot } from "../../../../../components/forms/FormRoot.js" +import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.js" +import { invalidateData } from "../../../../../utilities/invalidateData.js" + +export function UpdateOneFileForm(props: { file: v.InferOutput; onSuccess?: () => void }) { + return ( + , + text: "Modifier le fichier", + }} + onSubmit={async (data) => { + const updateFileResponse = await getResponseBodyFromAPI({ + routeDefinition: updateOneFileRouteDefinition, + body: { + idFile: props.file.id, + reference: data.reference, + name: data.name, + date: data.date, + }, + }) + if (updateFileResponse.ok === false) { + toast({ + title: "Impossible de modifier le fichier", + variant: "error", + }) + return false + } + + if (data.file !== undefined) { + const signedUrlResponse = await getResponseBodyFromAPI({ + routeDefinition: generateFilePutSignedUrlRouteDefinition, + body: { + idFile: updateFileResponse.data.id, + type: data.file.type, + size: data.file.size, + }, + }) + if (signedUrlResponse.ok === false) { + toast({ + title: "Impossible de télécharger le fichier", + variant: "error", + }) + return false + } + const uploadFileResponse = await fetch(signedUrlResponse.data.url, { + method: "PUT", + body: data.file, + }) + if (uploadFileResponse.ok === false) { + toast({ + title: "Le fichier ne peut pas être téléchargé", + variant: "error", + }) + return false + } + + const finalizeResponse = await getResponseBodyFromAPI({ + routeDefinition: finalizeFileUploadRouteDefinition, + body: { + idFile: updateFileResponse.data.id, + }, + }) + if (finalizeResponse.ok === false) { + toast({ + title: "Le téléversement du fichier a échoué", + variant: "error", + }) + return false + } + } + + toast({ + title: "Fichier modifié avec succès", + variant: "success", + }) + return true + }} + onCancel={undefined} + onSuccess={async () => { + await Promise.all([ + invalidateData({ + routeDefinition: readAllFilesRouteDefinition, + body: {}, + }), + invalidateData({ + routeDefinition: readOneFileRouteDefinition, + body: { + idFile: props.file.id, + }, + }), + ]) + + props.onSuccess?.() + }} + > + {(form) => ( + + ( + + + + + + + + )} + /> + ( + + + + + + + + )} + /> + ( + + + + + + + + )} + /> + ( + + + + + + + + )} + /> + + )} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/files/$idFile/deleteOneFile.tsx b/packages/website/src/features/dashboard/$idYear/files/$idFile/deleteOneFile.tsx deleted file mode 100644 index eaf17c7e..00000000 --- a/packages/website/src/features/dashboard/$idYear/files/$idFile/deleteOneFile.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { readAllFilesRouteDefinition } from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { toast } from "@arrhes/ui" -import type { ComponentPropsWithRef, ReactElement } from "react" -import type * as v from "valibot" -import { ConfirmationModal } from "../../../../../components/overlays/dialog/confirmationModal.tsx" -import { applicationRouter } from "../../../../../routes/applicationRouter.tsx" -import { invalidateData } from "../../../../../utilities/invalidateData.ts" -import { deleteFileWithSignedUrl } from "../deleteFileWithSignedUrl.ts" - -export function DeleteOneFile(props: { - file: v.InferOutput - children: ReactElement> -}) { - async function onSubmit() { - const isDeleted = await deleteFileWithSignedUrl({ - idFile: props.file.id, - idYear: props.file.idYear, - }) - - if (isDeleted === false) { - toast({ - title: "Erreur lors de la suppression du fichier", - variant: "error", - }) - return - } - - await invalidateData({ - routeDefinition: readAllFilesRouteDefinition, - body: { - idYear: props.file.idYear, - }, - }) - - toast({ - title: "Fichier supprimé", - variant: "success", - }) - - applicationRouter.navigate({ - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/stockage", - params: { - idOrganization: props.file.idOrganization, - idYear: props.file.idYear, - }, - search: { - idFolder: undefined, - }, - }) - } - - return ( - - Cette action supprimera le fichier et toutes les données associées. -
- Cette action est irréversible. - - } - submitButtonProps={{ - color: "danger", - text: "Supprimer le fichier", - }} - onSubmit={onSubmit} - > - {props.children} -
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/files/$idFile/fileData.tsx b/packages/website/src/features/dashboard/$idYear/files/$idFile/fileData.tsx deleted file mode 100644 index 997a0eb3..00000000 --- a/packages/website/src/features/dashboard/$idYear/files/$idFile/fileData.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { readOneFileRouteDefinition } from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { CircularLoader, FormatError } from "@arrhes/ui" -import type { ReactElement } from "react" -import type * as v from "valibot" -import { useDataFromAPI } from "../../../../../utilities/useHTTPData.ts" - -export function FileData(props: { - idFile: v.InferOutput["id"] - idOrganization: v.InferOutput["id"] - idYear: v.InferOutput["id"] - children: ( - data: v.InferOutput, - ) => ReactElement | Array | null -}) { - const fileResponse = useDataFromAPI({ - routeDefinition: readOneFileRouteDefinition, - body: { - idFile: props.idFile, - idYear: props.idYear, - }, - }) - - if (fileResponse.data === undefined) { - if (fileResponse.isPending) { - return - } - return - } - - return props.children(fileResponse.data) -} diff --git a/packages/website/src/features/dashboard/$idYear/files/$idFile/fileFile.tsx b/packages/website/src/features/dashboard/$idYear/files/$idFile/fileFile.tsx deleted file mode 100644 index 3d3181ef..00000000 --- a/packages/website/src/features/dashboard/$idYear/files/$idFile/fileFile.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { generateFileGetSignedUrlRouteDefinition } from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { CircularLoader, FormatError } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { useEffect, useState } from "react" -import type * as v from "valibot" -import { useDataFromAPI } from "../../../../../utilities/useHTTPData.ts" - -export function FileFile(props: { file: v.InferOutput }) { - const [markdownContent, setMarkdownContent] = useState(null) - const [markdownLoading, setMarkdownLoading] = useState(false) - const [markdownError, setMarkdownError] = useState(null) - const fileSignedUrlResponse = useDataFromAPI({ - routeDefinition: generateFileGetSignedUrlRouteDefinition, - body: { - idFile: props.file.id, - idYear: props.file.idYear, - }, - }) - - const isMarkdownFile = props.file.type?.startsWith("text/markdown") ?? false - - useEffect(() => { - if (!isMarkdownFile || !fileSignedUrlResponse.data?.url) { - return - } - - let canceled = false - - async function loadMarkdown() { - setMarkdownLoading(true) - setMarkdownError(null) - - try { - const response = await fetch(fileSignedUrlResponse.data!.url) - if (!response.ok) { - throw new Error("Impossible de récupérer le fichier markdown") - } - - const bytes = await response.arrayBuffer() - const text = new TextDecoder("utf-8").decode(bytes) - if (!canceled) { - setMarkdownContent(text) - } - } catch { - if (!canceled) { - setMarkdownError("Impossible d'afficher le contenu markdown.") - } - } finally { - if (!canceled) { - setMarkdownLoading(false) - } - } - } - - void loadMarkdown() - - return () => { - canceled = true - } - }, [ - isMarkdownFile, - fileSignedUrlResponse.data?.url, - ]) - - if (fileSignedUrlResponse.data === undefined) { - if (fileSignedUrlResponse.isPending) { - return - } - return - } - - if (isMarkdownFile) { - if (markdownLoading) { - return - } - - if (markdownError) { - return - } - - return ( -
-                {markdownContent ?? ""}
-            
- ) - } - - return ( - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/files/$idFile/fileLayout.tsx b/packages/website/src/features/dashboard/$idYear/files/$idFile/fileLayout.tsx deleted file mode 100644 index 1489960c..00000000 --- a/packages/website/src/features/dashboard/$idYear/files/$idFile/fileLayout.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { ButtonOutlineContent, ButtonPlainContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconChevronLeft, IconDatabase, IconEye, IconInfoCircle, IconPencil, IconTrash } from "@tabler/icons-react" -import { Outlet, useParams } from "@tanstack/react-router" -import { Page } from "../../../../../components/layouts/page/page.tsx" -import { Section } from "../../../../../components/layouts/section/section.tsx" -import { Tab } from "../../../../../components/layouts/tab/tab.tsx" -import { LinkButton } from "../../../../../components/linkButton.tsx" -import { fileLayoutRoute } from "../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/files/$idFile/fileLayoutRoute.tsx" -import { DeleteOneFile } from "./deleteOneFile.tsx" -import { FileData } from "./fileData.tsx" -import { UpdateOneFile } from "./updateOneFile.tsx" - -export function FileLayout() { - const params = useParams({ - from: fileLayoutRoute.id, - }) - - return ( - - - - {(file) => { - return ( - - -
- - } - text="Retour" - /> - -
-
- - } - text="Modifier" - /> - - - } - color="danger" - /> - -
-
- - , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/stockage/$idFile", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - idFile: params.idFile, - }, - }, - { - label: "Métadonnées", - icon: , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/stockage/$idFile/métadonnées", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - idFile: params.idFile, - }, - }, - { - label: "Visualisation", - icon: , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/stockage/$idFile/visualisation", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - idFile: params.idFile, - }, - }, - ]} - /> - - -
- ) - }} -
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/files/$idFile/fileMetadataTab.tsx b/packages/website/src/features/dashboard/$idYear/files/$idFile/fileMetadataTab.tsx deleted file mode 100644 index f5a58b46..00000000 --- a/packages/website/src/features/dashboard/$idYear/files/$idFile/fileMetadataTab.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { FormatDateTime, FormatText } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { useParams } from "@tanstack/react-router" -import { DataBlock } from "../../../../../components/layouts/dataBlock/dataBlock.tsx" -import { Section } from "../../../../../components/layouts/section/section.tsx" -import { fileLayoutRoute } from "../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/files/$idFile/fileLayoutRoute.tsx" -import { FileData } from "./fileData.tsx" - -export function FileMetadataTab() { - const params = useParams({ - from: fileLayoutRoute.id, - }) - - return ( - - {(file) => { - return ( - - - - - - - - - - - - {file.id} - - - - - ) - }} - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/files/$idFile/filePage.tsx b/packages/website/src/features/dashboard/$idYear/files/$idFile/filePage.tsx deleted file mode 100644 index df7b80d2..00000000 --- a/packages/website/src/features/dashboard/$idYear/files/$idFile/filePage.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { FormatFileSize, FormatText } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { useParams } from "@tanstack/react-router" -import { DataBlock } from "../../../../../components/layouts/dataBlock/dataBlock.tsx" -import { Section } from "../../../../../components/layouts/section/section.tsx" -import { fileLayoutRoute } from "../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/files/$idFile/fileLayoutRoute.tsx" -import { FileData } from "./fileData.tsx" - -export function FilePage() { - const params = useParams({ - from: fileLayoutRoute.id, - }) - - return ( - - {(file) => { - return ( - - - - - - {file.reference} - - - {file.name} - - - {file.type?.split("/").at(1)} - - - - - - - - ) - }} - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/files/$idFile/fileVisualisationTab.tsx b/packages/website/src/features/dashboard/$idYear/files/$idFile/fileVisualisationTab.tsx deleted file mode 100644 index 8aab9d90..00000000 --- a/packages/website/src/features/dashboard/$idYear/files/$idFile/fileVisualisationTab.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" -import { useParams } from "@tanstack/react-router" -import { Section } from "../../../../../components/layouts/section/section.tsx" -import { fileLayoutRoute } from "../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/files/$idFile/fileLayoutRoute.tsx" -import { FileData } from "./fileData.tsx" -import { FileFile } from "./fileFile.tsx" - -export function FileVisualisationTab() { - const params = useParams({ - from: fileLayoutRoute.id, - }) - - return ( - - {(file) => { - return ( - - - - ) - }} - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/files/$idFile/updateOneFile.tsx b/packages/website/src/features/dashboard/$idYear/files/$idFile/updateOneFile.tsx deleted file mode 100644 index 8a092a21..00000000 --- a/packages/website/src/features/dashboard/$idYear/files/$idFile/updateOneFile.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { type JSX, useState } from "react" -import type * as v from "valibot" -import { Drawer } from "../../../../../components/overlays/drawer/drawer.js" -import { UpdateOneFileForm } from "./updateOneFileForm.js" - -export function UpdateOneFile(props: { file: v.InferOutput; children: JSX.Element }) { - const [open, setOpen] = useState(false) - - return ( - - {props.children} - - - - setOpen(false)} - /> - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/files/$idFile/updateOneFileForm.tsx b/packages/website/src/features/dashboard/$idYear/files/$idFile/updateOneFileForm.tsx deleted file mode 100644 index c571c4d6..00000000 --- a/packages/website/src/features/dashboard/$idYear/files/$idFile/updateOneFileForm.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import { fileSchema } from "@arrhes/application-metadata/components" -import { - finalizeFileUploadRouteDefinition, - generateFilePutSignedUrlRouteDefinition, - readAllFilesRouteDefinition, - readOneFileRouteDefinition, - updateOneFileRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { InputFile, InputText, toast } from "@arrhes/ui" -import { IconPencil } from "@tabler/icons-react" -import { Fragment } from "react/jsx-runtime" -import * as v from "valibot" -import { FormControl } from "../../../../../components/forms/formControl.js" -import { FormError } from "../../../../../components/forms/formError.js" -import { FormField } from "../../../../../components/forms/formField.js" -import { FormItem } from "../../../../../components/forms/formItem.js" -import { FormLabel } from "../../../../../components/forms/formLabel.js" -import { FormRoot } from "../../../../../components/forms/formRoot.js" -import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.js" -import { invalidateData } from "../../../../../utilities/invalidateData.js" - -export function UpdateOneFileForm(props: { file: v.InferOutput; onSuccess?: () => void }) { - return ( - , - text: "Modifier le fichier", - }} - onSubmit={async (data) => { - const updateFileResponse = await getResponseBodyFromAPI({ - routeDefinition: updateOneFileRouteDefinition, - body: { - idFile: props.file.id, - idYear: data.idYear, - reference: data.reference, - name: data.name, - }, - }) - if (updateFileResponse.ok === false) { - toast({ - title: "Impossible de modifier le fichier", - variant: "error", - }) - return false - } - - if (data.file !== undefined) { - const signedUrlResponse = await getResponseBodyFromAPI({ - routeDefinition: generateFilePutSignedUrlRouteDefinition, - body: { - idFile: updateFileResponse.data.id, - type: data.file.type, - size: data.file.size, - }, - }) - if (signedUrlResponse.ok === false) { - toast({ - title: "Impossible de télécharger le fichier", - variant: "error", - }) - return false - } - const uploadFileResponse = await fetch(signedUrlResponse.data.url, { - method: "PUT", - body: data.file, - }) - if (uploadFileResponse.ok === false) { - toast({ - title: "Le fichier ne peut pas être téléchargé", - variant: "error", - }) - return false - } - - const finalizeResponse = await getResponseBodyFromAPI({ - routeDefinition: finalizeFileUploadRouteDefinition, - body: { - idFile: updateFileResponse.data.id, - }, - }) - if (finalizeResponse.ok === false) { - toast({ - title: "Le téléversement du fichier a échoué", - variant: "error", - }) - return false - } - } - - toast({ - title: "Fichier modifié avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await Promise.all([ - invalidateData({ - routeDefinition: readAllFilesRouteDefinition, - body: { - idYear: props.file.idYear, - }, - }), - invalidateData({ - routeDefinition: readOneFileRouteDefinition, - body: { - idFile: props.file.id, - idYear: props.file.idYear, - }, - }), - ]) - - props.onSuccess?.() - }} - > - {(form) => ( - - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - - )} - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/files/CreateOneFile.tsx b/packages/website/src/features/dashboard/$idYear/files/CreateOneFile.tsx new file mode 100644 index 00000000..a945b735 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/files/CreateOneFile.tsx @@ -0,0 +1,191 @@ +import { computeSHA256 } from "@arrhes/application-metadata" +import { + createOneFileRouteDefinition, + finalizeFileUploadRouteDefinition, + generateFilePutSignedUrlRouteDefinition, + readAllFilesRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, toast } from "@arrhes/ui" +import type { JSX } from "react" +import { useRef } from "react" +import type * as v from "valibot" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" +import { invalidateData } from "../../../../utilities/invalidateData.js" + +const MAX_FILE_SIZE = 1024 * 1024 * 50 // 50 MB + +/** + * Derive a human-readable reference from a file name by stripping the extension. + */ +function referenceFromFileName(name: string): string { + const dotIndex = name.lastIndexOf(".") + return dotIndex > 0 ? name.slice(0, dotIndex) : name +} + +async function uploadOneFile(params: { + idOrganization: string + idFolder?: string | null + file: File +}): Promise<"added" | "duplicate" | "error"> { + const { file, idOrganization: _idOrganization, idFolder } = params + + if (file.size > MAX_FILE_SIZE) { + toast({ + title: `"${file.name}" dépasse la taille maximale de 50 Mo`, + variant: "error", + }) + return "error" + } + + const hash = await computeSHA256(file) + + // Step 1 - create the database record (server deduplicates by hash) + const createResponse = await getResponseBodyFromAPI({ + routeDefinition: createOneFileRouteDefinition, + body: { + idFolder: idFolder ?? undefined, + reference: referenceFromFileName(file.name), + name: referenceFromFileName(file.name), + hash: hash, + }, + }) + if (createResponse.ok === false) { + toast({ + title: `Impossible de créer "${file.name}"`, + description: createResponse.error.message, + variant: "error", + }) + return "error" + } + + // If the file already exists in storage (deduplication), skip upload + if (createResponse.data.storageKey !== null) { + return "duplicate" + } + + // Step 2 - obtain a pre-signed PUT URL and update storage metadata + const signedUrlResponse = await getResponseBodyFromAPI({ + routeDefinition: generateFilePutSignedUrlRouteDefinition, + body: { + idFile: createResponse.data.id, + type: file.type, + size: file.size, + }, + }) + if (signedUrlResponse.ok === false) { + toast({ + title: `Impossible de télécharger "${file.name}"`, + description: signedUrlResponse.error.message, + variant: "error", + }) + return "error" + } + + // Step 3 - upload the binary directly to object storage + const uploadResponse = await fetch(signedUrlResponse.data.url, { + method: "PUT", + body: file, + }) + if (uploadResponse.ok === false) { + toast({ + title: `Échec du téléchargement de "${file.name}"`, + variant: "error", + }) + return "error" + } + + // Step 4 - finalize upload only after the object was successfully stored + const finalizeResponse = await getResponseBodyFromAPI({ + routeDefinition: finalizeFileUploadRouteDefinition, + body: { + idFile: createResponse.data.id, + }, + }) + if (finalizeResponse.ok === false) { + toast({ + title: `Téléversement incomplet pour "${file.name}"`, + variant: "error", + }) + return "error" + } + + return "added" +} + +export function CreateOneFile(props: { + idOrganization: v.InferOutput["id"] + idFolder?: string | null + children: JSX.Element +}) { + const inputRef = useRef(null) + + async function handleFiles(files: FileList) { + const fileArray = Array.from(files) + if (fileArray.length === 0) return + + const results = await Promise.all( + fileArray.map((file) => + uploadOneFile({ + idOrganization: props.idOrganization, + idFolder: props.idFolder, + file, + }), + ), + ) + + const added = results.filter((r) => r === "added").length + const duplicates = results.filter((r) => r === "duplicate").length + const failed = results.filter((r) => r === "error").length + + if (added > 0) { + await invalidateData({ + routeDefinition: readAllFilesRouteDefinition, + body: {}, + }) + } + + if (failed > 0) { + toast({ + title: `${added} fichier(s) ajouté(s), ${failed} en erreur`, + variant: "error", + }) + } else if (duplicates > 0 && added === 0) { + toast({ + title: duplicates === 1 ? "Ce fichier existe déjà" : `Ces ${duplicates} fichiers existent déjà`, + variant: "information", + }) + } else if (duplicates > 0) { + toast({ + title: `${added} fichier(s) ajouté(s), ${duplicates} déjà existant(s)`, + variant: "success", + }) + } else { + toast({ + title: added === 1 ? "Fichier ajouté avec succès" : `${added} fichiers ajoutés avec succès`, + variant: "success", + }) + } + } + + return ( + <> + { + if (event.target.files && event.target.files.length > 0) { + handleFiles(event.target.files) + } + // Reset so selecting the same file(s) again still triggers onChange + event.target.value = "" + }} + /> + + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/files/CreateOneFolder.tsx b/packages/website/src/features/dashboard/$idYear/files/CreateOneFolder.tsx new file mode 100644 index 00000000..bf915850 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/files/CreateOneFolder.tsx @@ -0,0 +1,112 @@ +import { createOneFolderRouteDefinition, readAllFoldersRouteDefinition } from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, ButtonOutlineContent, InputText, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconFolderPlus } from "@tabler/icons-react" +import type * as v from "valibot" +import { FormControl } from "../../../../components/forms/FormControl.js" +import { FormError } from "../../../../components/forms/FormError.js" +import { FormField } from "../../../../components/forms/FormField.js" +import { FormItem } from "../../../../components/forms/FormItem.js" +import { FormLabel } from "../../../../components/forms/FormLabel.js" +import { FormRoot } from "../../../../components/forms/FormRoot.js" +import { useTabs } from "../../../../contexts/tabs/useTabs.js" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" +import { invalidateData } from "../../../../utilities/invalidateData.js" + +export function CreateOneFolder(props: { + idOrganization: v.InferOutput["id"] + idFolderParent?: string | null +}) { + const { openPanelTab, closeTab } = useTabs() + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/files/FileActions.tsx b/packages/website/src/features/dashboard/$idYear/files/FileActions.tsx new file mode 100644 index 00000000..46c17602 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/files/FileActions.tsx @@ -0,0 +1,319 @@ +import { + ocrFileRouteDefinition, + readAllFilesRouteDefinition, + readOrganizationBillingRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { + Button, + ButtonGhostContent, + ButtonOutlineContent, + ButtonPlainContent, + Separator, + toast, + useModalStore, +} from "@arrhes/ui" +import { css } from "@arrhes/ui/css" +import { IconArrowsMove, IconDotsVertical, IconEye, IconFileText, IconPencil, IconTrash } from "@tabler/icons-react" +import { useId, useState } from "react" +import type * as v from "valibot" +import { Dialog } from "../../../../components/overlays/dialog/dialog.js" +import { Popover } from "../../../../components/overlays/popover/popover.js" +import { useTabs } from "../../../../contexts/tabs/useTabs.js" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" +import { invalidateData } from "../../../../utilities/invalidateData.js" +import { useDataFromAPI } from "../../../../utilities/useHTTPData.ts" +import { UpdateOneFileForm } from "./$idFile/UpdateOneFileForm.js" +import { deleteFileWithSignedUrl } from "./deleteFileWithSignedUrl.js" +import { MoveOneFileForm } from "./MoveOneFileForm.js" + +export function FileActions(props: { file: v.InferOutput; idOrganization: string }) { + const moveModalId = useId() + const deleteModalId = useId() + const { open: openModal, close: closeModal } = useModalStore() + const [ocrLoading, setOcrLoading] = useState(false) + const [ocrTooltipOpen, setOcrTooltipOpen] = useState(false) + const { openPanelTab, closeTab, openTab } = useTabs() + + const subscription = useDataFromAPI({ + routeDefinition: readOrganizationBillingRouteDefinition, + body: {}, + }) + const hasOcrAvailable = (subscription.data?.ocrPagesTotalAvailable ?? 0) > 0 + const isOcrSupportedType = props.file.type === "application/pdf" || (props.file.type?.startsWith("image/") ?? false) + + async function handleDelete() { + const isDeleted = await deleteFileWithSignedUrl({ + idFile: props.file.id, + }) + + if (isDeleted === false) { + toast({ + title: "Erreur lors de la suppression du fichier", + variant: "error", + }) + return + } + + await invalidateData({ + routeDefinition: readAllFilesRouteDefinition, + body: {}, + }) + + toast({ + title: "Fichier supprimé", + variant: "success", + }) + } + + async function handleOcr() { + setOcrLoading(true) + const ocrResponse = await getResponseBodyFromAPI({ + routeDefinition: ocrFileRouteDefinition, + body: { + idFile: props.file.id, + }, + hasToastMessage: true, + }) + setOcrLoading(false) + + if (ocrResponse.ok === false) { + return + } + + await invalidateData({ + routeDefinition: readAllFilesRouteDefinition, + body: {}, + }) + + toast({ + title: "Fichier converti en Markdown", + variant: "success", + }) + } + + return ( + + + + + + + + + + + + + + + {props.file.storageKey && isOcrSupportedType && ( +
{ + if (!hasOcrAvailable) { + setOcrTooltipOpen(true) + } + }} + onPointerLeave={() => setOcrTooltipOpen(false)} + > + + + + {!hasOcrAvailable && ocrTooltipOpen && ( +
+ Aucune page OCR disponible +
+ )} +
+ )} + + + + + + , + ) + } + > + } + text="Supprimer" + color="danger" + className={css({ + width: "100%", + justifyContent: "start", + })} + /> + + +
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/files/FileContextMenu.tsx b/packages/website/src/features/dashboard/$idYear/files/FileContextMenu.tsx new file mode 100644 index 00000000..0efad7b8 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/files/FileContextMenu.tsx @@ -0,0 +1,160 @@ +import { readAllFilesRouteDefinition } from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, ButtonOutlineContent, ButtonPlainContent, toast, useModalStore } from "@arrhes/ui" +import { css } from "@arrhes/ui/css" +import { IconArrowsMove, IconEye, IconPencil, IconTrash } from "@tabler/icons-react" +import { type ReactElement, useId } from "react" +import type * as v from "valibot" +import { ContextMenu } from "../../../../components/overlays/contextMenu/contextMenu.js" +import { Dialog } from "../../../../components/overlays/dialog/dialog.js" +import { useTabs } from "../../../../contexts/tabs/useTabs.js" +import { applicationRouter } from "../../../../routes/applicationRouter.js" +import { invalidateData } from "../../../../utilities/invalidateData.js" +import { UpdateOneFileForm } from "./$idFile/UpdateOneFileForm.js" +import { deleteFileWithSignedUrl } from "./deleteFileWithSignedUrl.js" +import { MoveOneFileForm } from "./MoveOneFileForm.js" + +export function FileContextMenu(props: { + file: v.InferOutput + idOrganization: string + children: ReactElement +}) { + const moveModalId = useId() + const deleteModalId = useId() + const { open: openModal, close: closeModal } = useModalStore() + const { openPanelTab, closeTab } = useTabs() + + async function handleDelete() { + const isDeleted = await deleteFileWithSignedUrl({ + idFile: props.file.id, + }) + + if (isDeleted === false) { + toast({ + title: "Erreur lors de la suppression du fichier", + variant: "error", + }) + return + } + + await invalidateData({ + routeDefinition: readAllFilesRouteDefinition, + body: {}, + }) + + toast({ + title: "Fichier supprimé", + variant: "success", + }) + } + + return ( + + {props.children} + + } + onSelect={() => { + applicationRouter.navigate({ + to: "/dashboard/organisations/$idOrganization/stockage/$idFile", + params: { + idOrganization: props.idOrganization, + idFile: props.file.id, + }, + }) + }} + > + Ouvrir + + } + onSelect={() => { + const r = { + current: "", + } + r.current = openPanelTab( + "Modifier le fichier", +
+ closeTab(r.current)} + /> +
, + ) + }} + > + Modifier +
+ } + onSelect={() => + openModal( + moveModalId, + + + Déplacer le fichier + + + closeModal(moveModalId)} + /> + + , + ) + } + > + Déplacer + + + } + color="danger" + onSelect={() => + openModal( + deleteModalId, + + + Voulez-vous supprimer ce fichier ? + + + + Cette action supprimera le fichier et toutes les données associées. Cette action + est irréversible. + + + + + + + , + ) + } + > + Supprimer + +
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/files/FileSelect.tsx b/packages/website/src/features/dashboard/$idYear/files/FileSelect.tsx new file mode 100644 index 00000000..7fb2dee7 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/files/FileSelect.tsx @@ -0,0 +1,37 @@ +import { readAllFilesRouteDefinition } from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { InputCombobox } from "@arrhes/ui" +import type * as v from "valibot" +import { useDataFromAPI } from "../../../../utilities/useHTTPData.ts" + +export function FileSelect(props: { + idOrganization: v.InferOutput["id"] + idYear: v.InferOutput["id"] + value?: string | null + onChange: (value?: string | null) => void +}) { + const filesResponse = useDataFromAPI({ + routeDefinition: readAllFilesRouteDefinition, + body: { + idYear: props.idYear, + }, + }) + + return ( + ({ + key: file.id, + label: file.reference ? `${file.name} (${file.reference})` : file.name, + })) + } + /> + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/files/FilesGrid.tsx b/packages/website/src/features/dashboard/$idYear/files/FilesGrid.tsx new file mode 100644 index 00000000..e704d30f --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/files/FilesGrid.tsx @@ -0,0 +1,642 @@ +import { + readAllFilesRouteDefinition, + readAllFoldersRouteDefinition, + updateOneFileRouteDefinition, + updateOneFolderRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { FormatDate, FormatFileSize, toast } from "@arrhes/ui" +import { css, cx } from "@arrhes/ui/utilities/cn.js" +import { IconArrowUp, IconFile, IconFileTypePdf, IconFolder, IconPhoto } from "@tabler/icons-react" +import { type DragEvent, useRef, useState } from "react" +import type * as v from "valibot" +import { EmptyState } from "../../../../components/layouts/EmptyState.js" +import { applicationRouter } from "../../../../routes/applicationRouter.js" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" +import { invalidateData } from "../../../../utilities/invalidateData.js" +import { FileContextMenu } from "./FileContextMenu.js" +import { FolderContextMenu } from "./FolderContextMenu.js" + +function getFileIcon(type: string | null) { + if (!type) return + if (type.startsWith("image/")) return + if (type === "application/pdf") return + return +} + +function getFileIconColor(type: string | null) { + if (!type) return "neutral/40" + if (type.startsWith("image/")) return "blue.500" + if (type === "application/pdf") return "red.500" + return "neutral/40" +} + +function getFileIconBg(type: string | null) { + if (!type) return "neutral/5" + if (type.startsWith("image/")) return "blue.50" + if (type === "application/pdf") return "red.50" + return "neutral/5" +} + +function getFileTypeLabel(type: string | null): string | null { + if (!type) return null + if (type.startsWith("image/")) return "Image" + if (type === "application/pdf") return "PDF" + if (type.startsWith("text/")) return "Texte" + if (type.includes("spreadsheet") || type.includes("excel")) return "Tableur" + if (type.includes("document") || type.includes("word")) return "Document" + if (type.includes("zip") || type.includes("archive") || type.includes("compressed")) return "Archive" + return null +} + +function getFileTypeBadgeColor(type: string | null): { + bg: string + text: string +} { + if (!type) + return { + bg: "neutral/8", + text: "neutral/50", + } + if (type.startsWith("image/")) + return { + bg: "blue.50", + text: "blue.600", + } + if (type === "application/pdf") + return { + bg: "red.50", + text: "red.600", + } + if (type.startsWith("text/")) + return { + bg: "green.50", + text: "green.600", + } + if (type.includes("spreadsheet") || type.includes("excel")) + return { + bg: "emerald.50", + text: "emerald.600", + } + if (type.includes("document") || type.includes("word")) + return { + bg: "indigo.50", + text: "indigo.600", + } + return { + bg: "neutral/8", + text: "neutral/50", + } +} + +const cardStyle = css({ + display: "flex", + flexDirection: "column", + alignItems: "center", + gap: "0", + borderRadius: "xl", + border: "1px solid", + borderColor: "neutral/10", + backgroundColor: "white", + cursor: "pointer", + transition: "all 0.2s ease", + overflow: "hidden", + _hover: { + borderColor: "primary/25", + boxShadow: "0 4px 12px rgba(0, 0, 0, 0.06)", + transform: "translateY(-2px)", + }, +}) + +type DragPayload = + | { + kind: "file" + id: string + sourceFolderId: string | null + } + | { + kind: "folder" + id: string + sourceParentFolderId: string | null + } + +export function FilesGrid(props: { + idOrganization: v.InferOutput["id"] + files: Array> + folders: Array> + currentFolderId: string | null + parentFolderId: string | null + onFolderOpen: (folderId: string | null) => void + hasActiveFilters?: boolean +}) { + const [dragOverFolderId, setDragOverFolderId] = useState(null) + const [draggingPayload, setDraggingPayload] = useState(null) + const draggingPayloadRef = useRef(null) + const suppressClickRef = useRef(false) + + const isEmpty = props.folders.length === 0 && props.files.length === 0 && props.currentFolderId === null + + function handleDragStart(event: DragEvent, payload: DragPayload) { + event.dataTransfer.setData("text/plain", JSON.stringify(payload)) + event.dataTransfer.effectAllowed = "move" + suppressClickRef.current = true + draggingPayloadRef.current = payload + setDraggingPayload(payload) + } + + function handleDragEnd() { + draggingPayloadRef.current = null + setDraggingPayload(null) + setDragOverFolderId(null) + setTimeout(() => { + suppressClickRef.current = false + }, 0) + } + + function getDragPayload(event: DragEvent): DragPayload | null { + try { + const rawPayload = event.dataTransfer.getData("text/plain") + if (!rawPayload) return null + const payload = JSON.parse(rawPayload) as DragPayload + if (payload.kind !== "file" && payload.kind !== "folder") return null + if (!payload.id) return null + return payload + } catch { + return null + } + } + + function canDropOnTarget(parameters: { payload: DragPayload; targetFolderId: string | null }) { + if (parameters.payload.kind === "file") { + return parameters.payload.sourceFolderId !== parameters.targetFolderId + } + + if (parameters.targetFolderId === parameters.payload.id) return false + return parameters.payload.sourceParentFolderId !== parameters.targetFolderId + } + + function handleDragOver( + event: DragEvent, + parameters: { + targetId: string + targetFolderId: string | null + }, + ) { + event.preventDefault() + event.dataTransfer.dropEffect = "move" + setDragOverFolderId(parameters.targetId) + } + + function handleDragLeave() { + setDragOverFolderId(null) + } + + async function handleDrop(event: DragEvent, folderId: string | null) { + event.preventDefault() + setDragOverFolderId(null) + + const payload = draggingPayloadRef.current ?? draggingPayload ?? getDragPayload(event) + if (!payload) return + if ( + !canDropOnTarget({ + payload, + targetFolderId: folderId, + }) + ) + return + + if (payload.kind === "file") { + const updateResponse = await getResponseBodyFromAPI({ + routeDefinition: updateOneFileRouteDefinition, + body: { + idFile: payload.id, + idFolder: folderId, + }, + }) + + if (updateResponse.ok === false) { + toast({ + title: "Impossible de déplacer le fichier", + variant: "error", + }) + return + } + + await invalidateData({ + routeDefinition: readAllFilesRouteDefinition, + body: {}, + }) + + toast({ + title: "Fichier déplacé", + variant: "success", + }) + return + } + + if (folderId === payload.id) { + return + } + + const updateResponse = await getResponseBodyFromAPI({ + routeDefinition: updateOneFolderRouteDefinition, + body: { + idFolder: payload.id, + idFolderParent: folderId, + }, + }) + + if (updateResponse.ok === false) { + toast({ + title: "Impossible de déplacer le dossier", + variant: "error", + }) + return + } + + await invalidateData({ + routeDefinition: readAllFoldersRouteDefinition, + body: {}, + }) + + toast({ + title: "Dossier déplacé", + variant: "success", + }) + } + + if (isEmpty) { + return ( +
+ } + title={props.hasActiveFilters ? "Aucun résultat" : "Aucun fichier"} + subtitle={props.hasActiveFilters ? undefined : "Ajoutez un fichier ou un dossier pour commencer"} + /> +
+ ) + } + + return ( +
+
+ {/* Back folder ("..") when inside a folder */} + {props.currentFolderId !== null && ( +
props.onFolderOpen(props.parentFolderId)} + onDragOver={(event) => + handleDragOver(event, { + targetId: "__parent__", + targetFolderId: props.parentFolderId, + }) + } + onDragLeave={handleDragLeave} + onDrop={(event) => handleDrop(event, props.parentFolderId)} + className={css({ + display: "flex", + flexDirection: "column", + alignItems: "center", + gap: "0", + borderRadius: "lg", + border: "2px dashed", + borderColor: "neutral/12", + backgroundColor: "neutral/2", + cursor: "pointer", + transition: "all 0.2s ease", + overflow: "hidden", + _hover: { + borderColor: "primary/25", + backgroundColor: "primary/3", + }, + })} + > +
+
+ +
+
+
+ + .. + +
+
+ )} + + {/* Folders */} + {props.folders.map((folder) => ( + +
+ handleDragStart(event, { + kind: "folder", + id: folder.id, + sourceParentFolderId: folder.idFolderParent ?? null, + }) + } + onDragEnd={handleDragEnd} + onClick={(event) => { + if (suppressClickRef.current) { + event.preventDefault() + event.stopPropagation() + return + } + props.onFolderOpen(folder.id) + }} + onDragOver={(event) => + handleDragOver(event, { + targetId: folder.id, + targetFolderId: folder.id, + }) + } + onDragLeave={handleDragLeave} + onDrop={(event) => handleDrop(event, folder.id)} + className={cx( + css({ + display: "flex", + flexDirection: "column", + alignItems: "center", + gap: "0", + borderRadius: "xl", + border: "1px solid", + borderColor: "amber.200", + backgroundColor: "white", + cursor: "pointer", + transition: "all 0.2s ease", + overflow: "hidden", + _hover: { + borderColor: "amber.300", + boxShadow: "0 4px 12px rgba(0, 0, 0, 0.06)", + transform: "translateY(-2px)", + }, + }), + dragOverFolderId === folder.id && + css({ + borderColor: "primary", + backgroundColor: "primary/5", + boxShadow: "0 4px 16px rgba(0, 0, 0, 0.1)", + }), + )} + > + {/* Folder icon area */} +
+
+ +
+
+ + {/* Folder info */} +
+ + {folder.name} + + + + +
+
+
+ ))} + + {/* Files */} + {props.files.map((file) => { + const typeLabel = getFileTypeLabel(file.type) + const badgeColor = getFileTypeBadgeColor(file.type) + + return ( + +
+ handleDragStart(event, { + kind: "file", + id: file.id, + sourceFolderId: file.idFolder ?? null, + }) + } + onDragEnd={handleDragEnd} + onClick={(event) => { + if (suppressClickRef.current) { + event.preventDefault() + event.stopPropagation() + return + } + applicationRouter.navigate({ + to: "/dashboard/organisations/$idOrganization/stockage/$idFile", + params: { + idOrganization: props.idOrganization, + idFile: file.id, + }, + }) + }} + className={cardStyle} + > + {/* File icon area */} +
+
+ {getFileIcon(file.type)} +
+ {/* File type badge */} + {typeLabel && ( + + {typeLabel} + + )} +
+ + {/* File info */} +
+ + {file.name} + + {file.reference && ( + + {file.reference} + + )} +
+ + {file.size && ( + <> + + · + + + + )} +
+
+
+
+ ) + })} +
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/files/FilesPage.tsx b/packages/website/src/features/dashboard/$idYear/files/FilesPage.tsx new file mode 100644 index 00000000..19002606 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/files/FilesPage.tsx @@ -0,0 +1,143 @@ +import { + readAllFilesRouteDefinition, + readAllFoldersRouteDefinition, + updateOneFileRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { ButtonPlainContent, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPlus } from "@tabler/icons-react" +import { useNavigate, useParams, useSearch } from "@tanstack/react-router" +import { type DragEvent, useState } from "react" +import { DataWrapper } from "../../../../components/layouts/DataWrapper.js" +import { Page } from "../../../../components/layouts/page/page.js" + +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" +import { invalidateData } from "../../../../utilities/invalidateData.js" +import { CreateOneFile } from "./CreateOneFile.js" +import { CreateOneFolder } from "./CreateOneFolder.js" +import { FilesPageContent } from "./FilesPageContent.js" + +export function FilesPage() { + const params = useParams({ + strict: false, + }) as { + idOrganization: string + } + const { idFolder } = useSearch({ + strict: false, + }) as { + idFolder?: string + } + const navigate = useNavigate() + + // Current folder is derived from the URL search param + const currentFolderId = idFolder ?? null + + function navigateToFolder(folderId: string | null) { + navigate({ + to: ".", + search: folderId + ? { + idFolder: folderId, + } + : {}, + }) + } + + // Breadcrumb drag-and-drop - move files to a specific breadcrumb folder (or root) + const [breadcrumbDragOver, setBreadcrumbDragOver] = useState(null) + + function handleBreadcrumbDragOver(event: DragEvent, targetId: string) { + event.preventDefault() + event.dataTransfer.dropEffect = "move" + setBreadcrumbDragOver(targetId) + } + + function handleBreadcrumbDragLeave() { + setBreadcrumbDragOver(null) + } + + async function handleBreadcrumbDrop(event: DragEvent, targetFolderId: string | null) { + event.preventDefault() + setBreadcrumbDragOver(null) + + const fileId = event.dataTransfer.getData("text/plain") + if (!fileId) return + + const updateResponse = await getResponseBodyFromAPI({ + routeDefinition: updateOneFileRouteDefinition, + body: { + idFile: fileId, + idFolder: targetFolderId, + }, + }) + + if (updateResponse.ok === false) { + toast({ + title: "Impossible de déplacer le fichier", + variant: "error", + }) + return + } + + await invalidateData({ + routeDefinition: readAllFilesRouteDefinition, + body: {}, + }) + + toast({ + title: "Fichier déplacé", + variant: "success", + }) + } + + return ( + + + {/* View toggle + actions */} +
+ + + } + text="Ajouter un fichier" + /> + +
+ + + {(folders) => ( + + )} + +
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/files/FilesPageContent.tsx b/packages/website/src/features/dashboard/$idYear/files/FilesPageContent.tsx new file mode 100644 index 00000000..855ce619 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/files/FilesPageContent.tsx @@ -0,0 +1,152 @@ +import { readAllFilesRouteDefinition, type readAllFoldersRouteDefinition } from "@arrhes/application-metadata/routes" +import { css } from "@arrhes/ui/utilities/cn.js" +import { type DragEvent, useMemo, useState } from "react" +import type * as v from "valibot" +import { DataWrapper } from "../../../../components/layouts/DataWrapper.js" +import { FilesGrid } from "./FilesGrid.js" +import { FilesTable } from "./FilesTable.js" + +type ViewMode = "grid" | "list" + +type Folder = v.InferOutput[number] + +/** + * Build the breadcrumb path from the root to the target folder by + * walking `idFolderParent` links upward, then reversing. + */ +function buildFolderPath( + folders: Array, + targetId: string | undefined, +): Array<{ + id: string + name: string +}> { + if (!targetId) return [] + + const map = new Map( + folders.map((f) => [ + f.id, + f, + ]), + ) + const path: Array<{ + id: string + name: string + }> = [] + let current = map.get(targetId) + + while (current) { + path.push({ + id: current.id, + name: current.name, + }) + current = current.idFolderParent ? map.get(current.idFolderParent) : undefined + } + + return path.reverse() +} + +/** + * Inner component that has access to the fetched folders data, + * allowing us to compute the breadcrumb path. + */ +export function FilesPageContent(props: { + folders: Array + idFolder: string | undefined + currentFolderId: string | null + navigateToFolder: (folderId: string | null) => void + breadcrumbDragOver: string | null + handleBreadcrumbDragOver: (event: DragEvent, targetId: string) => void + handleBreadcrumbDragLeave: () => void + handleBreadcrumbDrop: (event: DragEvent, targetFolderId: string | null) => void + params: { + idOrganization: string + } +}) { + const { + folders, + idFolder, + currentFolderId, + navigateToFolder, + breadcrumbDragOver: _breadcrumbDragOver, + handleBreadcrumbDragOver: _handleBreadcrumbDragOver, + handleBreadcrumbDragLeave: _handleBreadcrumbDragLeave, + handleBreadcrumbDrop: _handleBreadcrumbDrop, + params, + } = props + const [viewMode, _setViewMode] = useState("list") + + const _folderPath = useMemo( + () => buildFolderPath(folders, idFolder), + [ + folders, + idFolder, + ], + ) + + const currentFolders = folders.filter((f) => (f.idFolderParent ?? null) === currentFolderId) + + // Compute the parent folder ID so that ".." navigation works + const parentFolderId = useMemo(() => { + if (!currentFolderId) return null + const currentFolder = folders.find((f) => f.id === currentFolderId) + return currentFolder?.idFolderParent ?? null + }, [ + folders, + currentFolderId, + ]) + + const sortedFolders = useMemo( + () => + [ + ...currentFolders, + ].sort((a, b) => a.name.localeCompare(b.name)), + [ + currentFolders, + ], + ) + + return ( +
+ + {(files) => { + const currentFiles = files.filter((f) => (f.idFolder ?? null) === currentFolderId) + + if (viewMode === "grid") { + return ( + + ) + } + return ( + + ) + }} + +
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/files/FilesTable.tsx b/packages/website/src/features/dashboard/$idYear/files/FilesTable.tsx new file mode 100644 index 00000000..a576145e --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/files/FilesTable.tsx @@ -0,0 +1,533 @@ +import { + readAllFilesRouteDefinition, + readAllFoldersRouteDefinition, + updateOneFileRouteDefinition, + updateOneFolderRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, ButtonGhostContent, FormatDateTime, FormatFileSize, FormatNull, toast } from "@arrhes/ui" +import { css, cx } from "@arrhes/ui/utilities/cn.js" +import { IconArrowLeft, IconFile, IconFileTypePdf, IconFolder } from "@tabler/icons-react" +import { type DragEvent, type MouseEvent, type ReactElement, useEffect, useRef, useState } from "react" +import type * as v from "valibot" +import { DataTable } from "../../../../components/layouts/DataTable.js" +import { applicationRouter } from "../../../../routes/applicationRouter.js" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" +import { invalidateData } from "../../../../utilities/invalidateData.js" +import { FileActions } from "./FileActions.js" +import type { TableRow } from "./FilesTableSelectionActions.js" +import { FilesTableSelectionActions } from "./FilesTableSelectionActions.js" +import { FolderActions } from "./FolderActions.js" + +type DragPayload = + | { + kind: "file" + id: string + sourceFolderId: string | null + } + | { + kind: "folder" + id: string + sourceParentFolderId: string | null + } + +export function FilesTable(props: { + idOrganization: v.InferOutput["id"] + files: Array> + folders: Array> + currentFolderId: string | null + parentFolderId: string | null + onFolderOpen: (folderId: string | null) => void + hasActiveFilters?: boolean +}) { + const [dragOverTargetId, setDragOverTargetId] = useState(null) + const [draggingPayload, setDraggingPayload] = useState(null) + const [longPressReadyId, setLongPressReadyId] = useState(null) + const draggingPayloadRef = useRef(null) + const suppressClickRef = useRef(false) + // Long-press-to-drag: 300ms hold activates draggable on the row + const LONG_PRESS_MS = 300 + const longPressTimerRef = useRef | null>(null) + const pendingDragRef = useRef<{ + row: HTMLTableRowElement + payload: DragPayload + id: string + } | null>(null) + + function startLongPress(event: MouseEvent, payload: DragPayload, id: string) { + if (event.button !== 0) return + const row = event.currentTarget + if (longPressTimerRef.current !== null) clearTimeout(longPressTimerRef.current) + pendingDragRef.current = { + row, + payload, + id, + } + longPressTimerRef.current = setTimeout(() => { + if (pendingDragRef.current) { + pendingDragRef.current.row.draggable = true + pendingDragRef.current.row.style.cursor = "grab" + setLongPressReadyId(pendingDragRef.current.id) + } + }, LONG_PRESS_MS) + } + + function cancelLongPress() { + if (longPressTimerRef.current !== null) { + clearTimeout(longPressTimerRef.current) + longPressTimerRef.current = null + } + // Only reset if no drag is in progress + if (pendingDragRef.current && draggingPayloadRef.current === null) { + pendingDragRef.current.row.draggable = false + pendingDragRef.current.row.style.cursor = "" + pendingDragRef.current = null + setLongPressReadyId(null) + } + } + + function handleRowDragStart(event: DragEvent) { + const pending = pendingDragRef.current + if (!pending) { + event.preventDefault() + return + } + event.dataTransfer.setData("text/plain", JSON.stringify(pending.payload)) + event.dataTransfer.effectAllowed = "move" + const row = event.currentTarget + const rect = row.getBoundingClientRect() + event.dataTransfer.setDragImage(row, event.clientX - rect.left, event.clientY - rect.top) + suppressClickRef.current = true + draggingPayloadRef.current = pending.payload + setTimeout(() => { + if (draggingPayloadRef.current !== null) { + setDraggingPayload(draggingPayloadRef.current) + } + }, 0) + } + + function handleDragEnd() { + if (pendingDragRef.current) { + pendingDragRef.current.row.draggable = false + pendingDragRef.current.row.style.cursor = "" + pendingDragRef.current = null + } + if (longPressTimerRef.current !== null) { + clearTimeout(longPressTimerRef.current) + longPressTimerRef.current = null + } + draggingPayloadRef.current = null + setDraggingPayload(null) + setDragOverTargetId(null) + setLongPressReadyId(null) + setTimeout(() => { + suppressClickRef.current = false + }, 0) + } + + // Window-level fallback: if the element-level onDragEnd is missed (e.g. the DOM + // node was replaced by a re-render mid-drag), this guarantees state is cleaned up. + useEffect(() => { + const onWindowDragEnd = () => { + if (draggingPayloadRef.current === null) return + if (pendingDragRef.current) { + pendingDragRef.current.row.draggable = false + pendingDragRef.current.row.style.cursor = "" + pendingDragRef.current = null + } + draggingPayloadRef.current = null + setDraggingPayload(null) + setDragOverTargetId(null) + setLongPressReadyId(null) + setTimeout(() => { + suppressClickRef.current = false + }, 0) + } + window.addEventListener("dragend", onWindowDragEnd) + return () => window.removeEventListener("dragend", onWindowDragEnd) + }, []) + + function getDragPayload(event: DragEvent): DragPayload | null { + try { + const rawPayload = event.dataTransfer.getData("text/plain") + if (!rawPayload) return null + const payload = JSON.parse(rawPayload) as DragPayload + if (payload.kind !== "file" && payload.kind !== "folder") return null + if (!payload.id) return null + return payload + } catch { + return null + } + } + + function canDropOnTarget(parameters: { payload: DragPayload; targetFolderId: string | null }) { + if (parameters.payload.kind === "file") { + return parameters.payload.sourceFolderId !== parameters.targetFolderId + } + + if (parameters.targetFolderId === parameters.payload.id) return false + return parameters.payload.sourceParentFolderId !== parameters.targetFolderId + } + + function getRowInteractionProps(item: TableRow) { + const dropTargetProps = (() => { + if (item.kind === "file") return {} + const targetId = item.kind === "back" ? "back" : item.data.id + const targetFolderId = item.kind === "back" ? props.parentFolderId : item.data.id + return { + onDragOver: (event: DragEvent) => { + event.preventDefault() + event.dataTransfer.dropEffect = "move" + setDragOverTargetId(targetId) + }, + onDragLeave: (event: DragEvent) => { + if ((event.currentTarget as HTMLElement).contains(event.relatedTarget as Node)) return + setDragOverTargetId(null) + }, + onDrop: (event: DragEvent) => handleDrop(event, targetFolderId), + } + })() + + const dragSourceProps = (() => { + if (item.kind === "back") return {} + const id = item.kind === "folder" ? item.data.id : item.data.id + const payload: DragPayload = + item.kind === "folder" + ? { + kind: "folder", + id: item.data.id, + sourceParentFolderId: item.data.idFolderParent ?? null, + } + : { + kind: "file", + id: item.data.id, + sourceFolderId: item.data.idFolder ?? null, + } + return { + onMouseDown: (event: MouseEvent) => startLongPress(event, payload, id), + onMouseUp: cancelLongPress, + onMouseLeave: cancelLongPress, + onDragStart: (event: DragEvent) => handleRowDragStart(event), + onDragEnd: () => handleDragEnd(), + } + })() + + const itemId = item.kind !== "back" ? item.data.id : "back" + const isDropTarget = + dragOverTargetId !== null && + ((item.kind === "back" && dragOverTargetId === "back") || + (item.kind === "folder" && dragOverTargetId === item.data.id)) + + const isDraggingThis = + draggingPayload !== null && + ((item.kind === "folder" && draggingPayload.kind === "folder" && draggingPayload.id === item.data.id) || + (item.kind === "file" && draggingPayload.kind === "file" && draggingPayload.id === item.data.id)) + + const isLongPressReady = longPressReadyId === itemId + + return { + ...dropTargetProps, + ...dragSourceProps, + className: cx( + isDropTarget + ? css({ + backgroundColor: "primary/6", + }) + : undefined, + isDraggingThis + ? css({ + opacity: "0.4", + }) + : undefined, + isLongPressReady + ? css({ + outline: "2px solid", + outlineColor: "primary/40", + outlineOffset: "-2px", + borderRadius: "sm", + backgroundColor: "primary/4", + }) + : undefined, + ), + } + } + + async function handleDrop(event: DragEvent, targetFolderId: string | null) { + event.preventDefault() + setDragOverTargetId(null) + + const payload = draggingPayloadRef.current ?? draggingPayload ?? getDragPayload(event) + if (!payload) return + if ( + !canDropOnTarget({ + payload, + targetFolderId, + }) + ) { + handleDragEnd() + return + } + + if (payload.kind === "file") { + const updateResponse = await getResponseBodyFromAPI({ + routeDefinition: updateOneFileRouteDefinition, + body: { + idFile: payload.id, + idFolder: targetFolderId, + }, + }) + + if (updateResponse.ok === false) { + toast({ + title: "Impossible de déplacer le fichier", + variant: "error", + }) + return + } + + await invalidateData({ + routeDefinition: readAllFilesRouteDefinition, + body: {}, + }) + + toast({ + title: "Fichier déplacé", + variant: "success", + }) + return + } + + if (targetFolderId === payload.id) { + return + } + + const updateResponse = await getResponseBodyFromAPI({ + routeDefinition: updateOneFolderRouteDefinition, + body: { + idFolder: payload.id, + idFolderParent: targetFolderId, + }, + }) + + if (updateResponse.ok === false) { + toast({ + title: "Impossible de déplacer le dossier", + variant: "error", + }) + return + } + + await invalidateData({ + routeDefinition: readAllFoldersRouteDefinition, + body: {}, + }) + + toast({ + title: "Dossier déplacé", + variant: "success", + }) + } + + const rows: Array = [ + ...(props.currentFolderId !== null + ? [ + { + kind: "back" as const, + }, + ] + : []), + ...props.folders.map((folder) => ({ + kind: "folder" as const, + data: folder, + })), + ...props.files.map((file) => ({ + kind: "file" as const, + data: file, + })), + ] + + const icons: Record = { + "application/pdf": , + } + + return ( + getRowInteractionProps(row.original)} + hideSearchBar={false} + enableRowSelection={(row) => row.original.kind !== "back"} + getRowId={(row) => (row.kind === "back" ? "__back__" : row.data.id)} + resetSelectionTrigger={props.currentFolderId} + selectionActions={(selectedRows) => } + emptyStateProps={{ + icon: , + title: "Aucun fichier", + subtitle: "Les fichiers de votre exercice apparaîtront ici.", + }} + columns={[ + { + id: "name", + accessorFn: (row) => (row.kind === "back" ? ".." : (row.data.name ?? "")), + header: "Nom", + cell: ({ row }) => { + const item = row.original + if (item.kind === "back") { + return ( +
props.onFolderOpen(props.parentFolderId)} + className={css({ + width: "fit-content", + maxWidth: "100%", + cursor: "pointer", + })} + > + } + text=".." + /> +
+ ) + } + if (item.kind === "folder") { + return ( +
+ +
+ ) + } + if (item.kind === "file") { + const leftIcon = item.data.type !== null ? icons[item.data.type] : undefined + + return ( +
+ +
+ ) + } + }, + filterFn: "includesString", + }, + { + id: "size", + accessorFn: (row) => (row.kind === "file" ? (row.data.size ?? "") : ""), + header: "Size", + cell: ({ row }) => { + const item = row.original + if (item.kind === "back" || item.kind === "folder") return + if (item.kind === "file") return + }, + filterFn: "includesString", + }, + { + id: "createdAt", + accessorFn: (row) => (row.kind === "back" ? "" : row.data.createdAt), + header: "Date", + cell: ({ row }) => { + const item = row.original + if (item.kind === "back") + return ( + + -- + + ) + if (item.kind === "folder") return + return + }, + filterFn: "includesString", + }, + { + id: "actions", + header: " ", + cell: ({ row }) => { + const item = row.original + if (item.kind === "back") return null + if (item.kind === "folder") { + return ( + + ) + } + return ( + + ) + }, + enableSorting: false, + enableGlobalFilter: false, + }, + ]} + /> + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/files/FilesTableSelectionActions.tsx b/packages/website/src/features/dashboard/$idYear/files/FilesTableSelectionActions.tsx new file mode 100644 index 00000000..820ab688 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/files/FilesTableSelectionActions.tsx @@ -0,0 +1,182 @@ +import type { returnedSchemas } from "@arrhes/application-metadata" +import { + deleteOneFolderRouteDefinition, + readAllFilesRouteDefinition, + readAllFoldersRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { + Button, + ButtonGhostContent, + ButtonOutlineContent, + ButtonPlainContent, + Dialog, + toast, + useModalStore, +} from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconChevronDown, IconTrash } from "@tabler/icons-react" +import type { Row } from "@tanstack/react-table" +import { useId } from "react" +import type * as v from "valibot" +import { Popover } from "../../../../components/overlays/popover/popover.js" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" +import { invalidateData } from "../../../../utilities/invalidateData.js" +import { deleteFileWithSignedUrl } from "./deleteFileWithSignedUrl.js" + +export type TableRow = + | { + kind: "back" + } + | { + kind: "folder" + data: v.InferOutput + } + | { + kind: "file" + data: v.InferOutput + } + +export function FilesTableSelectionActions(props: { selectedRows: Array> }) { + const deleteModalId = useId() + const { open: openModal, close: closeModal } = useModalStore() + const selectedFiles = props.selectedRows + .filter((r) => r.original.kind === "file") + .map( + (r) => + ( + r.original as Extract< + TableRow, + { + kind: "file" + } + > + ).data, + ) + const selectedFolders = props.selectedRows + .filter((r) => r.original.kind === "folder") + .map( + (r) => + ( + r.original as Extract< + TableRow, + { + kind: "folder" + } + > + ).data, + ) + + async function handleDelete() { + const results = await Promise.all([ + ...selectedFiles.map((file) => + deleteFileWithSignedUrl({ + idFile: file.id, + }).then((ok) => ({ + ok, + })), + ), + ...selectedFolders.map((folder) => + getResponseBodyFromAPI({ + routeDefinition: deleteOneFolderRouteDefinition, + body: { + idFolder: folder.id, + }, + }), + ), + ]) + await Promise.all([ + invalidateData({ + routeDefinition: readAllFilesRouteDefinition, + body: {}, + }), + invalidateData({ + routeDefinition: readAllFoldersRouteDefinition, + body: {}, + }), + ]) + if (results.some((r) => r.ok === false)) { + toast({ + title: "Certains éléments n'ont pas pu être supprimés", + variant: "error", + }) + } else { + toast({ + title: "Éléments supprimés", + variant: "success", + }) + } + } + + return ( + + + + + + + + + + , + ) + } + > + } + text="Supprimer" + color="danger" + className={css({ + width: "100%", + justifyContent: "start", + })} + /> + + + + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/files/FolderActions.tsx b/packages/website/src/features/dashboard/$idYear/files/FolderActions.tsx new file mode 100644 index 00000000..0ec24cc0 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/files/FolderActions.tsx @@ -0,0 +1,219 @@ +import { deleteOneFolderRouteDefinition, readAllFoldersRouteDefinition } from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { + Button, + ButtonGhostContent, + ButtonOutlineContent, + ButtonPlainContent, + Separator, + toast, + useModalStore, +} from "@arrhes/ui" +import { css } from "@arrhes/ui/css" +import { IconArrowsMove, IconDotsVertical, IconEye, IconPencil, IconTrash } from "@tabler/icons-react" +import { useId } from "react" +import type * as v from "valibot" +import { Dialog } from "../../../../components/overlays/dialog/dialog.js" +import { Popover } from "../../../../components/overlays/popover/popover.js" +import { useTabs } from "../../../../contexts/tabs/useTabs.js" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" +import { invalidateData } from "../../../../utilities/invalidateData.js" +import { MoveOneFolderForm } from "./MoveOneFolderForm.js" +import { UpdateOneFolderForm } from "./UpdateOneFolderForm.js" + +export function FolderActions(props: { + folder: v.InferOutput + idOrganization: string + onFolderOpen: (folderId: string | null) => void +}) { + const moveModalId = useId() + const deleteModalId = useId() + const { open: openModal, close: closeModal } = useModalStore() + const { openPanelTab, closeTab } = useTabs() + + async function handleDelete() { + const deleteResponse = await getResponseBodyFromAPI({ + routeDefinition: deleteOneFolderRouteDefinition, + body: { + idFolder: props.folder.id, + }, + }) + + if (deleteResponse.ok === false) { + toast({ + title: "Erreur lors de la suppression du dossier", + variant: "error", + }) + return + } + + await invalidateData({ + routeDefinition: readAllFoldersRouteDefinition, + body: {}, + }) + + toast({ + title: "Dossier supprimé", + variant: "success", + }) + } + + return ( + + + + + + + + + + + + + + + + + + + + , + ) + } + > + } + text="Supprimer" + color="danger" + className={css({ + width: "100%", + justifyContent: "start", + })} + /> + + + + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/files/FolderContextMenu.tsx b/packages/website/src/features/dashboard/$idYear/files/FolderContextMenu.tsx new file mode 100644 index 00000000..6b02ee7e --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/files/FolderContextMenu.tsx @@ -0,0 +1,148 @@ +import { deleteOneFolderRouteDefinition, readAllFoldersRouteDefinition } from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, ButtonOutlineContent, ButtonPlainContent, toast, useModalStore } from "@arrhes/ui" +import { css } from "@arrhes/ui/css" +import { IconArrowsMove, IconPencil, IconTrash } from "@tabler/icons-react" +import { type ReactElement, useId } from "react" +import type * as v from "valibot" +import { ContextMenu } from "../../../../components/overlays/contextMenu/contextMenu.js" +import { Dialog } from "../../../../components/overlays/dialog/dialog.js" +import { useTabs } from "../../../../contexts/tabs/useTabs.js" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" +import { invalidateData } from "../../../../utilities/invalidateData.js" +import { MoveOneFolderForm } from "./MoveOneFolderForm.js" +import { UpdateOneFolderForm } from "./UpdateOneFolderForm.js" + +export function FolderContextMenu(props: { + folder: v.InferOutput + idOrganization: string + children: ReactElement +}) { + const moveModalId = useId() + const deleteModalId = useId() + const { open: openModal, close: closeModal } = useModalStore() + const { openPanelTab, closeTab } = useTabs() + + async function handleDelete() { + const deleteResponse = await getResponseBodyFromAPI({ + routeDefinition: deleteOneFolderRouteDefinition, + body: { + idFolder: props.folder.id, + }, + }) + + if (deleteResponse.ok === false) { + toast({ + title: "Erreur lors de la suppression du dossier", + variant: "error", + }) + return + } + + await invalidateData({ + routeDefinition: readAllFoldersRouteDefinition, + body: {}, + }) + + toast({ + title: "Dossier supprimé", + variant: "success", + }) + } + + return ( + + {props.children} + + } + onSelect={() => { + const r = { + current: "", + } + r.current = openPanelTab( + "Renommer le dossier", +
+ closeTab(r.current)} + /> +
, + ) + }} + > + Renommer +
+ } + onSelect={() => + openModal( + moveModalId, + + + Déplacer le dossier + + + closeModal(moveModalId)} + /> + + , + ) + } + > + Déplacer + + + } + color="danger" + onSelect={() => + openModal( + deleteModalId, + + + Voulez-vous supprimer ce dossier ? + + + + Cette action supprimera le dossier et tous ses sous-dossiers. Les fichiers + contenus ne seront pas supprimés. Cette action est irréversible. + + + + + + + , + ) + } + > + Supprimer + +
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/files/MoveOneFileForm.tsx b/packages/website/src/features/dashboard/$idYear/files/MoveOneFileForm.tsx new file mode 100644 index 00000000..afc96d10 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/files/MoveOneFileForm.tsx @@ -0,0 +1,120 @@ +import { + readAllFilesRouteDefinition, + readAllFoldersRouteDefinition, + updateOneFileRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { InputSelect, toast } from "@arrhes/ui" +import { IconArrowsMove } from "@tabler/icons-react" +import { useMemo } from "react" +import type * as v from "valibot" +import { FormControl } from "../../../../components/forms/FormControl.js" +import { FormError } from "../../../../components/forms/FormError.js" +import { FormField } from "../../../../components/forms/FormField.js" +import { FormItem } from "../../../../components/forms/FormItem.js" +import { FormLabel } from "../../../../components/forms/FormLabel.js" +import { FormRoot } from "../../../../components/forms/FormRoot.js" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" +import { invalidateData } from "../../../../utilities/invalidateData.js" +import { useDataFromAPI } from "../../../../utilities/useHTTPData.ts" + +const rootOptionKey = "__root__" + +export function MoveOneFileForm(props: { file: v.InferOutput; onSuccess?: () => void }) { + const foldersResponse = useDataFromAPI({ + routeDefinition: readAllFoldersRouteDefinition, + body: {}, + }) + + const folderOptions = useMemo( + () => [ + { + key: rootOptionKey, + label: "/", + }, + ...(foldersResponse.data ?? []).map((folder) => ({ + key: folder.id, + label: folder.name, + })), + ], + [ + foldersResponse.data, + ], + ) + + return ( + , + text: "Déplacer le fichier", + }} + onSubmit={async (data) => { + const updateResponse = await getResponseBodyFromAPI({ + routeDefinition: updateOneFileRouteDefinition, + body: { + idFile: props.file.id, + idFolder: data.idFolder, + }, + }) + + if (updateResponse.ok === false) { + toast({ + title: "Impossible de déplacer le fichier", + variant: "error", + }) + return false + } + + toast({ + title: "Fichier déplacé avec succès", + variant: "success", + }) + return true + }} + onCancel={undefined} + onSuccess={async () => { + await invalidateData({ + routeDefinition: readAllFilesRouteDefinition, + body: {}, + }) + + props.onSuccess?.() + }} + > + {(form) => ( + <> + ( + + + + + field.onChange(value === rootOptionKey ? null : (value ?? null)) + } + options={folderOptions} + placeholder="Sélectionner un dossier" + isLoading={foldersResponse.isPending} + /> + + + + )} + /> + + )} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/files/MoveOneFolderForm.tsx b/packages/website/src/features/dashboard/$idYear/files/MoveOneFolderForm.tsx new file mode 100644 index 00000000..cee45d0f --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/files/MoveOneFolderForm.tsx @@ -0,0 +1,165 @@ +import { readAllFoldersRouteDefinition, updateOneFolderRouteDefinition } from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { InputSelect, toast } from "@arrhes/ui" +import { IconArrowsMove } from "@tabler/icons-react" +import { useMemo } from "react" +import type * as v from "valibot" +import { FormControl } from "../../../../components/forms/FormControl.js" +import { FormError } from "../../../../components/forms/FormError.js" +import { FormField } from "../../../../components/forms/FormField.js" +import { FormItem } from "../../../../components/forms/FormItem.js" +import { FormLabel } from "../../../../components/forms/FormLabel.js" +import { FormRoot } from "../../../../components/forms/FormRoot.js" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" +import { invalidateData } from "../../../../utilities/invalidateData.js" +import { useDataFromAPI } from "../../../../utilities/useHTTPData.ts" + +const rootOptionKey = "__root__" + +function getDescendantFolderIds(parameters: { + folderId: string + folders: Array> +}) { + const childrenByParentId = new Map>() + + for (const folder of parameters.folders) { + if (folder.idFolderParent === null) { + continue + } + + const currentChildren = childrenByParentId.get(folder.idFolderParent) ?? [] + childrenByParentId.set(folder.idFolderParent, [ + ...currentChildren, + folder.id, + ]) + } + + const descendants = new Set() + const stack = [ + ...(childrenByParentId.get(parameters.folderId) ?? []), + ] + + while (stack.length > 0) { + const current = stack.pop() + + if (current === undefined || descendants.has(current)) { + continue + } + + descendants.add(current) + stack.push(...(childrenByParentId.get(current) ?? [])) + } + + return descendants +} + +export function MoveOneFolderForm(props: { + folder: v.InferOutput + onSuccess?: () => void +}) { + const foldersResponse = useDataFromAPI({ + routeDefinition: readAllFoldersRouteDefinition, + body: {}, + }) + + const folderOptions = useMemo(() => { + const allFolders = foldersResponse.data ?? [] + const blockedFolderIds = getDescendantFolderIds({ + folderId: props.folder.id, + folders: allFolders, + }) + blockedFolderIds.add(props.folder.id) + + return [ + { + key: rootOptionKey, + label: "/", + }, + ...allFolders + .filter((folder) => !blockedFolderIds.has(folder.id)) + .map((folder) => ({ + key: folder.id, + label: folder.name, + })), + ] + }, [ + foldersResponse.data, + props.folder.id, + ]) + + return ( + , + text: "Déplacer le dossier", + }} + onSubmit={async (data) => { + const updateResponse = await getResponseBodyFromAPI({ + routeDefinition: updateOneFolderRouteDefinition, + body: { + idFolder: props.folder.id, + idFolderParent: data.idFolderParent, + }, + }) + + if (updateResponse.ok === false) { + toast({ + title: "Impossible de déplacer le dossier", + variant: "error", + }) + return false + } + + toast({ + title: "Dossier déplacé avec succès", + variant: "success", + }) + return true + }} + onCancel={undefined} + onSuccess={async () => { + await invalidateData({ + routeDefinition: readAllFoldersRouteDefinition, + body: {}, + }) + + props.onSuccess?.() + }} + > + {(form) => ( + <> + ( + + + + + field.onChange(value === rootOptionKey ? null : (value ?? null)) + } + options={folderOptions} + placeholder="Sélectionner un dossier" + isLoading={foldersResponse.isPending} + /> + + + + )} + /> + + )} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/files/UpdateOneFolderForm.tsx b/packages/website/src/features/dashboard/$idYear/files/UpdateOneFolderForm.tsx new file mode 100644 index 00000000..1d872bfa --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/files/UpdateOneFolderForm.tsx @@ -0,0 +1,84 @@ +import { readAllFoldersRouteDefinition, updateOneFolderRouteDefinition } from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { InputText, toast } from "@arrhes/ui" +import { IconPencil } from "@tabler/icons-react" +import type * as v from "valibot" +import { FormControl } from "../../../../components/forms/FormControl.js" +import { FormError } from "../../../../components/forms/FormError.js" +import { FormField } from "../../../../components/forms/FormField.js" +import { FormItem } from "../../../../components/forms/FormItem.js" +import { FormLabel } from "../../../../components/forms/FormLabel.js" +import { FormRoot } from "../../../../components/forms/FormRoot.js" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" +import { invalidateData } from "../../../../utilities/invalidateData.js" + +export function UpdateOneFolderForm(props: { + folder: v.InferOutput + onSuccess?: () => void +}) { + return ( + , + text: "Modifier le dossier", + }} + onSubmit={async (data) => { + const updateResponse = await getResponseBodyFromAPI({ + routeDefinition: updateOneFolderRouteDefinition, + body: { + idFolder: props.folder.id, + name: data.name, + }, + }) + if (updateResponse.ok === false) { + toast({ + title: "Impossible de modifier le dossier", + variant: "error", + }) + return false + } + toast({ + title: "Dossier modifié avec succès", + variant: "success", + }) + return true + }} + onCancel={undefined} + onSuccess={async () => { + await invalidateData({ + routeDefinition: readAllFoldersRouteDefinition, + body: {}, + }) + props.onSuccess?.() + }} + > + {(form) => ( + ( + + + + + + + + )} + /> + )} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/files/createOneFile.tsx b/packages/website/src/features/dashboard/$idYear/files/createOneFile.tsx deleted file mode 100644 index 61346a90..00000000 --- a/packages/website/src/features/dashboard/$idYear/files/createOneFile.tsx +++ /dev/null @@ -1,197 +0,0 @@ -import { computeSHA256 } from "@arrhes/application-metadata" -import { - createOneFileRouteDefinition, - finalizeFileUploadRouteDefinition, - generateFilePutSignedUrlRouteDefinition, - readAllFilesRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { Button, toast } from "@arrhes/ui" -import type { JSX } from "react" -import { useRef } from "react" -import type * as v from "valibot" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" -import { invalidateData } from "../../../../utilities/invalidateData.js" - -const MAX_FILE_SIZE = 1024 * 1024 * 50 // 50 MB - -/** - * Derive a human-readable reference from a file name by stripping the extension. - */ -function referenceFromFileName(name: string): string { - const dotIndex = name.lastIndexOf(".") - return dotIndex > 0 ? name.slice(0, dotIndex) : name -} - -async function uploadOneFile(params: { - idOrganization: string - idYear: string - idFolder?: string | null - file: File -}): Promise<"added" | "duplicate" | "error"> { - const { file, idOrganization: _idOrganization, idYear, idFolder } = params - - if (file.size > MAX_FILE_SIZE) { - toast({ - title: `"${file.name}" dépasse la taille maximale de 50 Mo`, - variant: "error", - }) - return "error" - } - - const hash = await computeSHA256(file) - - // Step 1 - create the database record (server deduplicates by hash) - const createResponse = await getResponseBodyFromAPI({ - routeDefinition: createOneFileRouteDefinition, - body: { - idYear, - idFolder: idFolder ?? undefined, - reference: referenceFromFileName(file.name), - name: file.name, - hash: hash, - }, - }) - if (createResponse.ok === false) { - toast({ - title: `Impossible de créer "${file.name}"`, - description: createResponse.error.message, - variant: "error", - }) - return "error" - } - - // If the file already exists in storage (deduplication), skip upload - if (createResponse.data.storageKey !== null) { - return "duplicate" - } - - // Step 2 - obtain a pre-signed PUT URL and update storage metadata - const signedUrlResponse = await getResponseBodyFromAPI({ - routeDefinition: generateFilePutSignedUrlRouteDefinition, - body: { - idFile: createResponse.data.id, - type: file.type, - size: file.size, - }, - }) - if (signedUrlResponse.ok === false) { - toast({ - title: `Impossible de télécharger "${file.name}"`, - description: signedUrlResponse.error.message, - variant: "error", - }) - return "error" - } - - // Step 3 - upload the binary directly to object storage - const uploadResponse = await fetch(signedUrlResponse.data.url, { - method: "PUT", - body: file, - }) - if (uploadResponse.ok === false) { - toast({ - title: `Échec du téléchargement de "${file.name}"`, - variant: "error", - }) - return "error" - } - - // Step 4 - finalize upload only after the object was successfully stored - const finalizeResponse = await getResponseBodyFromAPI({ - routeDefinition: finalizeFileUploadRouteDefinition, - body: { - idFile: createResponse.data.id, - }, - }) - if (finalizeResponse.ok === false) { - toast({ - title: `Téléversement incomplet pour "${file.name}"`, - variant: "error", - }) - return "error" - } - - return "added" -} - -export function CreateOneFile(props: { - idOrganization: v.InferOutput["id"] - idYear: v.InferOutput["id"] - idFolder?: string | null - children: JSX.Element -}) { - const inputRef = useRef(null) - - async function handleFiles(files: FileList) { - const fileArray = Array.from(files) - if (fileArray.length === 0) return - - const results = await Promise.all( - fileArray.map((file) => - uploadOneFile({ - idOrganization: props.idOrganization, - idYear: props.idYear, - idFolder: props.idFolder, - file, - }), - ), - ) - - const added = results.filter((r) => r === "added").length - const duplicates = results.filter((r) => r === "duplicate").length - const failed = results.filter((r) => r === "error").length - - if (added > 0) { - await invalidateData({ - routeDefinition: readAllFilesRouteDefinition, - body: { - idYear: props.idYear, - }, - }) - } - - if (failed > 0) { - toast({ - title: `${added} fichier(s) ajouté(s), ${failed} en erreur`, - variant: "error", - }) - } else if (duplicates > 0 && added === 0) { - toast({ - title: duplicates === 1 ? "Ce fichier existe déjà" : `Ces ${duplicates} fichiers existent déjà`, - variant: "information", - }) - } else if (duplicates > 0) { - toast({ - title: `${added} fichier(s) ajouté(s), ${duplicates} déjà existant(s)`, - variant: "success", - }) - } else { - toast({ - title: added === 1 ? "Fichier ajouté avec succès" : `${added} fichiers ajoutés avec succès`, - variant: "success", - }) - } - } - - return ( - <> - { - if (event.target.files && event.target.files.length > 0) { - handleFiles(event.target.files) - } - // Reset so selecting the same file(s) again still triggers onChange - event.target.value = "" - }} - /> - - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/files/createOneFolder.tsx b/packages/website/src/features/dashboard/$idYear/files/createOneFolder.tsx deleted file mode 100644 index db6bab3e..00000000 --- a/packages/website/src/features/dashboard/$idYear/files/createOneFolder.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { createOneFolderRouteDefinition, readAllFoldersRouteDefinition } from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { Button, ButtonOutlineContent, InputText, toast } from "@arrhes/ui" -import { IconFolderPlus } from "@tabler/icons-react" -import { useState } from "react" -import type * as v from "valibot" -import { FormControl } from "../../../../components/forms/formControl.js" -import { FormError } from "../../../../components/forms/formError.js" -import { FormField } from "../../../../components/forms/formField.js" -import { FormItem } from "../../../../components/forms/formItem.js" -import { FormLabel } from "../../../../components/forms/formLabel.js" -import { FormRoot } from "../../../../components/forms/formRoot.js" -import { Drawer } from "../../../../components/overlays/drawer/drawer.js" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" -import { invalidateData } from "../../../../utilities/invalidateData.js" - -export function CreateOneFolder(props: { - idOrganization: v.InferOutput["id"] - idYear: v.InferOutput["id"] - idFolderParent?: string | null -}) { - const [open, setOpen] = useState(false) - - return ( - - - - - - , - text: "Créer le dossier", - }} - onSubmit={async (data) => { - const createResponse = await getResponseBodyFromAPI({ - routeDefinition: createOneFolderRouteDefinition, - body: { - idYear: data.idYear, - idFolderParent: data.idFolderParent, - name: data.name, - }, - }) - if (createResponse.ok === false) { - toast({ - title: "Impossible de créer le dossier", - variant: "error", - }) - return false - } - toast({ - title: "Dossier créé avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await invalidateData({ - routeDefinition: readAllFoldersRouteDefinition, - body: { - idYear: props.idYear, - }, - }) - setOpen(false) - }} - > - {(form) => ( - ( - - - - - - - - )} - /> - )} - - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/files/deleteFileWithSignedUrl.ts b/packages/website/src/features/dashboard/$idYear/files/deleteFileWithSignedUrl.ts index 06284882..030f4d34 100644 --- a/packages/website/src/features/dashboard/$idYear/files/deleteFileWithSignedUrl.ts +++ b/packages/website/src/features/dashboard/$idYear/files/deleteFileWithSignedUrl.ts @@ -4,7 +4,7 @@ import { } from "@arrhes/application-metadata/routes" import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" -export async function deleteFileWithSignedUrl(parameters: { idFile: string; idYear: string | null }) { +export async function deleteFileWithSignedUrl(parameters: { idFile: string }) { const signedDeleteUrlResponse = await getResponseBodyFromAPI({ routeDefinition: generateFileDeleteSignedUrlRouteDefinition, body: { diff --git a/packages/website/src/features/dashboard/$idYear/files/fileActions.tsx b/packages/website/src/features/dashboard/$idYear/files/fileActions.tsx deleted file mode 100644 index 7a852cb1..00000000 --- a/packages/website/src/features/dashboard/$idYear/files/fileActions.tsx +++ /dev/null @@ -1,308 +0,0 @@ -import { - ocrFileRouteDefinition, - readAllFilesRouteDefinition, - readOrganizationBillingRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { Button, ButtonGhostContent, Separator, toast } from "@arrhes/ui" -import { css } from "@arrhes/ui/css" -import { IconArrowsMove, IconDotsVertical, IconEye, IconFileText, IconPencil, IconTrash } from "@tabler/icons-react" -import { useState } from "react" -import type * as v from "valibot" -import { LinkButton } from "../../../../components/linkButton.js" -import { ConfirmationModal } from "../../../../components/overlays/dialog/confirmationModal.js" -import { Dialog } from "../../../../components/overlays/dialog/dialog.js" -import { Drawer } from "../../../../components/overlays/drawer/drawer.js" -import { Popover } from "../../../../components/overlays/popover/popover.js" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" -import { invalidateData } from "../../../../utilities/invalidateData.js" -import { useDataFromAPI } from "../../../../utilities/useHTTPData.ts" -import { UpdateOneFileForm } from "./$idFile/updateOneFileForm.js" -import { deleteFileWithSignedUrl } from "./deleteFileWithSignedUrl.js" -import { MoveOneFileForm } from "./moveOneFileForm.js" - -export function FileActions(props: { - file: v.InferOutput - idOrganization: string - idYear: string -}) { - const [editOpen, setEditOpen] = useState(false) - const [moveOpen, setMoveOpen] = useState(false) - const [deleteOpen, setDeleteOpen] = useState(false) - const [ocrLoading, setOcrLoading] = useState(false) - const [ocrTooltipOpen, setOcrTooltipOpen] = useState(false) - - const subscription = useDataFromAPI({ - routeDefinition: readOrganizationBillingRouteDefinition, - body: {}, - }) - const hasOcrAvailable = (subscription.data?.ocrPagesTotalAvailable ?? 0) > 0 - const isOcrSupportedType = props.file.type === "application/pdf" || (props.file.type?.startsWith("image/") ?? false) - - async function handleDelete() { - const isDeleted = await deleteFileWithSignedUrl({ - idFile: props.file.id, - idYear: props.idYear, - }) - - if (isDeleted === false) { - toast({ - title: "Erreur lors de la suppression du fichier", - variant: "error", - }) - return - } - - await invalidateData({ - routeDefinition: readAllFilesRouteDefinition, - body: { - idYear: props.idYear, - }, - }) - - toast({ - title: "Fichier supprimé", - variant: "success", - }) - } - - async function handleOcr() { - setOcrLoading(true) - const ocrResponse = await getResponseBodyFromAPI({ - routeDefinition: ocrFileRouteDefinition, - body: { - idFile: props.file.id, - idYear: props.idYear, - }, - hasToastMessage: true, - }) - setOcrLoading(false) - - if (ocrResponse.ok === false) { - return - } - - await invalidateData({ - routeDefinition: readAllFilesRouteDefinition, - body: { - idYear: props.idYear, - }, - }) - - toast({ - title: "Fichier converti en Markdown", - variant: "success", - }) - } - - return ( - <> - - - - - - - - } - text="Ouvrir" - className={css({ - width: "100%", - justifyContent: "start", - })} - /> - - - - - - - - - {props.file.storageKey && isOcrSupportedType && ( -
{ - if (!hasOcrAvailable) { - setOcrTooltipOpen(true) - } - }} - onPointerLeave={() => setOcrTooltipOpen(false)} - > - - - - {!hasOcrAvailable && ocrTooltipOpen && ( -
- Aucune page OCR disponible -
- )} -
- )} - - - - -
-
- - - - - - setEditOpen(false)} - /> - - - - - - - - Déplacer le fichier - - - setMoveOpen(false)} - /> - - - - - - Cette action supprimera le fichier et toutes les données associées. -
- Cette action est irréversible. - - } - submitButtonProps={{ - color: "danger", - text: "Supprimer le fichier", - }} - onSubmit={handleDelete} - open={deleteOpen} - onOpenChange={setDeleteOpen} - /> - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/files/fileContextMenu.tsx b/packages/website/src/features/dashboard/$idYear/files/fileContextMenu.tsx deleted file mode 100644 index 816cc676..00000000 --- a/packages/website/src/features/dashboard/$idYear/files/fileContextMenu.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import { readAllFilesRouteDefinition } from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { toast } from "@arrhes/ui" -import { css } from "@arrhes/ui/css" -import { IconArrowsMove, IconEye, IconPencil, IconTrash } from "@tabler/icons-react" -import { type ReactElement, useState } from "react" -import type * as v from "valibot" -import { ContextMenu } from "../../../../components/overlays/contextMenu/contextMenu.js" -import { ConfirmationModal } from "../../../../components/overlays/dialog/confirmationModal.js" -import { Dialog } from "../../../../components/overlays/dialog/dialog.js" -import { Drawer } from "../../../../components/overlays/drawer/drawer.js" -import { applicationRouter } from "../../../../routes/applicationRouter.js" -import { invalidateData } from "../../../../utilities/invalidateData.js" -import { UpdateOneFileForm } from "./$idFile/updateOneFileForm.js" -import { deleteFileWithSignedUrl } from "./deleteFileWithSignedUrl.js" -import { MoveOneFileForm } from "./moveOneFileForm.js" - -export function FileContextMenu(props: { - file: v.InferOutput - idOrganization: string - idYear: string - children: ReactElement -}) { - const [editOpen, setEditOpen] = useState(false) - const [moveOpen, setMoveOpen] = useState(false) - const [deleteOpen, setDeleteOpen] = useState(false) - - async function handleDelete() { - const isDeleted = await deleteFileWithSignedUrl({ - idFile: props.file.id, - idYear: props.idYear, - }) - - if (isDeleted === false) { - toast({ - title: "Erreur lors de la suppression du fichier", - variant: "error", - }) - return - } - - await invalidateData({ - routeDefinition: readAllFilesRouteDefinition, - body: { - idYear: props.idYear, - }, - }) - - toast({ - title: "Fichier supprimé", - variant: "success", - }) - } - - return ( - <> - - {props.children} - - } - onSelect={() => { - applicationRouter.navigate({ - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/stockage/$idFile", - params: { - idOrganization: props.idOrganization, - idYear: props.idYear, - idFile: props.file.id, - }, - }) - }} - > - Ouvrir - - } - onSelect={() => setEditOpen(true)} - > - Modifier - - } - onSelect={() => setMoveOpen(true)} - > - Déplacer - - - } - color="danger" - onSelect={() => setDeleteOpen(true)} - > - Supprimer - - - - - {/* Edit drawer (controlled externally) */} - - - - - setEditOpen(false)} - /> - - - - - - - - Déplacer le fichier - - - setMoveOpen(false)} - /> - - - - - {/* Delete dialog (controlled externally) */} - - Cette action supprimera le fichier et toutes les données associées. -
- Cette action est irréversible. - - } - submitButtonProps={{ - color: "danger", - text: "Supprimer le fichier", - }} - onSubmit={handleDelete} - open={deleteOpen} - onOpenChange={setDeleteOpen} - /> - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/files/fileSelect.tsx b/packages/website/src/features/dashboard/$idYear/files/fileSelect.tsx deleted file mode 100644 index d58304db..00000000 --- a/packages/website/src/features/dashboard/$idYear/files/fileSelect.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { readAllFilesRouteDefinition } from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { InputCombobox } from "@arrhes/ui" -import type * as v from "valibot" -import { useDataFromAPI } from "../../../../utilities/useHTTPData.ts" - -export function FileSelect(props: { - idOrganization: v.InferOutput["id"] - idYear: v.InferOutput["id"] - value?: string | null - onChange: (value?: string | null) => void -}) { - const filesResponse = useDataFromAPI({ - routeDefinition: readAllFilesRouteDefinition, - body: { - idYear: props.idYear, - }, - }) - - return ( - ({ - key: file.id, - label: file.reference ?? "", - })) - } - /> - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/files/filesGrid.tsx b/packages/website/src/features/dashboard/$idYear/files/filesGrid.tsx deleted file mode 100644 index 65bc9340..00000000 --- a/packages/website/src/features/dashboard/$idYear/files/filesGrid.tsx +++ /dev/null @@ -1,652 +0,0 @@ -import { - readAllFilesRouteDefinition, - readAllFoldersRouteDefinition, - updateOneFileRouteDefinition, - updateOneFolderRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { FormatDate, FormatFileSize, toast } from "@arrhes/ui" -import { css, cx } from "@arrhes/ui/utilities/cn.js" -import { IconArrowUp, IconFile, IconFileTypePdf, IconFolder, IconPhoto } from "@tabler/icons-react" -import { type DragEvent, useRef, useState } from "react" -import type * as v from "valibot" -import { EmptyState } from "../../../../components/layouts/emptyState.js" -import { applicationRouter } from "../../../../routes/applicationRouter.js" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" -import { invalidateData } from "../../../../utilities/invalidateData.js" -import { FileContextMenu } from "./fileContextMenu.js" -import { FolderContextMenu } from "./folderContextMenu.js" - -function getFileIcon(type: string | null) { - if (!type) return - if (type.startsWith("image/")) return - if (type === "application/pdf") return - return -} - -function getFileIconColor(type: string | null) { - if (!type) return "neutral/40" - if (type.startsWith("image/")) return "blue.500" - if (type === "application/pdf") return "red.500" - return "neutral/40" -} - -function getFileIconBg(type: string | null) { - if (!type) return "neutral/5" - if (type.startsWith("image/")) return "blue.50" - if (type === "application/pdf") return "red.50" - return "neutral/5" -} - -function getFileTypeLabel(type: string | null): string | null { - if (!type) return null - if (type.startsWith("image/")) return "Image" - if (type === "application/pdf") return "PDF" - if (type.startsWith("text/")) return "Texte" - if (type.includes("spreadsheet") || type.includes("excel")) return "Tableur" - if (type.includes("document") || type.includes("word")) return "Document" - if (type.includes("zip") || type.includes("archive") || type.includes("compressed")) return "Archive" - return null -} - -function getFileTypeBadgeColor(type: string | null): { - bg: string - text: string -} { - if (!type) - return { - bg: "neutral/8", - text: "neutral/50", - } - if (type.startsWith("image/")) - return { - bg: "blue.50", - text: "blue.600", - } - if (type === "application/pdf") - return { - bg: "red.50", - text: "red.600", - } - if (type.startsWith("text/")) - return { - bg: "green.50", - text: "green.600", - } - if (type.includes("spreadsheet") || type.includes("excel")) - return { - bg: "emerald.50", - text: "emerald.600", - } - if (type.includes("document") || type.includes("word")) - return { - bg: "indigo.50", - text: "indigo.600", - } - return { - bg: "neutral/8", - text: "neutral/50", - } -} - -const cardStyle = css({ - display: "flex", - flexDirection: "column", - alignItems: "center", - gap: "0", - borderRadius: "xl", - border: "1px solid", - borderColor: "neutral/10", - backgroundColor: "white", - cursor: "pointer", - transition: "all 0.2s ease", - overflow: "hidden", - _hover: { - borderColor: "primary/25", - boxShadow: "0 4px 12px rgba(0, 0, 0, 0.06)", - transform: "translateY(-2px)", - }, -}) - -type DragPayload = - | { - kind: "file" - id: string - sourceFolderId: string | null - } - | { - kind: "folder" - id: string - sourceParentFolderId: string | null - } - -export function FilesGrid(props: { - idOrganization: v.InferOutput["id"] - idYear: v.InferOutput["id"] - files: Array> - folders: Array> - currentFolderId: string | null - parentFolderId: string | null - onFolderOpen: (folderId: string | null) => void - hasActiveFilters?: boolean -}) { - const [dragOverFolderId, setDragOverFolderId] = useState(null) - const [draggingPayload, setDraggingPayload] = useState(null) - const draggingPayloadRef = useRef(null) - const suppressClickRef = useRef(false) - - const isEmpty = props.folders.length === 0 && props.files.length === 0 && props.currentFolderId === null - - function handleDragStart(event: DragEvent, payload: DragPayload) { - event.dataTransfer.setData("text/plain", JSON.stringify(payload)) - event.dataTransfer.effectAllowed = "move" - suppressClickRef.current = true - draggingPayloadRef.current = payload - setDraggingPayload(payload) - } - - function handleDragEnd() { - draggingPayloadRef.current = null - setDraggingPayload(null) - setDragOverFolderId(null) - setTimeout(() => { - suppressClickRef.current = false - }, 0) - } - - function getDragPayload(event: DragEvent): DragPayload | null { - try { - const rawPayload = event.dataTransfer.getData("text/plain") - if (!rawPayload) return null - const payload = JSON.parse(rawPayload) as DragPayload - if (payload.kind !== "file" && payload.kind !== "folder") return null - if (!payload.id) return null - return payload - } catch { - return null - } - } - - function canDropOnTarget(parameters: { payload: DragPayload; targetFolderId: string | null }) { - if (parameters.payload.kind === "file") { - return parameters.payload.sourceFolderId !== parameters.targetFolderId - } - - if (parameters.targetFolderId === parameters.payload.id) return false - return parameters.payload.sourceParentFolderId !== parameters.targetFolderId - } - - function handleDragOver( - event: DragEvent, - parameters: { - targetId: string - targetFolderId: string | null - }, - ) { - event.preventDefault() - event.dataTransfer.dropEffect = "move" - setDragOverFolderId(parameters.targetId) - } - - function handleDragLeave() { - setDragOverFolderId(null) - } - - async function handleDrop(event: DragEvent, folderId: string | null) { - event.preventDefault() - setDragOverFolderId(null) - - const payload = draggingPayloadRef.current ?? draggingPayload ?? getDragPayload(event) - if (!payload) return - if ( - !canDropOnTarget({ - payload, - targetFolderId: folderId, - }) - ) - return - - if (payload.kind === "file") { - const updateResponse = await getResponseBodyFromAPI({ - routeDefinition: updateOneFileRouteDefinition, - body: { - idFile: payload.id, - idYear: props.idYear, - idFolder: folderId, - }, - }) - - if (updateResponse.ok === false) { - toast({ - title: "Impossible de déplacer le fichier", - variant: "error", - }) - return - } - - await invalidateData({ - routeDefinition: readAllFilesRouteDefinition, - body: { - idYear: props.idYear, - }, - }) - - toast({ - title: "Fichier déplacé", - variant: "success", - }) - return - } - - if (folderId === payload.id) { - return - } - - const updateResponse = await getResponseBodyFromAPI({ - routeDefinition: updateOneFolderRouteDefinition, - body: { - idFolder: payload.id, - idYear: props.idYear, - idFolderParent: folderId, - }, - }) - - if (updateResponse.ok === false) { - toast({ - title: "Impossible de déplacer le dossier", - variant: "error", - }) - return - } - - await invalidateData({ - routeDefinition: readAllFoldersRouteDefinition, - body: { - idYear: props.idYear, - }, - }) - - toast({ - title: "Dossier déplacé", - variant: "success", - }) - } - - if (isEmpty) { - return ( -
- } - title={props.hasActiveFilters ? "Aucun résultat" : "Aucun fichier"} - subtitle={props.hasActiveFilters ? undefined : "Ajoutez un fichier ou un dossier pour commencer"} - /> -
- ) - } - - return ( -
-
- {/* Back folder ("..") when inside a folder */} - {props.currentFolderId !== null && ( -
props.onFolderOpen(props.parentFolderId)} - onDragOver={(event) => - handleDragOver(event, { - targetId: "__parent__", - targetFolderId: props.parentFolderId, - }) - } - onDragLeave={handleDragLeave} - onDrop={(event) => handleDrop(event, props.parentFolderId)} - className={css({ - display: "flex", - flexDirection: "column", - alignItems: "center", - gap: "0", - borderRadius: "lg", - border: "2px dashed", - borderColor: "neutral/12", - backgroundColor: "neutral/2", - cursor: "pointer", - transition: "all 0.2s ease", - overflow: "hidden", - _hover: { - borderColor: "primary/25", - backgroundColor: "primary/3", - }, - })} - > -
-
- -
-
-
- - .. - -
-
- )} - - {/* Folders */} - {props.folders.map((folder) => ( - -
- handleDragStart(event, { - kind: "folder", - id: folder.id, - sourceParentFolderId: folder.idFolderParent ?? null, - }) - } - onDragEnd={handleDragEnd} - onClick={(event) => { - if (suppressClickRef.current) { - event.preventDefault() - event.stopPropagation() - return - } - props.onFolderOpen(folder.id) - }} - onDragOver={(event) => - handleDragOver(event, { - targetId: folder.id, - targetFolderId: folder.id, - }) - } - onDragLeave={handleDragLeave} - onDrop={(event) => handleDrop(event, folder.id)} - className={cx( - css({ - display: "flex", - flexDirection: "column", - alignItems: "center", - gap: "0", - borderRadius: "xl", - border: "1px solid", - borderColor: "amber.200", - backgroundColor: "white", - cursor: "pointer", - transition: "all 0.2s ease", - overflow: "hidden", - _hover: { - borderColor: "amber.300", - boxShadow: "0 4px 12px rgba(0, 0, 0, 0.06)", - transform: "translateY(-2px)", - }, - }), - dragOverFolderId === folder.id && - css({ - borderColor: "primary", - backgroundColor: "primary/5", - boxShadow: "0 4px 16px rgba(0, 0, 0, 0.1)", - }), - )} - > - {/* Folder icon area */} -
-
- -
-
- - {/* Folder info */} -
- - {folder.name} - - - - -
-
-
- ))} - - {/* Files */} - {props.files.map((file) => { - const typeLabel = getFileTypeLabel(file.type) - const badgeColor = getFileTypeBadgeColor(file.type) - - return ( - -
- handleDragStart(event, { - kind: "file", - id: file.id, - sourceFolderId: file.idFolder ?? null, - }) - } - onDragEnd={handleDragEnd} - onClick={(event) => { - if (suppressClickRef.current) { - event.preventDefault() - event.stopPropagation() - return - } - applicationRouter.navigate({ - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/stockage/$idFile", - params: { - idOrganization: props.idOrganization, - idYear: props.idYear, - idFile: file.id, - }, - }) - }} - className={cardStyle} - > - {/* File icon area */} -
-
- {getFileIcon(file.type)} -
- {/* File type badge */} - {typeLabel && ( - - {typeLabel} - - )} -
- - {/* File info */} -
- - {file.name ?? file.reference} - - {file.name && file.reference && ( - - {file.reference} - - )} -
- - {file.size && ( - <> - - · - - - - )} -
-
-
-
- ) - })} -
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/files/filesPage.tsx b/packages/website/src/features/dashboard/$idYear/files/filesPage.tsx deleted file mode 100644 index 8940b48c..00000000 --- a/packages/website/src/features/dashboard/$idYear/files/filesPage.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { - readAllFilesRouteDefinition, - readAllFoldersRouteDefinition, - updateOneFileRouteDefinition, -} from "@arrhes/application-metadata/routes" -import { ButtonPlainContent, toast } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconPlus } from "@tabler/icons-react" -import { useNavigate, useParams, useSearch } from "@tanstack/react-router" -import { type DragEvent, useState } from "react" -import { DataWrapper } from "../../../../components/layouts/dataWrapper.js" -import { Page } from "../../../../components/layouts/page/page.js" -import { filesLayoutRoute } from "../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/files/filesLayoutRoute.js" -import { filesRoute } from "../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/files/filesRoute.js" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" -import { invalidateData } from "../../../../utilities/invalidateData.js" -import { CreateOneFile } from "./createOneFile.js" -import { CreateOneFolder } from "./createOneFolder.js" -import { FilesPageContent } from "./filesPageContent.js" - -export function FilesPage() { - const params = useParams({ - from: filesLayoutRoute.id, - }) - const { idFolder } = useSearch({ - from: filesRoute.id, - }) - const navigate = useNavigate() - - // Current folder is derived from the URL search param - const currentFolderId = idFolder ?? null - - function navigateToFolder(folderId: string | null) { - navigate({ - to: ".", - search: folderId - ? { - idFolder: folderId, - } - : {}, - }) - } - - // Breadcrumb drag-and-drop - move files to a specific breadcrumb folder (or root) - const [breadcrumbDragOver, setBreadcrumbDragOver] = useState(null) - - function handleBreadcrumbDragOver(event: DragEvent, targetId: string) { - event.preventDefault() - event.dataTransfer.dropEffect = "move" - setBreadcrumbDragOver(targetId) - } - - function handleBreadcrumbDragLeave() { - setBreadcrumbDragOver(null) - } - - async function handleBreadcrumbDrop(event: DragEvent, targetFolderId: string | null) { - event.preventDefault() - setBreadcrumbDragOver(null) - - const fileId = event.dataTransfer.getData("text/plain") - if (!fileId) return - - const updateResponse = await getResponseBodyFromAPI({ - routeDefinition: updateOneFileRouteDefinition, - body: { - idFile: fileId, - idYear: params.idYear, - idFolder: targetFolderId, - }, - }) - - if (updateResponse.ok === false) { - toast({ - title: "Impossible de déplacer le fichier", - variant: "error", - }) - return - } - - await invalidateData({ - routeDefinition: readAllFilesRouteDefinition, - body: { - idYear: params.idYear, - }, - }) - - toast({ - title: "Fichier déplacé", - variant: "success", - }) - } - - return ( - - - {/* View toggle + actions */} -
- - - } - text="Ajouter un fichier" - /> - -
- - - {(folders) => ( - - )} - -
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/files/filesPageContent.tsx b/packages/website/src/features/dashboard/$idYear/files/filesPageContent.tsx deleted file mode 100644 index 45990175..00000000 --- a/packages/website/src/features/dashboard/$idYear/files/filesPageContent.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import { readAllFilesRouteDefinition, type readAllFoldersRouteDefinition } from "@arrhes/application-metadata/routes" -import { css } from "@arrhes/ui/utilities/cn.js" -import { type DragEvent, useMemo, useState } from "react" -import type * as v from "valibot" -import { DataWrapper } from "../../../../components/layouts/dataWrapper.js" -import { FilesGrid } from "./filesGrid.js" -import { FilesTable } from "./filesTable.js" - -type ViewMode = "grid" | "list" - -type Folder = v.InferOutput[number] - -/** - * Build the breadcrumb path from the root to the target folder by - * walking `idFolderParent` links upward, then reversing. - */ -function buildFolderPath( - folders: Array, - targetId: string | undefined, -): Array<{ - id: string - name: string -}> { - if (!targetId) return [] - - const map = new Map( - folders.map((f) => [ - f.id, - f, - ]), - ) - const path: Array<{ - id: string - name: string - }> = [] - let current = map.get(targetId) - - while (current) { - path.push({ - id: current.id, - name: current.name, - }) - current = current.idFolderParent ? map.get(current.idFolderParent) : undefined - } - - return path.reverse() -} - -/** - * Inner component that has access to the fetched folders data, - * allowing us to compute the breadcrumb path. - */ -export function FilesPageContent(props: { - folders: Array - idFolder: string | undefined - currentFolderId: string | null - navigateToFolder: (folderId: string | null) => void - breadcrumbDragOver: string | null - handleBreadcrumbDragOver: (event: DragEvent, targetId: string) => void - handleBreadcrumbDragLeave: () => void - handleBreadcrumbDrop: (event: DragEvent, targetFolderId: string | null) => void - params: { - idOrganization: string - idYear: string - } -}) { - const { - folders, - idFolder, - currentFolderId, - navigateToFolder, - breadcrumbDragOver: _breadcrumbDragOver, - handleBreadcrumbDragOver: _handleBreadcrumbDragOver, - handleBreadcrumbDragLeave: _handleBreadcrumbDragLeave, - handleBreadcrumbDrop: _handleBreadcrumbDrop, - params, - } = props - const [viewMode, _setViewMode] = useState("list") - - const _folderPath = useMemo( - () => buildFolderPath(folders, idFolder), - [ - folders, - idFolder, - ], - ) - - const currentFolders = folders.filter((f) => (f.idFolderParent ?? null) === currentFolderId) - - // Compute the parent folder ID so that ".." navigation works - const parentFolderId = useMemo(() => { - if (!currentFolderId) return null - const currentFolder = folders.find((f) => f.id === currentFolderId) - return currentFolder?.idFolderParent ?? null - }, [ - folders, - currentFolderId, - ]) - - const sortedFolders = useMemo( - () => - [ - ...currentFolders, - ].sort((a, b) => a.name.localeCompare(b.name)), - [ - currentFolders, - ], - ) - - return ( -
- - {(files) => { - const currentFiles = files.filter((f) => (f.idFolder ?? null) === currentFolderId) - - if (viewMode === "grid") { - return ( - - ) - } - return ( - - ) - }} - -
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/files/filesTable.tsx b/packages/website/src/features/dashboard/$idYear/files/filesTable.tsx deleted file mode 100644 index bb115197..00000000 --- a/packages/website/src/features/dashboard/$idYear/files/filesTable.tsx +++ /dev/null @@ -1,548 +0,0 @@ -import { - readAllFilesRouteDefinition, - readAllFoldersRouteDefinition, - updateOneFileRouteDefinition, - updateOneFolderRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { Button, ButtonGhostContent, FormatDateTime, FormatFileSize, FormatNull, toast } from "@arrhes/ui" -import { css, cx } from "@arrhes/ui/utilities/cn.js" -import { IconArrowLeft, IconFile, IconFileTypePdf, IconFolder } from "@tabler/icons-react" -import { type DragEvent, type MouseEvent, type ReactElement, useEffect, useRef, useState } from "react" -import type * as v from "valibot" -import { DataTable } from "../../../../components/layouts/dataTable.js" -import { applicationRouter } from "../../../../routes/applicationRouter.js" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" -import { invalidateData } from "../../../../utilities/invalidateData.js" -import { FileActions } from "./fileActions.js" -import type { TableRow } from "./filesTableSelectionActions.js" -import { FilesTableSelectionActions } from "./filesTableSelectionActions.js" -import { FolderActions } from "./folderActions.js" - -type DragPayload = - | { - kind: "file" - id: string - sourceFolderId: string | null - } - | { - kind: "folder" - id: string - sourceParentFolderId: string | null - } - -export function FilesTable(props: { - idOrganization: v.InferOutput["id"] - idYear: v.InferOutput["id"] - files: Array> - folders: Array> - currentFolderId: string | null - parentFolderId: string | null - onFolderOpen: (folderId: string | null) => void - hasActiveFilters?: boolean -}) { - const [dragOverTargetId, setDragOverTargetId] = useState(null) - const [draggingPayload, setDraggingPayload] = useState(null) - const [longPressReadyId, setLongPressReadyId] = useState(null) - const draggingPayloadRef = useRef(null) - const suppressClickRef = useRef(false) - // Long-press-to-drag: 300ms hold activates draggable on the row - const LONG_PRESS_MS = 300 - const longPressTimerRef = useRef | null>(null) - const pendingDragRef = useRef<{ - row: HTMLTableRowElement - payload: DragPayload - id: string - } | null>(null) - - function startLongPress(event: MouseEvent, payload: DragPayload, id: string) { - if (event.button !== 0) return - const row = event.currentTarget - if (longPressTimerRef.current !== null) clearTimeout(longPressTimerRef.current) - pendingDragRef.current = { - row, - payload, - id, - } - longPressTimerRef.current = setTimeout(() => { - if (pendingDragRef.current) { - pendingDragRef.current.row.draggable = true - pendingDragRef.current.row.style.cursor = "grab" - setLongPressReadyId(pendingDragRef.current.id) - } - }, LONG_PRESS_MS) - } - - function cancelLongPress() { - if (longPressTimerRef.current !== null) { - clearTimeout(longPressTimerRef.current) - longPressTimerRef.current = null - } - // Only reset if no drag is in progress - if (pendingDragRef.current && draggingPayloadRef.current === null) { - pendingDragRef.current.row.draggable = false - pendingDragRef.current.row.style.cursor = "" - pendingDragRef.current = null - setLongPressReadyId(null) - } - } - - function handleRowDragStart(event: DragEvent) { - const pending = pendingDragRef.current - if (!pending) { - event.preventDefault() - return - } - event.dataTransfer.setData("text/plain", JSON.stringify(pending.payload)) - event.dataTransfer.effectAllowed = "move" - const row = event.currentTarget - const rect = row.getBoundingClientRect() - event.dataTransfer.setDragImage(row, event.clientX - rect.left, event.clientY - rect.top) - suppressClickRef.current = true - draggingPayloadRef.current = pending.payload - setTimeout(() => { - if (draggingPayloadRef.current !== null) { - setDraggingPayload(draggingPayloadRef.current) - } - }, 0) - } - - function handleDragEnd() { - if (pendingDragRef.current) { - pendingDragRef.current.row.draggable = false - pendingDragRef.current.row.style.cursor = "" - pendingDragRef.current = null - } - if (longPressTimerRef.current !== null) { - clearTimeout(longPressTimerRef.current) - longPressTimerRef.current = null - } - draggingPayloadRef.current = null - setDraggingPayload(null) - setDragOverTargetId(null) - setLongPressReadyId(null) - setTimeout(() => { - suppressClickRef.current = false - }, 0) - } - - // Window-level fallback: if the element-level onDragEnd is missed (e.g. the DOM - // node was replaced by a re-render mid-drag), this guarantees state is cleaned up. - useEffect(() => { - const onWindowDragEnd = () => { - if (draggingPayloadRef.current === null) return - if (pendingDragRef.current) { - pendingDragRef.current.row.draggable = false - pendingDragRef.current.row.style.cursor = "" - pendingDragRef.current = null - } - draggingPayloadRef.current = null - setDraggingPayload(null) - setDragOverTargetId(null) - setLongPressReadyId(null) - setTimeout(() => { - suppressClickRef.current = false - }, 0) - } - window.addEventListener("dragend", onWindowDragEnd) - return () => window.removeEventListener("dragend", onWindowDragEnd) - }, []) - - function getDragPayload(event: DragEvent): DragPayload | null { - try { - const rawPayload = event.dataTransfer.getData("text/plain") - if (!rawPayload) return null - const payload = JSON.parse(rawPayload) as DragPayload - if (payload.kind !== "file" && payload.kind !== "folder") return null - if (!payload.id) return null - return payload - } catch { - return null - } - } - - function canDropOnTarget(parameters: { payload: DragPayload; targetFolderId: string | null }) { - if (parameters.payload.kind === "file") { - return parameters.payload.sourceFolderId !== parameters.targetFolderId - } - - if (parameters.targetFolderId === parameters.payload.id) return false - return parameters.payload.sourceParentFolderId !== parameters.targetFolderId - } - - function getRowInteractionProps(item: TableRow) { - const dropTargetProps = (() => { - if (item.kind === "file") return {} - const targetId = item.kind === "back" ? "back" : item.data.id - const targetFolderId = item.kind === "back" ? props.parentFolderId : item.data.id - return { - onDragOver: (event: DragEvent) => { - event.preventDefault() - event.dataTransfer.dropEffect = "move" - setDragOverTargetId(targetId) - }, - onDragLeave: (event: DragEvent) => { - if ((event.currentTarget as HTMLElement).contains(event.relatedTarget as Node)) return - setDragOverTargetId(null) - }, - onDrop: (event: DragEvent) => handleDrop(event, targetFolderId), - } - })() - - const dragSourceProps = (() => { - if (item.kind === "back") return {} - const id = item.kind === "folder" ? item.data.id : item.data.id - const payload: DragPayload = - item.kind === "folder" - ? { - kind: "folder", - id: item.data.id, - sourceParentFolderId: item.data.idFolderParent ?? null, - } - : { - kind: "file", - id: item.data.id, - sourceFolderId: item.data.idFolder ?? null, - } - return { - onMouseDown: (event: MouseEvent) => startLongPress(event, payload, id), - onMouseUp: cancelLongPress, - onMouseLeave: cancelLongPress, - onDragStart: (event: DragEvent) => handleRowDragStart(event), - onDragEnd: () => handleDragEnd(), - } - })() - - const itemId = item.kind !== "back" ? item.data.id : "back" - const isDropTarget = - dragOverTargetId !== null && - ((item.kind === "back" && dragOverTargetId === "back") || - (item.kind === "folder" && dragOverTargetId === item.data.id)) - - const isDraggingThis = - draggingPayload !== null && - ((item.kind === "folder" && draggingPayload.kind === "folder" && draggingPayload.id === item.data.id) || - (item.kind === "file" && draggingPayload.kind === "file" && draggingPayload.id === item.data.id)) - - const isLongPressReady = longPressReadyId === itemId - - return { - ...dropTargetProps, - ...dragSourceProps, - className: cx( - isDropTarget - ? css({ - backgroundColor: "primary/6", - }) - : undefined, - isDraggingThis - ? css({ - opacity: "0.4", - }) - : undefined, - isLongPressReady - ? css({ - outline: "2px solid", - outlineColor: "primary/40", - outlineOffset: "-2px", - borderRadius: "sm", - backgroundColor: "primary/4", - }) - : undefined, - ), - } - } - - async function handleDrop(event: DragEvent, targetFolderId: string | null) { - event.preventDefault() - setDragOverTargetId(null) - - const payload = draggingPayloadRef.current ?? draggingPayload ?? getDragPayload(event) - if (!payload) return - if ( - !canDropOnTarget({ - payload, - targetFolderId, - }) - ) { - handleDragEnd() - return - } - - if (payload.kind === "file") { - const updateResponse = await getResponseBodyFromAPI({ - routeDefinition: updateOneFileRouteDefinition, - body: { - idFile: payload.id, - idYear: props.idYear, - idFolder: targetFolderId, - }, - }) - - if (updateResponse.ok === false) { - toast({ - title: "Impossible de déplacer le fichier", - variant: "error", - }) - return - } - - await invalidateData({ - routeDefinition: readAllFilesRouteDefinition, - body: { - idYear: props.idYear, - }, - }) - - toast({ - title: "Fichier déplacé", - variant: "success", - }) - return - } - - if (targetFolderId === payload.id) { - return - } - - const updateResponse = await getResponseBodyFromAPI({ - routeDefinition: updateOneFolderRouteDefinition, - body: { - idFolder: payload.id, - idYear: props.idYear, - idFolderParent: targetFolderId, - }, - }) - - if (updateResponse.ok === false) { - toast({ - title: "Impossible de déplacer le dossier", - variant: "error", - }) - return - } - - await invalidateData({ - routeDefinition: readAllFoldersRouteDefinition, - body: { - idYear: props.idYear, - }, - }) - - toast({ - title: "Dossier déplacé", - variant: "success", - }) - } - - const rows: Array = [ - ...(props.currentFolderId !== null - ? [ - { - kind: "back" as const, - }, - ] - : []), - ...props.folders.map((folder) => ({ - kind: "folder" as const, - data: folder, - })), - ...props.files.map((file) => ({ - kind: "file" as const, - data: file, - })), - ] - - const icons: Record = { - "application/pdf": , - } - - return ( - getRowInteractionProps(row.original)} - hideSearchBar={false} - enableRowSelection={(row) => row.original.kind !== "back"} - getRowId={(row) => (row.kind === "back" ? "__back__" : row.data.id)} - resetSelectionTrigger={props.currentFolderId} - selectionActions={(selectedRows) => ( - - )} - emptyStateProps={{ - icon: , - title: "Aucun fichier", - subtitle: "Les fichiers de votre exercice apparaîtront ici.", - }} - columns={[ - { - id: "name", - accessorFn: (row) => (row.kind === "back" ? ".." : (row.data.name ?? "")), - header: "Nom", - cell: ({ row }) => { - const item = row.original - if (item.kind === "back") { - return ( -
props.onFolderOpen(props.parentFolderId)} - className={css({ - width: "fit-content", - maxWidth: "100%", - cursor: "pointer", - })} - > - } - text=".." - /> -
- ) - } - if (item.kind === "folder") { - return ( -
- -
- ) - } - if (item.kind === "file") { - const leftIcon = item.data.type !== null ? icons[item.data.type] : undefined - - return ( -
- -
- ) - } - }, - filterFn: "includesString", - }, - { - id: "size", - accessorFn: (row) => (row.kind === "file" ? (row.data.size ?? "") : ""), - header: "Size", - cell: ({ row }) => { - const item = row.original - if (item.kind === "back" || item.kind === "folder") return - if (item.kind === "file") return - }, - filterFn: "includesString", - }, - { - id: "createdAt", - accessorFn: (row) => (row.kind === "back" ? "" : row.data.createdAt), - header: "Date", - cell: ({ row }) => { - const item = row.original - if (item.kind === "back") - return ( - - -- - - ) - if (item.kind === "folder") return - return - }, - filterFn: "includesString", - }, - { - id: "actions", - header: " ", - cell: ({ row }) => { - const item = row.original - if (item.kind === "back") return null - if (item.kind === "folder") { - return ( - - ) - } - return ( - - ) - }, - enableSorting: false, - enableGlobalFilter: false, - }, - ]} - /> - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/files/filesTableSelectionActions.tsx b/packages/website/src/features/dashboard/$idYear/files/filesTableSelectionActions.tsx deleted file mode 100644 index 8773d055..00000000 --- a/packages/website/src/features/dashboard/$idYear/files/filesTableSelectionActions.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import type { returnedSchemas } from "@arrhes/application-metadata" -import { - deleteOneFolderRouteDefinition, - readAllFilesRouteDefinition, - readAllFoldersRouteDefinition, -} from "@arrhes/application-metadata/routes" -import { Button, ButtonGhostContent, toast } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconChevronDown, IconTrash } from "@tabler/icons-react" -import type { Row } from "@tanstack/react-table" -import { useState } from "react" -import type * as v from "valibot" -import { ConfirmationModal } from "../../../../components/overlays/dialog/confirmationModal.js" -import { Popover } from "../../../../components/overlays/popover/popover.js" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" -import { invalidateData } from "../../../../utilities/invalidateData.js" -import { deleteFileWithSignedUrl } from "./deleteFileWithSignedUrl.js" - -export type TableRow = - | { - kind: "back" - } - | { - kind: "folder" - data: v.InferOutput - } - | { - kind: "file" - data: v.InferOutput - } - -export function FilesTableSelectionActions(props: { selectedRows: Array>; idYear: string }) { - const [deleteOpen, setDeleteOpen] = useState(false) - const selectedFiles = props.selectedRows - .filter((r) => r.original.kind === "file") - .map( - (r) => - ( - r.original as Extract< - TableRow, - { - kind: "file" - } - > - ).data, - ) - const selectedFolders = props.selectedRows - .filter((r) => r.original.kind === "folder") - .map( - (r) => - ( - r.original as Extract< - TableRow, - { - kind: "folder" - } - > - ).data, - ) - - async function handleDelete() { - const results = await Promise.all([ - ...selectedFiles.map((file) => - deleteFileWithSignedUrl({ - idFile: file.id, - idYear: props.idYear, - }).then((ok) => ({ - ok, - })), - ), - ...selectedFolders.map((folder) => - getResponseBodyFromAPI({ - routeDefinition: deleteOneFolderRouteDefinition, - body: { - idFolder: folder.id, - idYear: props.idYear, - }, - }), - ), - ]) - await Promise.all([ - invalidateData({ - routeDefinition: readAllFilesRouteDefinition, - body: { - idYear: props.idYear, - }, - }), - invalidateData({ - routeDefinition: readAllFoldersRouteDefinition, - body: { - idYear: props.idYear, - }, - }), - ]) - if (results.some((r) => r.ok === false)) { - toast({ - title: "Certains éléments n'ont pas pu être supprimés", - variant: "error", - }) - } else { - toast({ - title: "Éléments supprimés", - variant: "success", - }) - } - } - - return ( - <> - - - - - - - - - - - 1 ? "s" : "" - } ? Cette action est irréversible.`} - submitButtonProps={{ - text: "Supprimer", - leftIcon: , - color: "danger", - }} - onSubmit={handleDelete} - /> - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/files/folderActions.tsx b/packages/website/src/features/dashboard/$idYear/files/folderActions.tsx deleted file mode 100644 index ce6cb931..00000000 --- a/packages/website/src/features/dashboard/$idYear/files/folderActions.tsx +++ /dev/null @@ -1,205 +0,0 @@ -import { deleteOneFolderRouteDefinition, readAllFoldersRouteDefinition } from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { Button, ButtonGhostContent, Separator, toast } from "@arrhes/ui" -import { css } from "@arrhes/ui/css" -import { IconArrowsMove, IconDotsVertical, IconEye, IconPencil, IconTrash } from "@tabler/icons-react" -import { useState } from "react" -import type * as v from "valibot" -import { ConfirmationModal } from "../../../../components/overlays/dialog/confirmationModal.js" -import { Dialog } from "../../../../components/overlays/dialog/dialog.js" -import { Drawer } from "../../../../components/overlays/drawer/drawer.js" -import { Popover } from "../../../../components/overlays/popover/popover.js" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" -import { invalidateData } from "../../../../utilities/invalidateData.js" -import { MoveOneFolderForm } from "./moveOneFolderForm.js" -import { UpdateOneFolderForm } from "./updateOneFolderForm.js" - -export function FolderActions(props: { - folder: v.InferOutput - idOrganization: string - idYear: string - onFolderOpen: (folderId: string | null) => void -}) { - const [editOpen, setEditOpen] = useState(false) - const [moveOpen, setMoveOpen] = useState(false) - const [deleteOpen, setDeleteOpen] = useState(false) - - async function handleDelete() { - const deleteResponse = await getResponseBodyFromAPI({ - routeDefinition: deleteOneFolderRouteDefinition, - body: { - idFolder: props.folder.id, - idYear: props.idYear, - }, - }) - - if (deleteResponse.ok === false) { - toast({ - title: "Erreur lors de la suppression du dossier", - variant: "error", - }) - return - } - - await invalidateData({ - routeDefinition: readAllFoldersRouteDefinition, - body: { - idYear: props.idYear, - }, - }) - - toast({ - title: "Dossier supprimé", - variant: "success", - }) - } - - return ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - setEditOpen(false)} - /> - - - - - - - - Déplacer le dossier - - - setMoveOpen(false)} - /> - - - - - - Cette action supprimera le dossier et tous ses sous-dossiers. -
- Les fichiers contenus ne seront pas supprimés. -
- Cette action est irréversible. - - } - submitButtonProps={{ - color: "danger", - text: "Supprimer le dossier", - }} - onSubmit={handleDelete} - open={deleteOpen} - onOpenChange={setDeleteOpen} - /> - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/files/folderContextMenu.tsx b/packages/website/src/features/dashboard/$idYear/files/folderContextMenu.tsx deleted file mode 100644 index 3759f7cf..00000000 --- a/packages/website/src/features/dashboard/$idYear/files/folderContextMenu.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { deleteOneFolderRouteDefinition, readAllFoldersRouteDefinition } from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { toast } from "@arrhes/ui" -import { css } from "@arrhes/ui/css" -import { IconArrowsMove, IconPencil, IconTrash } from "@tabler/icons-react" -import { type ReactElement, useState } from "react" -import type * as v from "valibot" -import { ContextMenu } from "../../../../components/overlays/contextMenu/contextMenu.js" -import { ConfirmationModal } from "../../../../components/overlays/dialog/confirmationModal.js" -import { Dialog } from "../../../../components/overlays/dialog/dialog.js" -import { Drawer } from "../../../../components/overlays/drawer/drawer.js" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" -import { invalidateData } from "../../../../utilities/invalidateData.js" -import { MoveOneFolderForm } from "./moveOneFolderForm.js" -import { UpdateOneFolderForm } from "./updateOneFolderForm.js" - -export function FolderContextMenu(props: { - folder: v.InferOutput - idOrganization: string - idYear: string - children: ReactElement -}) { - const [editOpen, setEditOpen] = useState(false) - const [moveOpen, setMoveOpen] = useState(false) - const [deleteOpen, setDeleteOpen] = useState(false) - - async function handleDelete() { - const deleteResponse = await getResponseBodyFromAPI({ - routeDefinition: deleteOneFolderRouteDefinition, - body: { - idFolder: props.folder.id, - idYear: props.idYear, - }, - }) - - if (deleteResponse.ok === false) { - toast({ - title: "Erreur lors de la suppression du dossier", - variant: "error", - }) - return - } - - await invalidateData({ - routeDefinition: readAllFoldersRouteDefinition, - body: { - idYear: props.idYear, - }, - }) - - toast({ - title: "Dossier supprimé", - variant: "success", - }) - } - - return ( - <> - - {props.children} - - } - onSelect={() => setEditOpen(true)} - > - Renommer - - } - onSelect={() => setMoveOpen(true)} - > - Déplacer - - - } - color="danger" - onSelect={() => setDeleteOpen(true)} - > - Supprimer - - - - - {/* Edit drawer (controlled externally) */} - - - - - setEditOpen(false)} - /> - - - - - - - - Déplacer le dossier - - - setMoveOpen(false)} - /> - - - - - {/* Delete dialog (controlled externally) */} - - Cette action supprimera le dossier et tous ses sous-dossiers. -
- Les fichiers contenus ne seront pas supprimés. -
- Cette action est irréversible. - - } - submitButtonProps={{ - color: "danger", - text: "Supprimer le dossier", - }} - onSubmit={handleDelete} - open={deleteOpen} - onOpenChange={setDeleteOpen} - /> - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/files/moveOneFileForm.tsx b/packages/website/src/features/dashboard/$idYear/files/moveOneFileForm.tsx deleted file mode 100644 index 10fc4f6a..00000000 --- a/packages/website/src/features/dashboard/$idYear/files/moveOneFileForm.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { - readAllFilesRouteDefinition, - readAllFoldersRouteDefinition, - updateOneFileRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { InputSelect, toast } from "@arrhes/ui" -import { IconArrowsMove } from "@tabler/icons-react" -import { useMemo } from "react" -import type * as v from "valibot" -import { FormControl } from "../../../../components/forms/formControl.js" -import { FormError } from "../../../../components/forms/formError.js" -import { FormField } from "../../../../components/forms/formField.js" -import { FormItem } from "../../../../components/forms/formItem.js" -import { FormLabel } from "../../../../components/forms/formLabel.js" -import { FormRoot } from "../../../../components/forms/formRoot.js" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" -import { invalidateData } from "../../../../utilities/invalidateData.js" -import { useDataFromAPI } from "../../../../utilities/useHTTPData.ts" - -const rootOptionKey = "__root__" - -export function MoveOneFileForm(props: { - file: v.InferOutput - idYear: string - onSuccess?: () => void -}) { - const foldersResponse = useDataFromAPI({ - routeDefinition: readAllFoldersRouteDefinition, - body: { - idYear: props.idYear, - }, - }) - - const folderOptions = useMemo( - () => [ - { - key: rootOptionKey, - label: "/", - }, - ...(foldersResponse.data ?? []).map((folder) => ({ - key: folder.id, - label: folder.name, - })), - ], - [ - foldersResponse.data, - ], - ) - - return ( - , - text: "Déplacer le fichier", - }} - onSubmit={async (data) => { - const updateResponse = await getResponseBodyFromAPI({ - routeDefinition: updateOneFileRouteDefinition, - body: { - idFile: props.file.id, - idYear: data.idYear, - idFolder: data.idFolder, - }, - }) - - if (updateResponse.ok === false) { - toast({ - title: "Impossible de déplacer le fichier", - variant: "error", - }) - return false - } - - toast({ - title: "Fichier déplacé avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await invalidateData({ - routeDefinition: readAllFilesRouteDefinition, - body: { - idYear: props.idYear, - }, - }) - - props.onSuccess?.() - }} - > - {(form) => ( - <> - ( - - - - - field.onChange(value === rootOptionKey ? null : (value ?? null)) - } - options={folderOptions} - placeholder="Sélectionner un dossier" - isLoading={foldersResponse.isPending} - /> - - - - )} - /> - - )} - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/files/moveOneFolderForm.tsx b/packages/website/src/features/dashboard/$idYear/files/moveOneFolderForm.tsx deleted file mode 100644 index bc879fd7..00000000 --- a/packages/website/src/features/dashboard/$idYear/files/moveOneFolderForm.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import { readAllFoldersRouteDefinition, updateOneFolderRouteDefinition } from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { InputSelect, toast } from "@arrhes/ui" -import { IconArrowsMove } from "@tabler/icons-react" -import { useMemo } from "react" -import type * as v from "valibot" -import { FormControl } from "../../../../components/forms/formControl.js" -import { FormError } from "../../../../components/forms/formError.js" -import { FormField } from "../../../../components/forms/formField.js" -import { FormItem } from "../../../../components/forms/formItem.js" -import { FormLabel } from "../../../../components/forms/formLabel.js" -import { FormRoot } from "../../../../components/forms/formRoot.js" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" -import { invalidateData } from "../../../../utilities/invalidateData.js" -import { useDataFromAPI } from "../../../../utilities/useHTTPData.ts" - -const rootOptionKey = "__root__" - -function getDescendantFolderIds(parameters: { - folderId: string - folders: Array> -}) { - const childrenByParentId = new Map>() - - for (const folder of parameters.folders) { - if (folder.idFolderParent === null) { - continue - } - - const currentChildren = childrenByParentId.get(folder.idFolderParent) ?? [] - childrenByParentId.set(folder.idFolderParent, [ - ...currentChildren, - folder.id, - ]) - } - - const descendants = new Set() - const stack = [ - ...(childrenByParentId.get(parameters.folderId) ?? []), - ] - - while (stack.length > 0) { - const current = stack.pop() - - if (current === undefined || descendants.has(current)) { - continue - } - - descendants.add(current) - stack.push(...(childrenByParentId.get(current) ?? [])) - } - - return descendants -} - -export function MoveOneFolderForm(props: { - folder: v.InferOutput - onSuccess?: () => void -}) { - const foldersResponse = useDataFromAPI({ - routeDefinition: readAllFoldersRouteDefinition, - body: { - idYear: props.folder.idYear, - }, - }) - - const folderOptions = useMemo(() => { - const allFolders = foldersResponse.data ?? [] - const blockedFolderIds = getDescendantFolderIds({ - folderId: props.folder.id, - folders: allFolders, - }) - blockedFolderIds.add(props.folder.id) - - return [ - { - key: rootOptionKey, - label: "/", - }, - ...allFolders - .filter((folder) => !blockedFolderIds.has(folder.id)) - .map((folder) => ({ - key: folder.id, - label: folder.name, - })), - ] - }, [ - foldersResponse.data, - props.folder.id, - ]) - - return ( - , - text: "Déplacer le dossier", - }} - onSubmit={async (data) => { - const updateResponse = await getResponseBodyFromAPI({ - routeDefinition: updateOneFolderRouteDefinition, - body: { - idFolder: props.folder.id, - idYear: data.idYear, - idFolderParent: data.idFolderParent, - }, - }) - - if (updateResponse.ok === false) { - toast({ - title: "Impossible de déplacer le dossier", - variant: "error", - }) - return false - } - - toast({ - title: "Dossier déplacé avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await invalidateData({ - routeDefinition: readAllFoldersRouteDefinition, - body: { - idYear: props.folder.idYear, - }, - }) - - props.onSuccess?.() - }} - > - {(form) => ( - <> - ( - - - - - field.onChange(value === rootOptionKey ? null : (value ?? null)) - } - options={folderOptions} - placeholder="Sélectionner un dossier" - isLoading={foldersResponse.isPending} - /> - - - - )} - /> - - )} - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/files/updateOneFolderForm.tsx b/packages/website/src/features/dashboard/$idYear/files/updateOneFolderForm.tsx deleted file mode 100644 index 91e9e8ee..00000000 --- a/packages/website/src/features/dashboard/$idYear/files/updateOneFolderForm.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { readAllFoldersRouteDefinition, updateOneFolderRouteDefinition } from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { InputText, toast } from "@arrhes/ui" -import { IconPencil } from "@tabler/icons-react" -import type * as v from "valibot" -import { FormControl } from "../../../../components/forms/formControl.js" -import { FormError } from "../../../../components/forms/formError.js" -import { FormField } from "../../../../components/forms/formField.js" -import { FormItem } from "../../../../components/forms/formItem.js" -import { FormLabel } from "../../../../components/forms/formLabel.js" -import { FormRoot } from "../../../../components/forms/formRoot.js" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.js" -import { invalidateData } from "../../../../utilities/invalidateData.js" - -export function UpdateOneFolderForm(props: { - folder: v.InferOutput - onSuccess?: () => void -}) { - return ( - , - text: "Modifier le dossier", - }} - onSubmit={async (data) => { - const updateResponse = await getResponseBodyFromAPI({ - routeDefinition: updateOneFolderRouteDefinition, - body: { - idFolder: props.folder.id, - idYear: data.idYear, - name: data.name, - }, - }) - if (updateResponse.ok === false) { - toast({ - title: "Impossible de modifier le dossier", - variant: "error", - }) - return false - } - toast({ - title: "Dossier modifié avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await invalidateData({ - routeDefinition: readAllFoldersRouteDefinition, - body: { - idYear: props.folder.idYear, - }, - }) - props.onSuccess?.() - }} - > - {(form) => ( - ( - - - - - - - - )} - /> - )} - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/reports/reportFilterPopover.tsx b/packages/website/src/features/dashboard/$idYear/reports/ReportFilterPopover.tsx similarity index 100% rename from packages/website/src/features/dashboard/$idYear/reports/reportFilterPopover.tsx rename to packages/website/src/features/dashboard/$idYear/reports/ReportFilterPopover.tsx diff --git a/packages/website/src/features/dashboard/$idYear/reports/ReportsLayout.tsx b/packages/website/src/features/dashboard/$idYear/reports/ReportsLayout.tsx new file mode 100644 index 00000000..d642887f --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/reports/ReportsLayout.tsx @@ -0,0 +1,5 @@ +import { Outlet } from "@tanstack/react-router" + +export function ReportsLayout() { + return +} diff --git a/packages/website/src/features/dashboard/$idYear/reports/ReportsTabContent.tsx b/packages/website/src/features/dashboard/$idYear/reports/ReportsTabContent.tsx new file mode 100644 index 00000000..f310e727 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/reports/ReportsTabContent.tsx @@ -0,0 +1,107 @@ +import { IconBook, IconBook2, IconChartBar, IconReport, IconReportMoney, IconScale } from "@tabler/icons-react" +import { lazy, Suspense } from "react" +import { SubPageContent } from "../../../../components/layouts/SubPageContent.tsx" + +const JournalReportPage = lazy(() => + import("./journalReport/JournalReportPage.js").then((m) => ({ + default: m.JournalReportPage, + })), +) +const LedgerReportPage = lazy(() => + import("./ledgerReport/LedgerReportPage.js").then((m) => ({ + default: m.LedgerReportPage, + })), +) +const BalanceReportPage = lazy(() => + import("./balanceReport/BalanceReportPage.js").then((m) => ({ + default: m.BalanceReportPage, + })), +) +const BalanceSheetReportPage = lazy(() => + import("./balanceSheetReport/BalanceSheetReportPage.js").then((m) => ({ + default: m.BalanceSheetReportPage, + })), +) +const IncomeStatementReportPage = lazy(() => + import("./incomeStatementReport/IncomeStatementReportPage.js").then((m) => ({ + default: m.IncomeStatementReportPage, + })), +) + +export function ReportsTabContent(props: { idOrganization: string; idYear: string }) { + return ( + , + items: [ + { + key: "livre-journal", + label: "Livre-journal", + icon: , + content: ( + + + + ), + }, + { + key: "grand-livre", + label: "Grand livre", + icon: , + content: ( + + + + ), + }, + ], + }, + summaries: { + title: "Synthèses", + icon: , + items: [ + { + key: "balance", + label: "Balance", + icon: , + content: ( + + + + ), + }, + { + key: "bilan", + label: "Bilan", + icon: , + content: ( + + + + ), + }, + { + key: "compte-de-résultat", + label: "Compte de résultat", + icon: , + content: ( + + + + ), + }, + ], + }, + }} + /> + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/reports/balanceReport/BalanceReportPage.tsx b/packages/website/src/features/dashboard/$idYear/reports/balanceReport/BalanceReportPage.tsx new file mode 100644 index 00000000..b565a17d --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/reports/balanceReport/BalanceReportPage.tsx @@ -0,0 +1,107 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import { useParams } from "@tanstack/react-router" +import { useState } from "react" +import { Box } from "../../../../../components/layouts/Box.tsx" +import { Page } from "../../../../../components/layouts/page/page.tsx" +import { Section } from "../../../../../components/layouts/section/section.tsx" +import type { YearDataKey } from "../../YearDataWrapper.tsx" +import { YearDataWrapper } from "../../YearDataWrapper.tsx" +import { ReportFilterPopover } from "../ReportFilterPopover.tsx" +import { BalanceReportTable } from "./BalanceReportTable.tsx" + +const requiredKeys = [ + "accounts", + "entries", + "entryLines", + "journals", + "tags", + "entryTags", +] as const satisfies readonly YearDataKey[] + +export function BalanceReportPage({ idYear: idYearProp }: { idYear?: string } = {}) { + const params = useParams({ + strict: false, + }) as { + idYear?: string + } + const idYear = idYearProp ?? params.idYear ?? "" + const [selectedJournalId, setSelectedJournalId] = useState(null) + const [selectedTags, setSelectedTags] = useState< + Array<{ + key: string + label: string + }> + >([]) + + return ( + + {({ accounts, entries, entryLines, journals, tags, entryTags }) => { + let filteredEntryLines = entryLines.filter((entryLine) => entryLine.isComputedForBalanceReport === true) + const filteredAccounts = accounts + + const journalOptions = journals.map((j) => ({ + key: j.id, + label: `${j.code} ${j.label ?? ""}`.trim(), + })) + + const tagOptions = tags.map((t) => ({ + key: t.id, + label: t.label, + })) + + if (selectedJournalId) { + const matchingEntryIds = new Set( + entries.filter((entry) => entry.idJournal === selectedJournalId).map((entry) => entry.id), + ) + filteredEntryLines = filteredEntryLines.filter((el) => matchingEntryIds.has(el.idEntry)) + } + + if (selectedTags.length > 0) { + const selectedTagIds = new Set(selectedTags.map((t) => t.key)) + const matchingEntryIds = new Set( + entryTags.filter((et) => selectedTagIds.has(et.idTag)).map((et) => et.idEntry), + ) + filteredEntryLines = filteredEntryLines.filter((el) => matchingEntryIds.has(el.idEntry)) + } + + return ( + + + + +
+ +
+ + + +
+
+
+
+ ) + }} +
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/reports/balanceReport/balanceReportTable.tsx b/packages/website/src/features/dashboard/$idYear/reports/balanceReport/BalanceReportTable.tsx similarity index 100% rename from packages/website/src/features/dashboard/$idYear/reports/balanceReport/balanceReportTable.tsx rename to packages/website/src/features/dashboard/$idYear/reports/balanceReport/BalanceReportTable.tsx diff --git a/packages/website/src/features/dashboard/$idYear/reports/balanceReport/balanceReportPage.tsx b/packages/website/src/features/dashboard/$idYear/reports/balanceReport/balanceReportPage.tsx deleted file mode 100644 index 6d46cd55..00000000 --- a/packages/website/src/features/dashboard/$idYear/reports/balanceReport/balanceReportPage.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" -import { useParams } from "@tanstack/react-router" -import { useState } from "react" -import { Box } from "../../../../../components/layouts/box.tsx" -import { Page } from "../../../../../components/layouts/page/page.tsx" -import { Section } from "../../../../../components/layouts/section/section.tsx" -import { balanceReportRoute } from "../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/reports/balanceReportRoute.tsx" -import type { YearDataKey } from "../../yearDataWrapper.tsx" -import { YearDataWrapper } from "../../yearDataWrapper.tsx" -import { ReportFilterPopover } from "../reportFilterPopover.tsx" -import { BalanceReportTable } from "./balanceReportTable.tsx" - -const requiredKeys = [ - "accounts", - "entries", - "entryLines", - "journals", - "tags", - "entryTags", -] as const satisfies readonly YearDataKey[] - -export function BalanceReportPage() { - const params = useParams({ - from: balanceReportRoute.id, - }) - const [selectedJournalId, setSelectedJournalId] = useState(null) - const [selectedTags, setSelectedTags] = useState< - Array<{ - key: string - label: string - }> - >([]) - - return ( - - {({ accounts, entries, entryLines, journals, tags, entryTags }) => { - let filteredEntryLines = entryLines.filter((entryLine) => entryLine.isComputedForBalanceReport === true) - const filteredAccounts = accounts - - const journalOptions = journals.map((j) => ({ - key: j.id, - label: `${j.code} ${j.label ?? ""}`.trim(), - })) - - const tagOptions = tags.map((t) => ({ - key: t.id, - label: t.label, - })) - - if (selectedJournalId) { - const matchingEntryIds = new Set( - entries.filter((entry) => entry.idJournal === selectedJournalId).map((entry) => entry.id), - ) - filteredEntryLines = filteredEntryLines.filter((el) => matchingEntryIds.has(el.idEntry)) - } - - if (selectedTags.length > 0) { - const selectedTagIds = new Set(selectedTags.map((t) => t.key)) - const matchingEntryIds = new Set( - entryTags.filter((et) => selectedTagIds.has(et.idTag)).map((et) => et.idEntry), - ) - filteredEntryLines = filteredEntryLines.filter((el) => matchingEntryIds.has(el.idEntry)) - } - - return ( - - - - -
- -
- - - -
-
-
-
- ) - }} -
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/BalanceSheetReportPage.tsx b/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/BalanceSheetReportPage.tsx new file mode 100644 index 00000000..698bb29b --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/BalanceSheetReportPage.tsx @@ -0,0 +1,199 @@ +import { Button, ButtonGhostContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { useParams } from "@tanstack/react-router" +import { useState } from "react" +import { Box } from "../../../../../components/layouts/Box.tsx" +import { Page } from "../../../../../components/layouts/page/page.tsx" +import { Section } from "../../../../../components/layouts/section/section.tsx" +import type { YearDataKey } from "../../YearDataWrapper.tsx" +import { YearDataWrapper } from "../../YearDataWrapper.tsx" +import { ReportFilterPopover } from "../ReportFilterPopover.tsx" +import { BalanceSheetAssetsReportTable } from "./balanceSheetAsset/BalanceSheetAssetsReportTable.tsx" +import { BalanceSheetLiabilitiesReportTable } from "./balanceSheetLiability/BalanceSheetLiabilitiesReportTable.tsx" +import { DownloadBalanceSheetReport } from "./DownloadBalanceSheetReport.tsx" + +const requiredKeys = [ + "accounts", + "entries", + "entryLines", + "balanceSheets", + "journals", + "tags", + "entryTags", +] as const satisfies readonly YearDataKey[] + +export function BalanceSheetReportPage({ + idOrganization: idOrganizationProp, + idYear: idYearProp, +}: { + idOrganization?: string + idYear?: string +} = {}) { + const params = useParams({ + strict: false, + }) as { + idOrganization?: string + idYear?: string + } + const idOrganization = idOrganizationProp ?? params.idOrganization ?? "" + const idYear = idYearProp ?? params.idYear ?? "" + const [selectedJournalId, setSelectedJournalId] = useState(null) + const [selectedTags, setSelectedTags] = useState< + Array<{ + key: string + label: string + }> + >([]) + const [activeTab, setActiveTab] = useState<"asset" | "liability">("asset") + return ( + + {({ accounts, entries, entryLines, balanceSheets, journals, tags, entryTags }) => { + let filteredEntryLines = entryLines.filter( + (entryLine) => entryLine.isComputedForBalanceSheetReport === true, + ) + const filteredAccounts = accounts.filter((account) => account.type === "balance-sheet") + + const journalOptions = journals.map((j) => ({ + key: j.id, + label: `${j.code} ${j.label ?? ""}`.trim(), + })) + + const tagOptions = tags.map((t) => ({ + key: t.id, + label: t.label, + })) + + if (selectedJournalId) { + const matchingEntryIds = new Set( + entries.filter((entry) => entry.idJournal === selectedJournalId).map((entry) => entry.id), + ) + filteredEntryLines = filteredEntryLines.filter((el) => matchingEntryIds.has(el.idEntry)) + } + + if (selectedTags.length > 0) { + const selectedTagIds = new Set(selectedTags.map((t) => t.key)) + const matchingEntryIds = new Set( + entryTags.filter((et) => selectedTagIds.has(et.idTag)).map((et) => et.idEntry), + ) + filteredEntryLines = filteredEntryLines.filter((el) => matchingEntryIds.has(el.idEntry)) + } + + return ( + + + + +
+ + +
+
+
+ + +
+
+ + balanceSheet.side === "asset", + )} + entryLines={filteredEntryLines} + accounts={filteredAccounts} + /> + +
+
+ + balanceSheet.side === "liability", + )} + entryLines={filteredEntryLines} + accounts={filteredAccounts} + /> + +
+
+
+
+
+
+ ) + }} +
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetReportPdf.tsx b/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/BalanceSheetReportPdf.tsx similarity index 100% rename from packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetReportPdf.tsx rename to packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/BalanceSheetReportPdf.tsx diff --git a/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/DownloadBalanceSheetReport.tsx b/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/DownloadBalanceSheetReport.tsx new file mode 100644 index 00000000..fb262803 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/DownloadBalanceSheetReport.tsx @@ -0,0 +1,120 @@ +import { generateBalanceSheetXmlRouteDefinition } from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, ButtonGhostContent, ButtonOutlineContent, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { pdf } from "@react-pdf/renderer" +import { IconDownload, IconFileTypePdf, IconFileTypeXml } from "@tabler/icons-react" +import type * as v from "valibot" +import { Popover } from "../../../../../components/overlays/popover/popover.tsx" +import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" +import { BalanceSheetReportPdf } from "./BalanceSheetReportPdf.tsx" + +export function DownloadBalanceSheetReport(props: { + idOrganization: v.InferOutput["id"] + idYear: v.InferOutput["id"] + balanceSheets: Array> + entryLines: Array> + accounts: Array> +}) { + async function handlePdf() { + const blob = await pdf( + , + ).toBlob() + const objectUrl = URL.createObjectURL(blob) + const link = document.createElement("a") + link.href = objectUrl + link.download = `${props.idOrganization}-${props.idYear}-bilan.pdf` + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(objectUrl) + } + + async function handleXml() { + const generateResponse = await getResponseBodyFromAPI({ + routeDefinition: generateBalanceSheetXmlRouteDefinition, + body: { + idYear: props.idYear, + }, + }) + if (generateResponse.ok === false) { + toast({ + title: "Impossible de générer le fichier XML", + variant: "error", + }) + return + } + + const response = await fetch(generateResponse.data.url) + const blob = await response.blob() + const objectUrl = URL.createObjectURL(blob) + const link = document.createElement("a") + link.href = objectUrl + link.download = `${props.idOrganization}-${props.idYear}-bilan.xml` + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(objectUrl) + } + + return ( + + + + + + + + + + + + + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetAsset/BalanceSheetAssetsReportItem.tsx b/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetAsset/BalanceSheetAssetsReportItem.tsx new file mode 100644 index 00000000..b64c94b9 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetAsset/BalanceSheetAssetsReportItem.tsx @@ -0,0 +1,107 @@ +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { type ComponentProps, Fragment } from "react" +import type * as v from "valibot" +import { toRoman } from "../../../../../../utilities/toRoman.ts" +import { getBalanceSheetChildren } from "../../../yearSettings/balanceSheets/getBalanceSheetChildren.tsx" +import { BalanceSheetAssetsReportRow } from "./BalanceSheetAssetsReportRow.tsx" + +export function BalanceSheetAssetsReportItem(props: { + idOrganization: v.InferOutput["id"] + idYear: v.InferOutput["id"] + accounts: Array> + entryLines: Array> + balanceSheet: v.InferOutput + balanceSheetChildren: Array> + level: number + className?: ComponentProps<"div">["className"] +}) { + const number = props.level === 0 ? toRoman(Number(props.balanceSheet.number)) : null + + const label = props.balanceSheet.label + + const isAmountDisplayed = props.balanceSheet.isComputed === true || props.balanceSheetChildren.length === 0 + + let grossTotalAmount = 0 + let amortizationTotalAmount = 0 + props.accounts + .filter((account) => { + const hasAccount = account.idBalanceSheetAsset === props.balanceSheet.id + const hasChildrenAccount = props.balanceSheetChildren.some( + (balanceSheet) => balanceSheet.id === account.idBalanceSheetAsset, + ) + return hasAccount || hasChildrenAccount + }) + .forEach((account) => { + let accountTotalDebit = 0 + let accountTotalCredit = 0 + + props.entryLines + .filter((entryLine) => entryLine.idAccount === account.id) + .forEach((entryLine) => { + accountTotalDebit += Number(entryLine.debit) + accountTotalCredit += Number(entryLine.credit) + }) + + const accountBalance = accountTotalDebit - accountTotalCredit + + if (accountBalance < 0 && account.balanceSheetAssetFlow === "debit") { + return + } + + if (accountBalance > 0 && account.balanceSheetAssetFlow === "credit") { + return + } + + if (account.balanceSheetAssetColumn === "gross") { + if (account.balanceSheetAssetFlow === "debit") { + grossTotalAmount += Math.abs(accountBalance) + } + if (account.balanceSheetAssetFlow === "credit") { + grossTotalAmount += -Math.abs(accountBalance) + } + } + if (account.balanceSheetAssetColumn === "amortization") { + if (account.balanceSheetAssetFlow === "debit") { + amortizationTotalAmount += Math.abs(accountBalance) + } + if (account.balanceSheetAssetFlow === "credit") { + amortizationTotalAmount += -Math.abs(accountBalance) + } + } + }) + + return ( + + + {props.balanceSheetChildren + .filter((balanceSheet) => balanceSheet.idBalanceSheetParent === props.balanceSheet.id) + .map((balanceSheet) => { + const balanceSheetChildren = getBalanceSheetChildren({ + balanceSheet: balanceSheet, + balanceSheets: props.balanceSheetChildren, + }) + + return ( + + ) + })} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetAsset/balanceSheetAssetsReportRow.tsx b/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetAsset/BalanceSheetAssetsReportRow.tsx similarity index 100% rename from packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetAsset/balanceSheetAssetsReportRow.tsx rename to packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetAsset/BalanceSheetAssetsReportRow.tsx diff --git a/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetAsset/BalanceSheetAssetsReportTable.tsx b/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetAsset/BalanceSheetAssetsReportTable.tsx new file mode 100644 index 00000000..2ee02c80 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetAsset/BalanceSheetAssetsReportTable.tsx @@ -0,0 +1,172 @@ +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { FormatNull } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { Fragment } from "react/jsx-runtime" +import type * as v from "valibot" +import { Table } from "../../../../../../components/layouts/table/table.tsx" +import { getBalanceSheetChildren } from "../../../yearSettings/balanceSheets/getBalanceSheetChildren.tsx" +import { BalanceSheetAssetsReportItem } from "./BalanceSheetAssetsReportItem.tsx" +import { BalanceSheetAssetsReportRow } from "./BalanceSheetAssetsReportRow.tsx" + +export function BalanceSheetAssetsReportTable(props: { + balanceSheets: Array> + entryLines: Array> + accounts: Array> +}) { + let grossTotalAmount = 0 + let amortizationTotalAmount = 0 + props.accounts.forEach((account) => { + let accountTotalDebit = 0 + let accountTotalCredit = 0 + + props.entryLines + .filter((entryLine) => entryLine.idAccount === account.id) + .forEach((entryLine) => { + accountTotalDebit += Number(entryLine.debit) + accountTotalCredit += Number(entryLine.credit) + }) + + const accountBalance = accountTotalDebit - accountTotalCredit + + if (accountBalance < 0 && account.balanceSheetAssetFlow === "debit") { + return + } + + if (accountBalance > 0 && account.balanceSheetAssetFlow === "credit") { + return + } + + if (account.balanceSheetAssetColumn === "gross") { + if (account.balanceSheetAssetFlow === "debit") { + grossTotalAmount += Math.abs(accountBalance) + } + if (account.balanceSheetAssetFlow === "credit") { + grossTotalAmount += -Math.abs(accountBalance) + } + } + if (account.balanceSheetAssetColumn === "amortization") { + if (account.balanceSheetAssetFlow === "debit") { + amortizationTotalAmount += Math.abs(accountBalance) + } + if (account.balanceSheetAssetFlow === "credit") { + amortizationTotalAmount += -Math.abs(accountBalance) + } + } + }) + + return ( +
+ + + + + + + Brut + + + + + Amort. & Dépré. + + + + + Net + + + + + + {props.balanceSheets.length === 0 ? ( + + + + + + + + ) : ( + + {props.balanceSheets + .filter((balanceSheet) => balanceSheet.idBalanceSheetParent === null) + .sort((a, b) => Number(a.number) - Number(b.number)) + .map((balanceSheet) => { + const balanceSheetChildren = getBalanceSheetChildren({ + balanceSheet: balanceSheet, + balanceSheets: props.balanceSheets, + }) + + return ( + + ) + })} + + + )} + + +
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetAsset/balanceSheetAssetsReportItem.tsx b/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetAsset/balanceSheetAssetsReportItem.tsx deleted file mode 100644 index 26d836dc..00000000 --- a/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetAsset/balanceSheetAssetsReportItem.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { type ComponentProps, Fragment } from "react" -import type * as v from "valibot" -import { toRoman } from "../../../../../../utilities/toRoman.ts" -import { getBalanceSheetChildren } from "../../../yearSettings/balanceSheets/getBalanceSheetChildren.tsx" -import { BalanceSheetAssetsReportRow } from "./balanceSheetAssetsReportRow.tsx" - -export function BalanceSheetAssetsReportItem(props: { - idOrganization: v.InferOutput["id"] - idYear: v.InferOutput["id"] - accounts: Array> - entryLines: Array> - balanceSheet: v.InferOutput - balanceSheetChildren: Array> - level: number - className?: ComponentProps<"div">["className"] -}) { - const number = props.level === 0 ? toRoman(Number(props.balanceSheet.number)) : null - - const label = props.balanceSheet.label - - const isAmountDisplayed = props.balanceSheet.isComputed === true || props.balanceSheetChildren.length === 0 - - let grossTotalAmount = 0 - let amortizationTotalAmount = 0 - props.accounts - .filter((account) => { - const hasAccount = account.idBalanceSheetAsset === props.balanceSheet.id - const hasChildrenAccount = props.balanceSheetChildren.some( - (balanceSheet) => balanceSheet.id === account.idBalanceSheetAsset, - ) - return hasAccount || hasChildrenAccount - }) - .forEach((account) => { - let accountTotalDebit = 0 - let accountTotalCredit = 0 - - props.entryLines - .filter((entryLine) => entryLine.idAccount === account.id) - .forEach((entryLine) => { - accountTotalDebit += Number(entryLine.debit) - accountTotalCredit += Number(entryLine.credit) - }) - - const accountBalance = accountTotalDebit - accountTotalCredit - - if (accountBalance < 0 && account.balanceSheetAssetFlow === "debit") { - return - } - - if (accountBalance > 0 && account.balanceSheetAssetFlow === "credit") { - return - } - - if (account.balanceSheetAssetColumn === "gross") { - if (account.balanceSheetAssetFlow === "debit") { - grossTotalAmount += Math.abs(accountBalance) - } - if (account.balanceSheetAssetFlow === "credit") { - grossTotalAmount += -Math.abs(accountBalance) - } - } - if (account.balanceSheetAssetColumn === "amortization") { - if (account.balanceSheetAssetFlow === "debit") { - amortizationTotalAmount += Math.abs(accountBalance) - } - if (account.balanceSheetAssetFlow === "credit") { - amortizationTotalAmount += -Math.abs(accountBalance) - } - } - }) - - return ( - - - {props.balanceSheetChildren - .filter((balanceSheet) => balanceSheet.idBalanceSheetParent === props.balanceSheet.id) - .map((balanceSheet) => { - const balanceSheetChildren = getBalanceSheetChildren({ - balanceSheet: balanceSheet, - balanceSheets: props.balanceSheetChildren, - }) - - return ( - - ) - })} - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetAsset/balanceSheetAssetsReportTable.tsx b/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetAsset/balanceSheetAssetsReportTable.tsx deleted file mode 100644 index 19d11592..00000000 --- a/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetAsset/balanceSheetAssetsReportTable.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { FormatNull } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { Fragment } from "react/jsx-runtime" -import type * as v from "valibot" -import { Table } from "../../../../../../components/layouts/table/table.tsx" -import { getBalanceSheetChildren } from "../../../yearSettings/balanceSheets/getBalanceSheetChildren.tsx" -import { BalanceSheetAssetsReportItem } from "./balanceSheetAssetsReportItem.tsx" -import { BalanceSheetAssetsReportRow } from "./balanceSheetAssetsReportRow.tsx" - -export function BalanceSheetAssetsReportTable(props: { - balanceSheets: Array> - entryLines: Array> - accounts: Array> -}) { - let grossTotalAmount = 0 - let amortizationTotalAmount = 0 - props.accounts.forEach((account) => { - let accountTotalDebit = 0 - let accountTotalCredit = 0 - - props.entryLines - .filter((entryLine) => entryLine.idAccount === account.id) - .forEach((entryLine) => { - accountTotalDebit += Number(entryLine.debit) - accountTotalCredit += Number(entryLine.credit) - }) - - const accountBalance = accountTotalDebit - accountTotalCredit - - if (accountBalance < 0 && account.balanceSheetAssetFlow === "debit") { - return - } - - if (accountBalance > 0 && account.balanceSheetAssetFlow === "credit") { - return - } - - if (account.balanceSheetAssetColumn === "gross") { - if (account.balanceSheetAssetFlow === "debit") { - grossTotalAmount += Math.abs(accountBalance) - } - if (account.balanceSheetAssetFlow === "credit") { - grossTotalAmount += -Math.abs(accountBalance) - } - } - if (account.balanceSheetAssetColumn === "amortization") { - if (account.balanceSheetAssetFlow === "debit") { - amortizationTotalAmount += Math.abs(accountBalance) - } - if (account.balanceSheetAssetFlow === "credit") { - amortizationTotalAmount += -Math.abs(accountBalance) - } - } - }) - - return ( -
- - - - - - - Brut - - - - - Amort. & Dépré. - - - - - Net - - - - - - {props.balanceSheets.length === 0 ? ( - - - - - - - - ) : ( - - {props.balanceSheets - .filter((balanceSheet) => balanceSheet.idBalanceSheetParent === null) - .sort((a, b) => Number(a.number) - Number(b.number)) - .map((balanceSheet) => { - const balanceSheetChildren = getBalanceSheetChildren({ - balanceSheet: balanceSheet, - balanceSheets: props.balanceSheets, - }) - - return ( - - ) - })} - - - )} - - -
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetLiability/BalanceSheetLiabilitiesReportItem.tsx b/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetLiability/BalanceSheetLiabilitiesReportItem.tsx new file mode 100644 index 00000000..e9759d9d --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetLiability/BalanceSheetLiabilitiesReportItem.tsx @@ -0,0 +1,97 @@ +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { type ComponentProps, Fragment } from "react" +import type * as v from "valibot" +import { toRoman } from "../../../../../../utilities/toRoman.ts" +import { getBalanceSheetChildren } from "../../../yearSettings/balanceSheets/getBalanceSheetChildren.tsx" +import { BalanceSheetLiabilitiesReportRow } from "./BalanceSheetLiabilitiesReportRow.tsx" + +export function BalanceSheetLiabilitiesReportItem(props: { + idOrganization: v.InferOutput["id"] + idYear: v.InferOutput["id"] + accounts: Array> + entryLines: Array> + balanceSheet: v.InferOutput + balanceSheetChildren: Array> + level: number + className?: ComponentProps<"div">["className"] +}) { + const number = props.level === 0 ? toRoman(Number(props.balanceSheet.number)) : null + + const label = props.balanceSheet.label + + const isAmountDisplayed = props.balanceSheet.isComputed === true || props.balanceSheetChildren.length === 0 + + let netTotalAmount = 0 + props.accounts + .filter((account) => { + const hasAccount = account.idBalanceSheetLiability === props.balanceSheet.id + const hasChildrenAccount = props.balanceSheetChildren.some( + (balanceSheet) => balanceSheet.id === account.idBalanceSheetLiability, + ) + return hasAccount || hasChildrenAccount + }) + .forEach((account) => { + let accountTotalDebit = 0 + let accountTotalCredit = 0 + + props.entryLines + .filter((entryLine) => entryLine.idAccount === account.id) + .forEach((entryLine) => { + accountTotalDebit += Number(entryLine.debit) + accountTotalCredit += Number(entryLine.credit) + }) + + const accountBalance = accountTotalCredit - accountTotalDebit + + if (accountBalance > 0 && account.balanceSheetLiabilityFlow === "debit") { + return + } + + if (accountBalance < 0 && account.balanceSheetLiabilityFlow === "credit") { + return + } + + if (account.balanceSheetLiabilityColumn === "net") { + if (account.balanceSheetLiabilityFlow === "debit") { + netTotalAmount += accountBalance + } + if (account.balanceSheetLiabilityFlow === "credit") { + netTotalAmount += accountBalance + } + } + }) + + return ( + + + {props.balanceSheetChildren + .filter((balanceSheet) => balanceSheet.idBalanceSheetParent === props.balanceSheet.id) + .map((balanceSheet) => { + const balanceSheetChildren = getBalanceSheetChildren({ + balanceSheet: balanceSheet, + balanceSheets: props.balanceSheetChildren, + }) + + return ( + + ) + })} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetLiability/balanceSheetLiabilityiesReportRow.tsx b/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetLiability/BalanceSheetLiabilitiesReportRow.tsx similarity index 100% rename from packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetLiability/balanceSheetLiabilityiesReportRow.tsx rename to packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetLiability/BalanceSheetLiabilitiesReportRow.tsx diff --git a/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetLiability/BalanceSheetLiabilitiesReportTable.tsx b/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetLiability/BalanceSheetLiabilitiesReportTable.tsx new file mode 100644 index 00000000..2fa249af --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetLiability/BalanceSheetLiabilitiesReportTable.tsx @@ -0,0 +1,131 @@ +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { FormatNull } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { Fragment } from "react/jsx-runtime" +import type * as v from "valibot" +import { Table } from "../../../../../../components/layouts/table/table.tsx" +import { getBalanceSheetChildren } from "../../../yearSettings/balanceSheets/getBalanceSheetChildren.tsx" +import { BalanceSheetLiabilitiesReportItem } from "./BalanceSheetLiabilitiesReportItem.tsx" +import { BalanceSheetLiabilitiesReportRow } from "./BalanceSheetLiabilitiesReportRow.tsx" + +export function BalanceSheetLiabilitiesReportTable(props: { + balanceSheets: Array> + entryLines: Array> + accounts: Array> +}) { + let netTotalAmount = 0 + props.accounts.forEach((account) => { + let accountTotalDebit = 0 + let accountTotalCredit = 0 + + props.entryLines + .filter((entryLine) => entryLine.idAccount === account.id) + .forEach((entryLine) => { + accountTotalDebit += Number(entryLine.debit) + accountTotalCredit += Number(entryLine.credit) + }) + + const accountBalance = accountTotalDebit - accountTotalCredit + + if (accountBalance < 0 && account.balanceSheetLiabilityFlow === "debit") { + return + } + + if (accountBalance > 0 && account.balanceSheetLiabilityFlow === "credit") { + return + } + + if (account.balanceSheetLiabilityColumn === "net") { + if (account.balanceSheetLiabilityFlow === "debit") { + netTotalAmount += -Math.abs(accountBalance) + } + if (account.balanceSheetLiabilityFlow === "credit") { + netTotalAmount += Math.abs(accountBalance) + } + } + }) + + return ( +
+ + + + + + + Net + + + + + + {props.balanceSheets.length === 0 ? ( + + + + + + + + ) : ( + + {props.balanceSheets + .filter((balanceSheet) => balanceSheet.idBalanceSheetParent === null) + .sort((a, b) => Number(a.number) - Number(b.number)) + .map((balanceSheet) => { + const balanceSheetChildren = getBalanceSheetChildren({ + balanceSheet: balanceSheet, + balanceSheets: props.balanceSheets, + }) + + return ( + + ) + })} + + + )} + + +
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetLiability/balanceSheetLiabilitiesReportItem.tsx b/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetLiability/balanceSheetLiabilitiesReportItem.tsx deleted file mode 100644 index a6ccd2a8..00000000 --- a/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetLiability/balanceSheetLiabilitiesReportItem.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { type ComponentProps, Fragment } from "react" -import type * as v from "valibot" -import { toRoman } from "../../../../../../utilities/toRoman.ts" -import { getBalanceSheetChildren } from "../../../yearSettings/balanceSheets/getBalanceSheetChildren.tsx" -import { BalanceSheetLiabilitiesReportRow } from "./balanceSheetLiabilityiesReportRow.tsx" - -export function BalanceSheetLiabilitiesReportItem(props: { - idOrganization: v.InferOutput["id"] - idYear: v.InferOutput["id"] - accounts: Array> - entryLines: Array> - balanceSheet: v.InferOutput - balanceSheetChildren: Array> - level: number - className?: ComponentProps<"div">["className"] -}) { - const number = props.level === 0 ? toRoman(Number(props.balanceSheet.number)) : null - - const label = props.balanceSheet.label - - const isAmountDisplayed = props.balanceSheet.isComputed === true || props.balanceSheetChildren.length === 0 - - let netTotalAmount = 0 - props.accounts - .filter((account) => { - const hasAccount = account.idBalanceSheetLiability === props.balanceSheet.id - const hasChildrenAccount = props.balanceSheetChildren.some( - (balanceSheet) => balanceSheet.id === account.idBalanceSheetLiability, - ) - return hasAccount || hasChildrenAccount - }) - .forEach((account) => { - let accountTotalDebit = 0 - let accountTotalCredit = 0 - - props.entryLines - .filter((entryLine) => entryLine.idAccount === account.id) - .forEach((entryLine) => { - accountTotalDebit += Number(entryLine.debit) - accountTotalCredit += Number(entryLine.credit) - }) - - const accountBalance = accountTotalCredit - accountTotalDebit - - if (accountBalance > 0 && account.balanceSheetLiabilityFlow === "debit") { - return - } - - if (accountBalance < 0 && account.balanceSheetLiabilityFlow === "credit") { - return - } - - if (account.balanceSheetLiabilityColumn === "net") { - if (account.balanceSheetLiabilityFlow === "debit") { - netTotalAmount += accountBalance - } - if (account.balanceSheetLiabilityFlow === "credit") { - netTotalAmount += accountBalance - } - } - }) - - return ( - - - {props.balanceSheetChildren - .filter((balanceSheet) => balanceSheet.idBalanceSheetParent === props.balanceSheet.id) - .map((balanceSheet) => { - const balanceSheetChildren = getBalanceSheetChildren({ - balanceSheet: balanceSheet, - balanceSheets: props.balanceSheetChildren, - }) - - return ( - - ) - })} - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetLiability/balanceSheetLiabilitiesReportTable.tsx b/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetLiability/balanceSheetLiabilitiesReportTable.tsx deleted file mode 100644 index 1f8b315f..00000000 --- a/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetLiability/balanceSheetLiabilitiesReportTable.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { FormatNull } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { Fragment } from "react/jsx-runtime" -import type * as v from "valibot" -import { Table } from "../../../../../../components/layouts/table/table.tsx" -import { getBalanceSheetChildren } from "../../../yearSettings/balanceSheets/getBalanceSheetChildren.tsx" -import { BalanceSheetLiabilitiesReportItem } from "./balanceSheetLiabilitiesReportItem.tsx" -import { BalanceSheetLiabilitiesReportRow } from "./balanceSheetLiabilityiesReportRow.tsx" - -export function BalanceSheetLiabilitiesReportTable(props: { - balanceSheets: Array> - entryLines: Array> - accounts: Array> -}) { - let netTotalAmount = 0 - props.accounts.forEach((account) => { - let accountTotalDebit = 0 - let accountTotalCredit = 0 - - props.entryLines - .filter((entryLine) => entryLine.idAccount === account.id) - .forEach((entryLine) => { - accountTotalDebit += Number(entryLine.debit) - accountTotalCredit += Number(entryLine.credit) - }) - - const accountBalance = accountTotalDebit - accountTotalCredit - - if (accountBalance < 0 && account.balanceSheetLiabilityFlow === "debit") { - return - } - - if (accountBalance > 0 && account.balanceSheetLiabilityFlow === "credit") { - return - } - - if (account.balanceSheetLiabilityColumn === "net") { - if (account.balanceSheetLiabilityFlow === "debit") { - netTotalAmount += -Math.abs(accountBalance) - } - if (account.balanceSheetLiabilityFlow === "credit") { - netTotalAmount += Math.abs(accountBalance) - } - } - }) - - return ( -
- - - - - - - Net - - - - - - {props.balanceSheets.length === 0 ? ( - - - - - - - - ) : ( - - {props.balanceSheets - .filter((balanceSheet) => balanceSheet.idBalanceSheetParent === null) - .sort((a, b) => Number(a.number) - Number(b.number)) - .map((balanceSheet) => { - const balanceSheetChildren = getBalanceSheetChildren({ - balanceSheet: balanceSheet, - balanceSheets: props.balanceSheets, - }) - - return ( - - ) - })} - - - )} - - -
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetReportPage.tsx b/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetReportPage.tsx deleted file mode 100644 index ea89326c..00000000 --- a/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetReportPage.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import { Button, ButtonGhostContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { useParams } from "@tanstack/react-router" -import { useState } from "react" -import { Box } from "../../../../../components/layouts/box.tsx" -import { Page } from "../../../../../components/layouts/page/page.tsx" -import { Section } from "../../../../../components/layouts/section/section.tsx" -import { balanceSheetReportRoute } from "../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/reports/balanceSheetReportRoute.tsx" -import type { YearDataKey } from "../../yearDataWrapper.tsx" -import { YearDataWrapper } from "../../yearDataWrapper.tsx" -import { ReportFilterPopover } from "../reportFilterPopover.tsx" -import { BalanceSheetAssetsReportTable } from "./balanceSheetAsset/balanceSheetAssetsReportTable.tsx" -import { BalanceSheetLiabilitiesReportTable } from "./balanceSheetLiability/balanceSheetLiabilitiesReportTable.tsx" -import { DownloadBalanceSheetReport } from "./downloadBalanceSheetReport.tsx" - -const requiredKeys = [ - "accounts", - "entries", - "entryLines", - "balanceSheets", - "journals", - "tags", - "entryTags", -] as const satisfies readonly YearDataKey[] - -export function BalanceSheetReportPage() { - const params = useParams({ - from: balanceSheetReportRoute.id, - }) - const [selectedJournalId, setSelectedJournalId] = useState(null) - const [selectedTags, setSelectedTags] = useState< - Array<{ - key: string - label: string - }> - >([]) - const [activeTab, setActiveTab] = useState<"asset" | "liability">("asset") - return ( - - {({ accounts, entries, entryLines, balanceSheets, journals, tags, entryTags }) => { - let filteredEntryLines = entryLines.filter( - (entryLine) => entryLine.isComputedForBalanceSheetReport === true, - ) - const filteredAccounts = accounts.filter((account) => account.type === "balance-sheet") - - const journalOptions = journals.map((j) => ({ - key: j.id, - label: `${j.code} ${j.label ?? ""}`.trim(), - })) - - const tagOptions = tags.map((t) => ({ - key: t.id, - label: t.label, - })) - - if (selectedJournalId) { - const matchingEntryIds = new Set( - entries.filter((entry) => entry.idJournal === selectedJournalId).map((entry) => entry.id), - ) - filteredEntryLines = filteredEntryLines.filter((el) => matchingEntryIds.has(el.idEntry)) - } - - if (selectedTags.length > 0) { - const selectedTagIds = new Set(selectedTags.map((t) => t.key)) - const matchingEntryIds = new Set( - entryTags.filter((et) => selectedTagIds.has(et.idTag)).map((et) => et.idEntry), - ) - filteredEntryLines = filteredEntryLines.filter((el) => matchingEntryIds.has(el.idEntry)) - } - - return ( - - - - -
- - -
-
-
- - -
-
- - balanceSheet.side === "asset", - )} - entryLines={filteredEntryLines} - accounts={filteredAccounts} - /> - -
-
- - balanceSheet.side === "liability", - )} - entryLines={filteredEntryLines} - accounts={filteredAccounts} - /> - -
-
-
-
-
-
- ) - }} -
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/downloadBalanceSheetReport.tsx b/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/downloadBalanceSheetReport.tsx deleted file mode 100644 index 8bda96e9..00000000 --- a/packages/website/src/features/dashboard/$idYear/reports/balanceSheetReport/downloadBalanceSheetReport.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { generateBalanceSheetXmlRouteDefinition } from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { Button, ButtonGhostContent, ButtonOutlineContent, toast } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { pdf } from "@react-pdf/renderer" -import { IconDownload, IconFileTypePdf, IconFileTypeXml } from "@tabler/icons-react" -import type * as v from "valibot" -import { Popover } from "../../../../../components/overlays/popover/popover.tsx" -import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" -import { BalanceSheetReportPdf } from "./balanceSheetReportPdf.tsx" - -export function DownloadBalanceSheetReport(props: { - idOrganization: v.InferOutput["id"] - idYear: v.InferOutput["id"] - balanceSheets: Array> - entryLines: Array> - accounts: Array> -}) { - async function handlePdf() { - const blob = await pdf( - , - ).toBlob() - const objectUrl = URL.createObjectURL(blob) - const link = document.createElement("a") - link.href = objectUrl - link.download = `${props.idOrganization}-${props.idYear}-bilan.pdf` - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - URL.revokeObjectURL(objectUrl) - } - - async function handleXml() { - const generateResponse = await getResponseBodyFromAPI({ - routeDefinition: generateBalanceSheetXmlRouteDefinition, - body: { - idYear: props.idYear, - }, - }) - if (generateResponse.ok === false) { - toast({ - title: "Impossible de générer le fichier XML", - variant: "error", - }) - return - } - - const response = await fetch(generateResponse.data.url) - const blob = await response.blob() - const objectUrl = URL.createObjectURL(blob) - const link = document.createElement("a") - link.href = objectUrl - link.download = `${props.idOrganization}-${props.idYear}-bilan.xml` - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - URL.revokeObjectURL(objectUrl) - } - - return ( - - - - - - - - - - - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/DownloadIncomeStatementReport.tsx b/packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/DownloadIncomeStatementReport.tsx new file mode 100644 index 00000000..179b9393 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/DownloadIncomeStatementReport.tsx @@ -0,0 +1,124 @@ +import { generateIncomeStatementXmlRouteDefinition } from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, ButtonGhostContent, ButtonOutlineContent, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { pdf } from "@react-pdf/renderer" +import { IconDownload, IconFileTypePdf, IconFileTypeXml } from "@tabler/icons-react" +import type * as v from "valibot" +import { Popover } from "../../../../../components/overlays/popover/popover.tsx" +import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" +import { IncomeStatementReportPdf } from "./IncomeStatementReportPdf.tsx" + +export function DownloadIncomeStatementReport(props: { + idOrganization: v.InferOutput["id"] + idYear: v.InferOutput["id"] + incomeStatements: Array> + computations: Array> + computationIncomeStatements: Array> + entryLines: Array> + accounts: Array> +}) { + async function handlePdf() { + const blob = await pdf( + , + ).toBlob() + const objectUrl = URL.createObjectURL(blob) + const link = document.createElement("a") + link.href = objectUrl + link.download = `${props.idOrganization}-${props.idYear}-compte-de-résultat.pdf` + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(objectUrl) + } + + async function handleXml() { + const generateResponse = await getResponseBodyFromAPI({ + routeDefinition: generateIncomeStatementXmlRouteDefinition, + body: { + idYear: props.idYear, + }, + }) + if (generateResponse.ok === false) { + toast({ + title: "Impossible de générer le fichier XBRL", + variant: "error", + }) + return + } + + const response = await fetch(generateResponse.data.url) + const blob = await response.blob() + const objectUrl = URL.createObjectURL(blob) + const link = document.createElement("a") + link.href = objectUrl + link.download = `${props.idOrganization}-${props.idYear}-compte-de-résultat.xml` + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(objectUrl) + } + + return ( + + + + + + + + + + + + + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/IncomeStatementReportItem.tsx b/packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/IncomeStatementReportItem.tsx new file mode 100644 index 00000000..4b2dd4d5 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/IncomeStatementReportItem.tsx @@ -0,0 +1,82 @@ +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { type ComponentProps, Fragment } from "react" +import type * as v from "valibot" +import { toRoman } from "../../../../../utilities/toRoman.ts" +import { getIncomeStatementChildren } from "../../yearSettings/incomeStatements/getIncomeStatementChildren.tsx" +import { IncomeStatementReportRow } from "./IncomeStatementReportRow.tsx" + +export function IncomeStatementReportItem(props: { + idOrganization: v.InferOutput["id"] + idYear: v.InferOutput["id"] + accounts: Array> + entryLines: Array> + incomeStatement: v.InferOutput + incomeStatementChildren: Array> + level: number + className?: ComponentProps<"div">["className"] +}) { + const number = props.level === 0 ? toRoman(Number(props.incomeStatement.number)) : null + + const label = props.incomeStatement.label + + const isAmountDisplayed = props.incomeStatement.isComputed === true || props.incomeStatementChildren.length === 0 + + let netAmount = 0 + props.accounts + .filter((account) => { + const hasAccount = props.incomeStatement.id === account.idIncomeStatement + const hasChildrenAccount = props.incomeStatementChildren.some( + (incomeStatement) => incomeStatement.id === account.idIncomeStatement, + ) + return hasAccount || hasChildrenAccount + }) + .forEach((account) => { + let accountTotalDebit = 0 + let accountTotalCredit = 0 + + props.entryLines + .filter((entryLine) => entryLine.idAccount === account.id) + .forEach((entryLine) => { + accountTotalDebit += Number(entryLine.debit) + accountTotalCredit += Number(entryLine.credit) + }) + + const accountBalance = accountTotalDebit - accountTotalCredit + + netAmount += Math.abs(accountBalance) + }) + + return ( + + + {props.incomeStatementChildren + .filter((incomeStatement) => incomeStatement.idIncomeStatementParent === props.incomeStatement.id) + .map((incomeStatement) => { + const incomeStatementChildren = getIncomeStatementChildren({ + incomeStatement: incomeStatement, + incomeStatements: props.incomeStatementChildren, + }) + + return ( + + ) + })} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/IncomeStatementReportPage.tsx b/packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/IncomeStatementReportPage.tsx new file mode 100644 index 00000000..f4515283 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/IncomeStatementReportPage.tsx @@ -0,0 +1,149 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import { useParams } from "@tanstack/react-router" +import { useState } from "react" +import { Box } from "../../../../../components/layouts/Box.tsx" +import { Page } from "../../../../../components/layouts/page/page.tsx" +import { Section } from "../../../../../components/layouts/section/section.tsx" +import type { YearDataKey } from "../../YearDataWrapper.tsx" +import { YearDataWrapper } from "../../YearDataWrapper.tsx" +import { ReportFilterPopover } from "../ReportFilterPopover.tsx" +import { DownloadIncomeStatementReport } from "./DownloadIncomeStatementReport.tsx" +import { IncomeStatementsReportTable } from "./IncomeStatementsReportTable.tsx" + +const requiredKeys = [ + "accounts", + "entries", + "entryLines", + "incomeStatements", + "computations", + "computationIncomeStatements", + "journals", + "tags", + "entryTags", +] as const satisfies readonly YearDataKey[] + +export function IncomeStatementReportPage({ + idOrganization: idOrganizationProp, + idYear: idYearProp, +}: { + idOrganization?: string + idYear?: string +} = {}) { + const params = useParams({ + strict: false, + }) as { + idOrganization?: string + idYear?: string + } + const idOrganization = idOrganizationProp ?? params.idOrganization ?? "" + const idYear = idYearProp ?? params.idYear ?? "" + const [selectedJournalId, setSelectedJournalId] = useState(null) + const [selectedTags, setSelectedTags] = useState< + Array<{ + key: string + label: string + }> + >([]) + + return ( + + {({ + accounts, + entries, + entryLines, + incomeStatements, + computations, + computationIncomeStatements, + journals, + tags, + entryTags, + }) => { + let filteredEntryLines = entryLines.filter( + (entryLine) => entryLine.isComputedForIncomeStatementReport === true, + ) + const filteredAccounts = accounts.filter((account) => account.type === "income-statement") + + const journalOptions = journals.map((j) => ({ + key: j.id, + label: `${j.code} ${j.label ?? ""}`.trim(), + })) + + const tagOptions = tags.map((t) => ({ + key: t.id, + label: t.label, + })) + + if (selectedJournalId) { + const matchingEntryIds = new Set( + entries.filter((entry) => entry.idJournal === selectedJournalId).map((entry) => entry.id), + ) + filteredEntryLines = filteredEntryLines.filter((el) => matchingEntryIds.has(el.idEntry)) + } + + if (selectedTags.length > 0) { + const selectedTagIds = new Set(selectedTags.map((t) => t.key)) + const matchingEntryIds = new Set( + entryTags.filter((et) => selectedTagIds.has(et.idTag)).map((et) => et.idEntry), + ) + filteredEntryLines = filteredEntryLines.filter((el) => matchingEntryIds.has(el.idEntry)) + } + + return ( + + + + +
+ + +
+
+ + + +
+
+
+
+
+ ) + }} +
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/incomeStatementReportPdf.tsx b/packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/IncomeStatementReportPdf.tsx similarity index 100% rename from packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/incomeStatementReportPdf.tsx rename to packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/IncomeStatementReportPdf.tsx diff --git a/packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/incomeStatementReportRow.tsx b/packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/IncomeStatementReportRow.tsx similarity index 100% rename from packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/incomeStatementReportRow.tsx rename to packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/IncomeStatementReportRow.tsx diff --git a/packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/IncomeStatementsReportTable.tsx b/packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/IncomeStatementsReportTable.tsx new file mode 100644 index 00000000..8eff22b0 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/IncomeStatementsReportTable.tsx @@ -0,0 +1,220 @@ +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { FormatNull, FormatPrice, FormatText } from "@arrhes/ui" +import { css, cx } from "@arrhes/ui/utilities/cn.js" +import type * as v from "valibot" +import { Table } from "../../../../../components/layouts/table/table.tsx" +import { toRoman } from "../../../../../utilities/toRoman.ts" +import { getIncomeStatementChildren } from "../../yearSettings/incomeStatements/getIncomeStatementChildren.tsx" +import { IncomeStatementReportItem } from "./IncomeStatementReportItem.tsx" + +export function IncomeStatementsReportTable(props: { + incomeStatements: Array> + computations: Array> + computationIncomeStatements: Array> + entryLines: Array> + accounts: Array> +}) { + return ( +
+ + + + + + + Net + + + + + + {props.incomeStatements + .filter((incomeStatement) => incomeStatement.idIncomeStatementParent === null) + .sort((a, b) => Number(a.number) - Number(b.number)) + .map((incomeStatement) => { + const incomeStatementChildren = getIncomeStatementChildren({ + incomeStatement: incomeStatement, + incomeStatements: props.incomeStatements, + }) + + return ( + + ) + })} + + + {props.computations.length === 0 ? ( + + + + + + + + ) : ( + props.computations.map((computation, index) => { + let computationAmount = 0 + const _computationStatements = props.computationIncomeStatements + .filter( + (computationIncomeStatement) => + computationIncomeStatement.idComputation === computation.id, + ) + .forEach((computationIncomeStatement) => { + let incomeStatementAmount = 0 + props.accounts + .filter((account) => { + const foundIncomeStatement = props.incomeStatements.find( + (incomeStatement) => + incomeStatement.id === computationIncomeStatement.idIncomeStatement, + ) + if (foundIncomeStatement === undefined) { + return false + } + const incomeStatementChildren = getIncomeStatementChildren({ + incomeStatement: foundIncomeStatement, + incomeStatements: props.incomeStatements, + }) + + const hasAccount = + account.idIncomeStatement === + computationIncomeStatement.idIncomeStatement + const hasChildrenAccount = incomeStatementChildren.some( + (incomeStatement) => incomeStatement.id === account.idIncomeStatement, + ) + return hasAccount || hasChildrenAccount + }) + .forEach((account) => { + props.entryLines + .filter((entryLine) => entryLine.idAccount === account.id) + .forEach((entryLine) => { + incomeStatementAmount += + Number(entryLine.debit) - Number(entryLine.credit) + }) + }) + + if (computationIncomeStatement.operation === "plus") { + computationAmount += Math.abs(incomeStatementAmount) + } + if (computationIncomeStatement.operation === "minus") { + computationAmount += -Math.abs(incomeStatementAmount) + } + }) + + // .sort((a, b) => { + // if (!a.incomeStatement || !b.incomeStatement) return 0 + // return (a.incomeStatement.number - b.incomeStatement.number) + // }) + + const computationIncomeStatementsLabel = props.computationIncomeStatements + .filter( + (computationIncomeStatement) => + computationIncomeStatement.idComputation === computation.id, + ) + .map((computationIncomeStatement, computationIncomeStatementIndex) => { + const incomeStatement = props.incomeStatements.find( + (incomeStatement) => + incomeStatement.id === computationIncomeStatement.idIncomeStatement, + ) + if (incomeStatement === undefined) { + return "" + } + const romanNumber = toRoman(Number(incomeStatement.number)) + if (computationIncomeStatement.operation === "plus") { + if (computationIncomeStatementIndex === 0) return `${romanNumber}` + return `+${romanNumber}` + } + if (computationIncomeStatement.operation === "minus") return `-${romanNumber}` + return "" + }) + .join("") + + return ( + + +
+ + {computation.label} + + + {`(${computationIncomeStatementsLabel})`} + +
+
+ + + +
+ ) + }) + )} +
+
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/downloadIncomeStatementReport.tsx b/packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/downloadIncomeStatementReport.tsx deleted file mode 100644 index fc21a670..00000000 --- a/packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/downloadIncomeStatementReport.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { generateIncomeStatementXmlRouteDefinition } from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { Button, ButtonGhostContent, ButtonOutlineContent, toast } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { pdf } from "@react-pdf/renderer" -import { IconDownload, IconFileTypePdf, IconFileTypeXml } from "@tabler/icons-react" -import type * as v from "valibot" -import { Popover } from "../../../../../components/overlays/popover/popover.tsx" -import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" -import { IncomeStatementReportPdf } from "./incomeStatementReportPdf.tsx" - -export function DownloadIncomeStatementReport(props: { - idOrganization: v.InferOutput["id"] - idYear: v.InferOutput["id"] - incomeStatements: Array> - computations: Array> - computationIncomeStatements: Array> - entryLines: Array> - accounts: Array> -}) { - async function handlePdf() { - const blob = await pdf( - , - ).toBlob() - const objectUrl = URL.createObjectURL(blob) - const link = document.createElement("a") - link.href = objectUrl - link.download = `${props.idOrganization}-${props.idYear}-compte-de-résultat.pdf` - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - URL.revokeObjectURL(objectUrl) - } - - async function handleXml() { - const generateResponse = await getResponseBodyFromAPI({ - routeDefinition: generateIncomeStatementXmlRouteDefinition, - body: { - idYear: props.idYear, - }, - }) - if (generateResponse.ok === false) { - toast({ - title: "Impossible de générer le fichier XBRL", - variant: "error", - }) - return - } - - const response = await fetch(generateResponse.data.url) - const blob = await response.blob() - const objectUrl = URL.createObjectURL(blob) - const link = document.createElement("a") - link.href = objectUrl - link.download = `${props.idOrganization}-${props.idYear}-compte-de-résultat.xml` - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - URL.revokeObjectURL(objectUrl) - } - - return ( - - - - - - - - - - - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/incomeStatementReportItem.tsx b/packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/incomeStatementReportItem.tsx deleted file mode 100644 index 380b6a48..00000000 --- a/packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/incomeStatementReportItem.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { type ComponentProps, Fragment } from "react" -import type * as v from "valibot" -import { toRoman } from "../../../../../utilities/toRoman.ts" -import { getIncomeStatementChildren } from "../../yearSettings/incomeStatements/getIncomeStatementChildren.tsx" -import { IncomeStatementReportRow } from "./incomeStatementReportRow.tsx" - -export function IncomeStatementReportItem(props: { - idOrganization: v.InferOutput["id"] - idYear: v.InferOutput["id"] - accounts: Array> - entryLines: Array> - incomeStatement: v.InferOutput - incomeStatementChildren: Array> - level: number - className?: ComponentProps<"div">["className"] -}) { - const number = props.level === 0 ? toRoman(Number(props.incomeStatement.number)) : null - - const label = props.incomeStatement.label - - const isAmountDisplayed = props.incomeStatement.isComputed === true || props.incomeStatementChildren.length === 0 - - let netAmount = 0 - props.accounts - .filter((account) => { - const hasAccount = props.incomeStatement.id === account.idIncomeStatement - const hasChildrenAccount = props.incomeStatementChildren.some( - (incomeStatement) => incomeStatement.id === account.idIncomeStatement, - ) - return hasAccount || hasChildrenAccount - }) - .forEach((account) => { - let accountTotalDebit = 0 - let accountTotalCredit = 0 - - props.entryLines - .filter((entryLine) => entryLine.idAccount === account.id) - .forEach((entryLine) => { - accountTotalDebit += Number(entryLine.debit) - accountTotalCredit += Number(entryLine.credit) - }) - - const accountBalance = accountTotalDebit - accountTotalCredit - - netAmount += Math.abs(accountBalance) - }) - - return ( - - - {props.incomeStatementChildren - .filter((incomeStatement) => incomeStatement.idIncomeStatementParent === props.incomeStatement.id) - .map((incomeStatement) => { - const incomeStatementChildren = getIncomeStatementChildren({ - incomeStatement: incomeStatement, - incomeStatements: props.incomeStatementChildren, - }) - - return ( - - ) - })} - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/incomeStatementReportPage.tsx b/packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/incomeStatementReportPage.tsx deleted file mode 100644 index fe939e0c..00000000 --- a/packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/incomeStatementReportPage.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" -import { useParams } from "@tanstack/react-router" -import { useState } from "react" -import { Box } from "../../../../../components/layouts/box.tsx" -import { Page } from "../../../../../components/layouts/page/page.tsx" -import { Section } from "../../../../../components/layouts/section/section.tsx" -import { incomeStatementReportRoute } from "../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/reports/incomeStatementReportRoute.tsx" -import type { YearDataKey } from "../../yearDataWrapper.tsx" -import { YearDataWrapper } from "../../yearDataWrapper.tsx" -import { ReportFilterPopover } from "../reportFilterPopover.tsx" -import { DownloadIncomeStatementReport } from "./downloadIncomeStatementReport.tsx" -import { IncomeStatementsReportTable } from "./incomeStatementsReportTable.tsx" - -const requiredKeys = [ - "accounts", - "entries", - "entryLines", - "incomeStatements", - "computations", - "computationIncomeStatements", - "journals", - "tags", - "entryTags", -] as const satisfies readonly YearDataKey[] - -export function IncomeStatementReportPage() { - const params = useParams({ - from: incomeStatementReportRoute.id, - }) - const [selectedJournalId, setSelectedJournalId] = useState(null) - const [selectedTags, setSelectedTags] = useState< - Array<{ - key: string - label: string - }> - >([]) - - return ( - - {({ - accounts, - entries, - entryLines, - incomeStatements, - computations, - computationIncomeStatements, - journals, - tags, - entryTags, - }) => { - let filteredEntryLines = entryLines.filter( - (entryLine) => entryLine.isComputedForIncomeStatementReport === true, - ) - const filteredAccounts = accounts.filter((account) => account.type === "income-statement") - - const journalOptions = journals.map((j) => ({ - key: j.id, - label: `${j.code} ${j.label ?? ""}`.trim(), - })) - - const tagOptions = tags.map((t) => ({ - key: t.id, - label: t.label, - })) - - if (selectedJournalId) { - const matchingEntryIds = new Set( - entries.filter((entry) => entry.idJournal === selectedJournalId).map((entry) => entry.id), - ) - filteredEntryLines = filteredEntryLines.filter((el) => matchingEntryIds.has(el.idEntry)) - } - - if (selectedTags.length > 0) { - const selectedTagIds = new Set(selectedTags.map((t) => t.key)) - const matchingEntryIds = new Set( - entryTags.filter((et) => selectedTagIds.has(et.idTag)).map((et) => et.idEntry), - ) - filteredEntryLines = filteredEntryLines.filter((el) => matchingEntryIds.has(el.idEntry)) - } - - return ( - - - - -
- - -
-
- - - -
-
-
-
-
- ) - }} -
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/incomeStatementsReportTable.tsx b/packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/incomeStatementsReportTable.tsx deleted file mode 100644 index 11446376..00000000 --- a/packages/website/src/features/dashboard/$idYear/reports/incomeStatementReport/incomeStatementsReportTable.tsx +++ /dev/null @@ -1,220 +0,0 @@ -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { FormatNull, FormatPrice, FormatText } from "@arrhes/ui" -import { css, cx } from "@arrhes/ui/utilities/cn.js" -import type * as v from "valibot" -import { Table } from "../../../../../components/layouts/table/table.tsx" -import { toRoman } from "../../../../../utilities/toRoman.ts" -import { getIncomeStatementChildren } from "../../yearSettings/incomeStatements/getIncomeStatementChildren.tsx" -import { IncomeStatementReportItem } from "./incomeStatementReportItem.tsx" - -export function IncomeStatementsReportTable(props: { - incomeStatements: Array> - computations: Array> - computationIncomeStatements: Array> - entryLines: Array> - accounts: Array> -}) { - return ( -
- - - - - - - Net - - - - - - {props.incomeStatements - .filter((incomeStatement) => incomeStatement.idIncomeStatementParent === null) - .sort((a, b) => Number(a.number) - Number(b.number)) - .map((incomeStatement) => { - const incomeStatementChildren = getIncomeStatementChildren({ - incomeStatement: incomeStatement, - incomeStatements: props.incomeStatements, - }) - - return ( - - ) - })} - - - {props.computations.length === 0 ? ( - - - - - - - - ) : ( - props.computations.map((computation, index) => { - let computationAmount = 0 - const _computationStatements = props.computationIncomeStatements - .filter( - (computationIncomeStatement) => - computationIncomeStatement.idComputation === computation.id, - ) - .forEach((computationIncomeStatement) => { - let incomeStatementAmount = 0 - props.accounts - .filter((account) => { - const foundIncomeStatement = props.incomeStatements.find( - (incomeStatement) => - incomeStatement.id === computationIncomeStatement.idIncomeStatement, - ) - if (foundIncomeStatement === undefined) { - return false - } - const incomeStatementChildren = getIncomeStatementChildren({ - incomeStatement: foundIncomeStatement, - incomeStatements: props.incomeStatements, - }) - - const hasAccount = - account.idIncomeStatement === - computationIncomeStatement.idIncomeStatement - const hasChildrenAccount = incomeStatementChildren.some( - (incomeStatement) => incomeStatement.id === account.idIncomeStatement, - ) - return hasAccount || hasChildrenAccount - }) - .forEach((account) => { - props.entryLines - .filter((entryLine) => entryLine.idAccount === account.id) - .forEach((entryLine) => { - incomeStatementAmount += - Number(entryLine.debit) - Number(entryLine.credit) - }) - }) - - if (computationIncomeStatement.operation === "plus") { - computationAmount += Math.abs(incomeStatementAmount) - } - if (computationIncomeStatement.operation === "minus") { - computationAmount += -Math.abs(incomeStatementAmount) - } - }) - - // .sort((a, b) => { - // if (!a.incomeStatement || !b.incomeStatement) return 0 - // return (a.incomeStatement.number - b.incomeStatement.number) - // }) - - const computationIncomeStatementsLabel = props.computationIncomeStatements - .filter( - (computationIncomeStatement) => - computationIncomeStatement.idComputation === computation.id, - ) - .map((computationIncomeStatement, computationIncomeStatementIndex) => { - const incomeStatement = props.incomeStatements.find( - (incomeStatement) => - incomeStatement.id === computationIncomeStatement.idIncomeStatement, - ) - if (incomeStatement === undefined) { - return "" - } - const romanNumber = toRoman(Number(incomeStatement.number)) - if (computationIncomeStatement.operation === "plus") { - if (computationIncomeStatementIndex === 0) return `${romanNumber}` - return `+${romanNumber}` - } - if (computationIncomeStatement.operation === "minus") return `-${romanNumber}` - return "" - }) - .join("") - - return ( - - -
- - {computation.label} - - - {`(${computationIncomeStatementsLabel})`} - -
-
- - - -
- ) - }) - )} -
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/reports/journalReport/JournalReportPage.tsx b/packages/website/src/features/dashboard/$idYear/reports/journalReport/JournalReportPage.tsx new file mode 100644 index 00000000..259d25e6 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/reports/journalReport/JournalReportPage.tsx @@ -0,0 +1,202 @@ +import { Button, ButtonOutlineContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react" +import { useParams } from "@tanstack/react-router" +import { useState } from "react" +import { Box } from "../../../../../components/layouts/Box.tsx" +import { Page } from "../../../../../components/layouts/page/page.tsx" +import { Section } from "../../../../../components/layouts/section/section.tsx" +import type { YearDataKey } from "../../YearDataWrapper.tsx" +import { YearDataWrapper } from "../../YearDataWrapper.tsx" +import { ReportFilterPopover } from "../ReportFilterPopover.tsx" +import { JournalReportTable } from "./JournalReportTable.tsx" + +const PAGE_SIZE = 20 + +const requiredKeys = [ + "entries", + "entryLines", + "accounts", + "journals", + "tags", + "entryTags", +] as const satisfies readonly YearDataKey[] + +export function JournalReportPage({ idYear: idYearProp }: { idYear?: string } = {}) { + const params = useParams({ + strict: false, + }) as { + idYear?: string + } + const idYear = idYearProp ?? params.idYear ?? "" + const [pageIndex, setPageIndex] = useState(0) + const [selectedJournalId, setSelectedJournalId] = useState(null) + const [selectedTags, setSelectedTags] = useState< + Array<{ + key: string + label: string + }> + >([]) + + return ( + + {({ entries, entryLines, accounts, journals, tags, entryTags }) => { + const accountsMap = new Map( + accounts.map((account) => [ + account.id, + account, + ]), + ) + + const filteredEntryLines = entryLines.filter( + (entryLine) => entryLine.isComputedForJournalReport === true, + ) + + const journalOptions = journals.map((j) => ({ + key: j.id, + label: `${j.code} ${j.label ?? ""}`.trim(), + })) + + const tagOptions = tags.map((t) => ({ + key: t.id, + label: t.label, + })) + + let filteredEntries = [ + ...entries, + ] + + if (selectedJournalId) { + filteredEntries = filteredEntries.filter((entry) => entry.idJournal === selectedJournalId) + } + + if (selectedTags.length > 0) { + const selectedTagIds = new Set(selectedTags.map((t) => t.key)) + const matchingEntryIds = new Set( + entryTags.filter((et) => selectedTagIds.has(et.idTag)).map((et) => et.idEntry), + ) + filteredEntries = filteredEntries.filter((entry) => matchingEntryIds.has(entry.id)) + } + + const filteredEntryIds = new Set(filteredEntries.map((e) => e.id)) + const filteredEntryLinesForTable = filteredEntryLines.filter((el) => filteredEntryIds.has(el.idEntry)) + + const sortedEntries = filteredEntries.sort((a, b) => b.date.localeCompare(a.date)) + + const pageCount = Math.max(1, Math.ceil(sortedEntries.length / PAGE_SIZE)) + const clampedPageIndex = Math.min(pageIndex, pageCount - 1) + const paginatedEntries = sortedEntries.slice( + clampedPageIndex * PAGE_SIZE, + (clampedPageIndex + 1) * PAGE_SIZE, + ) + + const canPreviousPage = clampedPageIndex > 0 + const canNextPage = clampedPageIndex < pageCount - 1 + + return ( + + + + +
+ { + setSelectedJournalId(value) + setPageIndex(0) + }} + journalOptions={journalOptions} + selectedTags={selectedTags} + onTagsChange={(values) => { + setSelectedTags(values) + setPageIndex(0) + }} + tagOptions={tagOptions} + /> +
+ + + + {pageCount > 1 ? ( +
+ + {sortedEntries.length} écriture{sortedEntries.length > 1 ? "s" : ""} + +
+ + + Page {clampedPageIndex + 1} sur {pageCount} + + +
+
+ ) : null} +
+
+
+
+ ) + }} +
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/reports/journalReport/journalReportTable.tsx b/packages/website/src/features/dashboard/$idYear/reports/journalReport/JournalReportTable.tsx similarity index 100% rename from packages/website/src/features/dashboard/$idYear/reports/journalReport/journalReportTable.tsx rename to packages/website/src/features/dashboard/$idYear/reports/journalReport/JournalReportTable.tsx diff --git a/packages/website/src/features/dashboard/$idYear/reports/journalReport/journalReportPage.tsx b/packages/website/src/features/dashboard/$idYear/reports/journalReport/journalReportPage.tsx deleted file mode 100644 index bb029c0d..00000000 --- a/packages/website/src/features/dashboard/$idYear/reports/journalReport/journalReportPage.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import { Button, ButtonOutlineContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react" -import { useParams } from "@tanstack/react-router" -import { useState } from "react" -import { Box } from "../../../../../components/layouts/box.tsx" -import { Page } from "../../../../../components/layouts/page/page.tsx" -import { Section } from "../../../../../components/layouts/section/section.tsx" -import { journalReportRoute } from "../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/reports/journalReportRoute.tsx" -import type { YearDataKey } from "../../yearDataWrapper.tsx" -import { YearDataWrapper } from "../../yearDataWrapper.tsx" -import { ReportFilterPopover } from "../reportFilterPopover.tsx" -import { JournalReportTable } from "./journalReportTable.tsx" - -const PAGE_SIZE = 20 - -const requiredKeys = [ - "entries", - "entryLines", - "accounts", - "journals", - "tags", - "entryTags", -] as const satisfies readonly YearDataKey[] - -export function JournalReportPage() { - const params = useParams({ - from: journalReportRoute.id, - }) - const [pageIndex, setPageIndex] = useState(0) - const [selectedJournalId, setSelectedJournalId] = useState(null) - const [selectedTags, setSelectedTags] = useState< - Array<{ - key: string - label: string - }> - >([]) - - return ( - - {({ entries, entryLines, accounts, journals, tags, entryTags }) => { - const accountsMap = new Map( - accounts.map((account) => [ - account.id, - account, - ]), - ) - - const filteredEntryLines = entryLines.filter( - (entryLine) => entryLine.isComputedForJournalReport === true, - ) - - const journalOptions = journals.map((j) => ({ - key: j.id, - label: `${j.code} ${j.label ?? ""}`.trim(), - })) - - const tagOptions = tags.map((t) => ({ - key: t.id, - label: t.label, - })) - - let filteredEntries = [ - ...entries, - ] - - if (selectedJournalId) { - filteredEntries = filteredEntries.filter((entry) => entry.idJournal === selectedJournalId) - } - - if (selectedTags.length > 0) { - const selectedTagIds = new Set(selectedTags.map((t) => t.key)) - const matchingEntryIds = new Set( - entryTags.filter((et) => selectedTagIds.has(et.idTag)).map((et) => et.idEntry), - ) - filteredEntries = filteredEntries.filter((entry) => matchingEntryIds.has(entry.id)) - } - - const filteredEntryIds = new Set(filteredEntries.map((e) => e.id)) - const filteredEntryLinesForTable = filteredEntryLines.filter((el) => filteredEntryIds.has(el.idEntry)) - - const sortedEntries = filteredEntries.sort((a, b) => b.date.localeCompare(a.date)) - - const pageCount = Math.max(1, Math.ceil(sortedEntries.length / PAGE_SIZE)) - const clampedPageIndex = Math.min(pageIndex, pageCount - 1) - const paginatedEntries = sortedEntries.slice( - clampedPageIndex * PAGE_SIZE, - (clampedPageIndex + 1) * PAGE_SIZE, - ) - - const canPreviousPage = clampedPageIndex > 0 - const canNextPage = clampedPageIndex < pageCount - 1 - - return ( - - - - -
- { - setSelectedJournalId(value) - setPageIndex(0) - }} - journalOptions={journalOptions} - selectedTags={selectedTags} - onTagsChange={(values) => { - setSelectedTags(values) - setPageIndex(0) - }} - tagOptions={tagOptions} - /> -
- - - - {pageCount > 1 ? ( -
- - {sortedEntries.length} écriture{sortedEntries.length > 1 ? "s" : ""} - -
- - - Page {clampedPageIndex + 1} sur {pageCount} - - -
-
- ) : null} -
-
-
-
- ) - }} -
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/reports/ledgerReport/LedgerReportPage.tsx b/packages/website/src/features/dashboard/$idYear/reports/ledgerReport/LedgerReportPage.tsx new file mode 100644 index 00000000..d0fc147b --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/reports/ledgerReport/LedgerReportPage.tsx @@ -0,0 +1,107 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import { useParams } from "@tanstack/react-router" +import { useState } from "react" +import { Box } from "../../../../../components/layouts/Box.tsx" +import { Page } from "../../../../../components/layouts/page/page.tsx" +import { Section } from "../../../../../components/layouts/section/section.tsx" +import type { YearDataKey } from "../../YearDataWrapper.tsx" +import { YearDataWrapper } from "../../YearDataWrapper.tsx" +import { ReportFilterPopover } from "../ReportFilterPopover.tsx" +import { LedgerReportTable } from "./LedgerReportTable.tsx" + +const requiredKeys = [ + "accounts", + "entries", + "entryLines", + "journals", + "tags", + "entryTags", +] as const satisfies readonly YearDataKey[] + +export function LedgerReportPage({ idYear: idYearProp }: { idYear?: string } = {}) { + const params = useParams({ + strict: false, + }) as { + idYear?: string + } + const idYear = idYearProp ?? params.idYear ?? "" + const [selectedJournalId, setSelectedJournalId] = useState(null) + const [selectedTags, setSelectedTags] = useState< + Array<{ + key: string + label: string + }> + >([]) + + return ( + + {({ accounts, entries, entryLines, journals, tags, entryTags }) => { + let filteredEntryLines = entryLines.filter((entryLine) => entryLine.isComputedForLedgerReport === true) + const filteredAccounts = accounts + + const journalOptions = journals.map((j) => ({ + key: j.id, + label: `${j.code} ${j.label ?? ""}`.trim(), + })) + + const tagOptions = tags.map((t) => ({ + key: t.id, + label: t.label, + })) + + if (selectedJournalId) { + const matchingEntryIds = new Set( + entries.filter((entry) => entry.idJournal === selectedJournalId).map((entry) => entry.id), + ) + filteredEntryLines = filteredEntryLines.filter((el) => matchingEntryIds.has(el.idEntry)) + } + + if (selectedTags.length > 0) { + const selectedTagIds = new Set(selectedTags.map((t) => t.key)) + const matchingEntryIds = new Set( + entryTags.filter((et) => selectedTagIds.has(et.idTag)).map((et) => et.idEntry), + ) + filteredEntryLines = filteredEntryLines.filter((el) => matchingEntryIds.has(el.idEntry)) + } + + return ( + + + + +
+ +
+ + + +
+
+
+
+ ) + }} +
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/reports/ledgerReport/ledgerReportTable.tsx b/packages/website/src/features/dashboard/$idYear/reports/ledgerReport/LedgerReportTable.tsx similarity index 100% rename from packages/website/src/features/dashboard/$idYear/reports/ledgerReport/ledgerReportTable.tsx rename to packages/website/src/features/dashboard/$idYear/reports/ledgerReport/LedgerReportTable.tsx diff --git a/packages/website/src/features/dashboard/$idYear/reports/ledgerReport/ledgerReportPage.tsx b/packages/website/src/features/dashboard/$idYear/reports/ledgerReport/ledgerReportPage.tsx deleted file mode 100644 index c8de3e90..00000000 --- a/packages/website/src/features/dashboard/$idYear/reports/ledgerReport/ledgerReportPage.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" -import { useParams } from "@tanstack/react-router" -import { useState } from "react" -import { Box } from "../../../../../components/layouts/box.tsx" -import { Page } from "../../../../../components/layouts/page/page.tsx" -import { Section } from "../../../../../components/layouts/section/section.tsx" -import { ledgerReportRoute } from "../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/reports/ledgerReportRoute.tsx" -import type { YearDataKey } from "../../yearDataWrapper.tsx" -import { YearDataWrapper } from "../../yearDataWrapper.tsx" -import { ReportFilterPopover } from "../reportFilterPopover.tsx" -import { LedgerReportTable } from "./ledgerReportTable.tsx" - -const requiredKeys = [ - "accounts", - "entries", - "entryLines", - "journals", - "tags", - "entryTags", -] as const satisfies readonly YearDataKey[] - -export function LedgerReportPage() { - const params = useParams({ - from: ledgerReportRoute.id, - }) - const [selectedJournalId, setSelectedJournalId] = useState(null) - const [selectedTags, setSelectedTags] = useState< - Array<{ - key: string - label: string - }> - >([]) - - return ( - - {({ accounts, entries, entryLines, journals, tags, entryTags }) => { - let filteredEntryLines = entryLines.filter((entryLine) => entryLine.isComputedForLedgerReport === true) - const filteredAccounts = accounts - - const journalOptions = journals.map((j) => ({ - key: j.id, - label: `${j.code} ${j.label ?? ""}`.trim(), - })) - - const tagOptions = tags.map((t) => ({ - key: t.id, - label: t.label, - })) - - if (selectedJournalId) { - const matchingEntryIds = new Set( - entries.filter((entry) => entry.idJournal === selectedJournalId).map((entry) => entry.id), - ) - filteredEntryLines = filteredEntryLines.filter((el) => matchingEntryIds.has(el.idEntry)) - } - - if (selectedTags.length > 0) { - const selectedTagIds = new Set(selectedTags.map((t) => t.key)) - const matchingEntryIds = new Set( - entryTags.filter((et) => selectedTagIds.has(et.idTag)).map((et) => et.idEntry), - ) - filteredEntryLines = filteredEntryLines.filter((el) => matchingEntryIds.has(el.idEntry)) - } - - return ( - - - - -
- -
- - - -
-
-
-
- ) - }} -
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/reports/reportsLayout.tsx b/packages/website/src/features/dashboard/$idYear/reports/reportsLayout.tsx deleted file mode 100644 index 4677091f..00000000 --- a/packages/website/src/features/dashboard/$idYear/reports/reportsLayout.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { IconBook, IconBook2, IconChartBar, IconReport, IconReportMoney, IconScale } from "@tabler/icons-react" -import { useParams } from "@tanstack/react-router" -import { SubPageLayout } from "../../../../components/layouts/subPageLayout.js" -import { reportsLayoutRoute } from "../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/reports/reportsLayoutRoute.js" - -export function ReportsLayout() { - const params = useParams({ - from: reportsLayoutRoute.id, - }) - - return ( - , - items: [ - { - label: "Livre-journal", - icon: , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/documents/livre-journal", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - }, - }, - { - label: "Grand livre", - icon: , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/documents/grand-livre", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - }, - }, - ], - }, - summaries: { - title: "Synthèses", - icon: , - items: [ - { - label: "Balance", - icon: , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/documents/balance", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - }, - }, - { - label: "Bilan", - icon: , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/documents/bilan", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - }, - }, - { - label: "Compte de résultat", - icon: , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/documents/compte-de-résultat", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - }, - }, - ], - }, - }} - /> - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/useYearData.tsx b/packages/website/src/features/dashboard/$idYear/useYearData.tsx new file mode 100644 index 00000000..520f40bf --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/useYearData.tsx @@ -0,0 +1,10 @@ +import { useContext } from "react" +import { YearDataContext, type YearDataContextValue } from "./YearDataProvider.js" + +export function useYearData(): YearDataContextValue { + const context = useContext(YearDataContext) + if (context === null) { + throw new Error("useYearData must be used within a YearDataProvider") + } + return context +} diff --git a/packages/website/src/features/dashboard/$idYear/yearDataContext.tsx b/packages/website/src/features/dashboard/$idYear/yearDataContext.tsx index c4524f27..1bcec1b7 100644 --- a/packages/website/src/features/dashboard/$idYear/yearDataContext.tsx +++ b/packages/website/src/features/dashboard/$idYear/yearDataContext.tsx @@ -13,7 +13,7 @@ import { readAllTagsRouteDefinition, } from "@arrhes/application-metadata/routes" import { type UseQueryResult, useQuery } from "@tanstack/react-query" -import { createContext, type ReactNode, useContext, useMemo } from "react" +import { createContext, type ReactNode, useMemo } from "react" import type * as v from "valibot" import { ClientError } from "../../../utilities/clientError.ts" import { getResponseBodyFromAPI } from "../../../utilities/getResponseBodyFromAPI.ts" @@ -42,6 +42,7 @@ export type YearData = { export type YearDataKey = keyof YearData type YearScopedRouteDefinition = { + method: "GET" | "POST" | "PATCH" | "DELETE" path: string schemas: { body: v.ObjectSchema @@ -51,11 +52,11 @@ type YearScopedRouteDefinition = { } } -type YearDataContextValue = { +export type YearDataContextValue = { [K in YearDataKey]: UseQueryResult } -const YearDataContext = createContext(null) +export const YearDataContext = createContext(null) function useYearQuery( key: K, @@ -144,11 +145,3 @@ export function YearDataProvider(props: { idYear: string; children: ReactNode }) return {props.children} } - -export function useYearData(): YearDataContextValue { - const context = useContext(YearDataContext) - if (context === null) { - throw new Error("useYearData must be used within a YearDataProvider") - } - return context -} diff --git a/packages/website/src/features/dashboard/$idYear/yearDataWrapper.tsx b/packages/website/src/features/dashboard/$idYear/yearDataWrapper.tsx deleted file mode 100644 index d3e27bf3..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearDataWrapper.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { - readAllAccountsRouteDefinition, - readAllBalanceSheetsRouteDefinition, - readAllComputationIncomeStatementsRouteDefinition, - readAllComputationsRouteDefinition, - readAllEntriesRouteDefinition, - readAllEntryLinesRouteDefinition, - readAllEntryTagsRouteDefinition, - readAllFilesRouteDefinition, - readAllFoldersRouteDefinition, - readAllIncomeStatementsRouteDefinition, - readAllJournalsRouteDefinition, - readAllTagsRouteDefinition, -} from "@arrhes/application-metadata/routes" -import { CircularLoader, FormatError } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { useQueries } from "@tanstack/react-query" -import type { ReactElement } from "react" -import { useMemo } from "react" -import type * as v from "valibot" -import { ClientError } from "../../../utilities/clientError.ts" -import { getResponseBodyFromAPI } from "../../../utilities/getResponseBodyFromAPI.ts" - -const yearQueries = { - accounts: readAllAccountsRouteDefinition, - entries: readAllEntriesRouteDefinition, - entryLines: readAllEntryLinesRouteDefinition, - entryTags: readAllEntryTagsRouteDefinition, - journals: readAllJournalsRouteDefinition, - tags: readAllTagsRouteDefinition, - files: readAllFilesRouteDefinition, - folders: readAllFoldersRouteDefinition, - balanceSheets: readAllBalanceSheetsRouteDefinition, - incomeStatements: readAllIncomeStatementsRouteDefinition, - computations: readAllComputationsRouteDefinition, - computationIncomeStatements: readAllComputationIncomeStatementsRouteDefinition, -} as const - -type YearQueries = typeof yearQueries - -type YearData = { - [K in keyof YearQueries]: v.InferOutput -} - -export type YearDataKey = keyof YearData - -type YearScopedRouteDefinition = { - path: string - schemas: { - body: v.ObjectSchema - return: - | v.ObjectSchema - | v.ArraySchema, undefined> - } -} - -const yearQueryEntries = Object.entries(yearQueries) as [ - YearDataKey, - YearScopedRouteDefinition, -][] - -export function YearDataWrapper(props: { - idYear: string - requiredKeys: K - children: (data: Pick) => ReactElement | null -}) { - const body = useMemo( - () => ({ - idYear: props.idYear, - }), - [ - props.idYear, - ], - ) - - const results = useQueries({ - queries: yearQueryEntries.map(([_key, routeDef]) => ({ - queryKey: [ - routeDef.path, - body, - ], - queryFn: async (context: { signal: AbortSignal }) => { - const response = await getResponseBodyFromAPI({ - routeDefinition: routeDef, - body, - signal: context.signal, - }) - if (response.ok === false) { - throw new ClientError({ - message: "Error with the data fetching", - rawError: response.error, - }) - } - return response.data - }, - retry: 1, - })), - }) - - const requiredIndices = useMemo( - () => props.requiredKeys.map((key) => yearQueryEntries.findIndex(([k]) => k === key)), - [ - props.requiredKeys, - ], - ) - - const isPending = requiredIndices.some((index) => results[index].isPending) - const isError = requiredIndices.some((index) => results[index].isError) - - if (isPending) { - return ( - - ) - } - - if (isError) { - return ( - - ) - } - - const data = Object.fromEntries( - yearQueryEntries.map(([key], index) => [ - key, - results[index].data, - ]), - ) as Pick - - return props.children(data) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearLayout.tsx b/packages/website/src/features/dashboard/$idYear/yearLayout.tsx deleted file mode 100644 index d3548bed..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearLayout.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { IconFolders, IconPencil, IconReport, IconSettings } from "@tabler/icons-react" -import { Outlet, useParams } from "@tanstack/react-router" -import { Fragment } from "react/jsx-runtime" -import { PageNavigation } from "../../../components/layouts/page/pageNavigation.tsx" -import { yearLayoutRoute } from "../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearLayoutRoute.tsx" - -export function YearLayout() { - const params = useParams({ - from: yearLayoutRoute.id, - }) - - return ( - - , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/écritures", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - }, - }, - { - label: "Stockage", - icon: , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/stockage", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - }, - }, - { - label: "Documents comptables", - icon: , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/documents", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - }, - }, - { - label: "Paramètres", - icon: , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - }, - }, - ]} - /> - - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/DeleteOneYear.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/DeleteOneYear.tsx new file mode 100644 index 00000000..c581163d --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/DeleteOneYear.tsx @@ -0,0 +1,96 @@ +import { deleteOneYearRouteDefinition, readAllYearsRouteDefinition } from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, ButtonOutlineContent, ButtonPlainContent, Dialog, toast, useModalStore } from "@arrhes/ui" +import { type ComponentPropsWithRef, type ReactElement, useId } from "react" +import type * as v from "valibot" +import { useTabs } from "../../../../contexts/tabs/useTabs.tsx" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../utilities/invalidateData.ts" + +export function DeleteOneYear(props: { + year: v.InferOutput + children: ReactElement> +}) { + const { openTab } = useTabs() + const modalId = useId() + const { open: openModal, close: closeModal } = useModalStore() + + async function onSubmit() { + const deleteResponse = await getResponseBodyFromAPI({ + routeDefinition: deleteOneYearRouteDefinition, + body: { + idYear: props.year.id, + }, + }) + + if (deleteResponse.ok === false) { + toast({ + title: "Erreur lors de la suppression de l'exercice", + variant: "error", + }) + return + } + + await invalidateData({ + routeDefinition: readAllYearsRouteDefinition, + body: {}, + }) + + toast({ + title: "Exercice supprimé", + variant: "success", + }) + + openTab( + { + component: "exercices", + props: { + idOrganization: props.year.idOrganization, + }, + }, + { + newTab: true, + }, + ) + } + + return ( + + + + , + ) + } + > + {props.children} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/UpdateOneYear.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/UpdateOneYear.tsx new file mode 100644 index 00000000..d013706c --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/UpdateOneYear.tsx @@ -0,0 +1,181 @@ +import { readOneYearRouteDefinition, updateOneYearRouteDefinition } from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, InputDate, InputText, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPencil } from "@tabler/icons-react" +import type { JSX } from "react" +import { Fragment } from "react/jsx-runtime" +import type * as v from "valibot" +import { FormControl } from "../../../../components/forms/FormControl.tsx" +import { FormError } from "../../../../components/forms/FormError.tsx" +import { FormField } from "../../../../components/forms/FormField.tsx" +import { FormItem } from "../../../../components/forms/FormItem.tsx" +import { FormLabel } from "../../../../components/forms/FormLabel.tsx" +import { FormRoot } from "../../../../components/forms/FormRoot.tsx" +import { useTabs } from "../../../../contexts/tabs/useTabs.tsx" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../utilities/invalidateData.ts" +import { YearSelect } from "../../$idOrganization/years/YearSelect.tsx" + +export function UpdateOneYear(props: { year: v.InferOutput; children: JSX.Element }) { + const { openPanelTab, closeTab } = useTabs() + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/YearSettingsLayout.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/YearSettingsLayout.tsx new file mode 100644 index 00000000..5dac14e7 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/YearSettingsLayout.tsx @@ -0,0 +1,5 @@ +import { Outlet } from "@tanstack/react-router" + +export function YearSettingsLayout() { + return +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/YearSettingsPage.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/YearSettingsPage.tsx new file mode 100644 index 00000000..a41f57cd --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/YearSettingsPage.tsx @@ -0,0 +1,81 @@ +import { readOneYearRouteDefinition } from "@arrhes/application-metadata/routes" +import { ButtonOutlineContent } from "@arrhes/ui" +import { IconPencil, IconTrash } from "@tabler/icons-react" +import { useParams } from "@tanstack/react-router" +import { DataWrapper } from "../../../../components/layouts/DataWrapper.tsx" +import { PageRoot } from "../../../../components/layouts/page/PageRoot.tsx" +import { Page } from "../../../../components/layouts/page/page.tsx" +import { SettingsSection } from "../../../../components/layouts/settingsSection/settingsSection.tsx" +import { DeleteOneYear } from "./DeleteOneYear.tsx" +import { UpdateOneYear } from "./UpdateOneYear.tsx" + +export function YearSettingsPage({ + idOrganization: idOrganizationProp, + idYear: idYearProp, +}: { + idOrganization?: string + idYear?: string +} = {}) { + const params = useParams({ + strict: false, + }) as { + idOrganization?: string + idYear?: string + } + const idOrganization = idOrganizationProp ?? params.idOrganization ?? "" + const idYear = idYearProp ?? params.idYear ?? "" + void idOrganization + + return ( + + + + {(year) => { + return ( + <> + + + + + } + text="Modifier" + /> + + + + + + + + } + text="Supprimer" + color="danger" + /> + + + + + ) + }} + + + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/YearSettingsTabContent.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/YearSettingsTabContent.tsx new file mode 100644 index 00000000..fb8e544e --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/YearSettingsTabContent.tsx @@ -0,0 +1,143 @@ +import { + IconBook, + IconFileReport, + IconHome, + IconListNumbers, + IconReportMoney, + IconScale, + IconSettings, + IconTag, +} from "@tabler/icons-react" +import { lazy, Suspense } from "react" +import { SubPageContent } from "../../../../components/layouts/SubPageContent.tsx" + +const YearSettingsPage = lazy(() => + import("./YearSettingsPage.js").then((m) => ({ + default: m.YearSettingsPage, + })), +) +const AccountsPage = lazy(() => + import("./accounts/AccountsPage.js").then((m) => ({ + default: m.AccountsPage, + })), +) +const JournalsPage = lazy(() => + import("./journals/JournalsPage.js").then((m) => ({ + default: m.JournalsPage, + })), +) +const TagsPage = lazy(() => + import("./tags/TagsPage.js").then((m) => ({ + default: m.TagsPage, + })), +) +const BalanceSheetsPage = lazy(() => + import("./balanceSheets/BalanceSheetsPage.js").then((m) => ({ + default: m.BalanceSheetsPage, + })), +) +const IncomeStatementsPage = lazy(() => + import("./incomeStatements/IncomeStatementsPage.js").then((m) => ({ + default: m.IncomeStatementsPage, + })), +) + +export function YearSettingsTabContent(props: { idOrganization: string; idYear: string }) { + return ( + , + items: [ + { + key: "général", + label: "Général", + icon: , + content: ( + + + + ), + }, + { + key: "comptes", + label: "Plan des comptes", + icon: , + content: ( + + + + ), + }, + { + key: "journaux", + label: "Journaux", + icon: , + content: ( + + + + ), + }, + { + key: "catégories", + label: "Catégories", + icon: , + content: ( + + + + ), + }, + ], + }, + documents: { + title: "Documents comptables", + icon: , + items: [ + { + key: "bilan", + label: "Bilan", + icon: , + content: ( + + + + ), + }, + { + key: "compte-de-résultat", + label: "Compte de résultat", + icon: , + content: ( + + + + ), + }, + ], + }, + }} + /> + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/$idAccount/AccountLayout.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/$idAccount/AccountLayout.tsx new file mode 100644 index 00000000..5e9f17b7 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/$idAccount/AccountLayout.tsx @@ -0,0 +1,118 @@ +import { readOneAccountRouteDefinition } from "@arrhes/application-metadata/routes" +import { ButtonOutlineContent, ButtonPlainContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconChevronLeft, IconDatabase, IconInfoCircle, IconPencil, IconTrash } from "@tabler/icons-react" +import { Outlet, useParams } from "@tanstack/react-router" +import { LinkButton } from "../../../../../../components/LinkButton.tsx" +import { DataWrapper } from "../../../../../../components/layouts/DataWrapper.tsx" +import { Section } from "../../../../../../components/layouts/section/section.tsx" +import { Tab } from "../../../../../../components/layouts/tab/tab.tsx" + +import { DeleteOneAccount } from "./DeleteOneAccount.tsx" +import { UpdateOneAccount } from "./UpdateOneAccount.tsx" + +export function AccountLayout() { + const params = useParams({ + strict: false, + }) as { + idYear: string + idAccount: string + idOrganization: string + } + + return ( + + + {(account) => { + return ( + <> + +
+ + } + text="Retour" + /> + +
+
+ + } + text="Modifier" + /> + + + } + title="Supprimer" + color="danger" + /> + +
+
+ + , + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/comptes/$idAccount", + params: { + idOrganization: params.idOrganization, + idYear: params.idYear, + idAccount: params.idAccount, + }, + }, + { + label: "Métadonnées", + icon: , + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/comptes/$idAccount/métadonnées", + params: { + idOrganization: params.idOrganization, + idYear: params.idYear, + idAccount: params.idAccount, + }, + }, + ]} + /> + + + + ) + }} +
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/$idAccount/AccountMetadataTab.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/$idAccount/AccountMetadataTab.tsx new file mode 100644 index 00000000..02b0ab52 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/$idAccount/AccountMetadataTab.tsx @@ -0,0 +1,51 @@ +import { readOneAccountRouteDefinition } from "@arrhes/application-metadata/routes" +import { FormatDateTime, FormatText } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { useParams } from "@tanstack/react-router" +import { DataWrapper } from "../../../../../../components/layouts/DataWrapper.tsx" +import { DataBlock } from "../../../../../../components/layouts/dataBlock/dataBlock.tsx" +import { Section } from "../../../../../../components/layouts/section/section.tsx" + +export function AccountMetadataTab() { + const params = useParams({ + strict: false, + }) as { + idYear: string + idAccount: string + } + + return ( + + {(account) => { + return ( + + + + + + + + + + + + {account.id} + + + + + ) + }} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/$idAccount/AccountPage.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/$idAccount/AccountPage.tsx new file mode 100644 index 00000000..b3290ddf --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/$idAccount/AccountPage.tsx @@ -0,0 +1,51 @@ +import { readOneAccountRouteDefinition } from "@arrhes/application-metadata/routes" +import { FormatBoolean, FormatText } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { useParams } from "@tanstack/react-router" +import { DataWrapper } from "../../../../../../components/layouts/DataWrapper.tsx" +import { DataBlock } from "../../../../../../components/layouts/dataBlock/dataBlock.tsx" +import { Section } from "../../../../../../components/layouts/section/section.tsx" + +export function AccountPage() { + const params = useParams({ + strict: false, + }) as { + idYear: string + idAccount: string + } + + return ( + + {(account) => { + return ( + + + + + + {account.number.toString()} + + + {account.label} + + + + + + + + ) + }} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/$idAccount/DeleteOneAccount.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/$idAccount/DeleteOneAccount.tsx new file mode 100644 index 00000000..1c6c1c3c --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/$idAccount/DeleteOneAccount.tsx @@ -0,0 +1,96 @@ +import { deleteOneAccountRouteDefinition, readAllAccountsRouteDefinition } from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, ButtonOutlineContent, ButtonPlainContent, Dialog, toast, useModalStore } from "@arrhes/ui" +import { IconTrash } from "@tabler/icons-react" +import { type ComponentPropsWithRef, type ReactElement, useId } from "react" +import type * as v from "valibot" +import { applicationRouter } from "../../../../../../routes/applicationRouter.tsx" +import { getResponseBodyFromAPI } from "../../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../../utilities/invalidateData.ts" + +export function DeleteOneAccount(props: { + account: v.InferOutput + children: ReactElement> +}) { + const modalId = useId() + const { open: openModal, close: closeModal } = useModalStore() + + async function onSubmit() { + const deleteResponse = await getResponseBodyFromAPI({ + routeDefinition: deleteOneAccountRouteDefinition, + body: { + idAccount: props.account.id, + idYear: props.account.idYear, + }, + }) + + if (deleteResponse.ok === false) { + toast({ + title: "Erreur lors de la suppression du compte", + variant: "error", + }) + return + } + + await invalidateData({ + routeDefinition: readAllAccountsRouteDefinition, + body: { + idYear: props.account.idYear, + }, + }) + + toast({ + title: "Compte supprimé", + variant: "success", + }) + + applicationRouter.navigate({ + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/comptes", + params: { + idOrganization: props.account.idOrganization, + idYear: props.account.idYear, + }, + }) + } + + return ( + + + + , + ) + } + > + {props.children} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/$idAccount/UpdateOneAccount.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/$idAccount/UpdateOneAccount.tsx new file mode 100644 index 00000000..b63c5559 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/$idAccount/UpdateOneAccount.tsx @@ -0,0 +1,314 @@ +import { + readAllAccountsRouteDefinition, + readOneAccountRouteDefinition, + updateOneAccountRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, InputText, InputToggle, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPlus } from "@tabler/icons-react" +import type { JSX } from "react" +import { Fragment } from "react/jsx-runtime" +import type * as v from "valibot" +import { FormControl } from "../../../../../../components/forms/FormControl.tsx" +import { FormError } from "../../../../../../components/forms/FormError.tsx" +import { FormField } from "../../../../../../components/forms/FormField.tsx" +import { FormItem } from "../../../../../../components/forms/FormItem.tsx" +import { FormLabel } from "../../../../../../components/forms/FormLabel.tsx" +import { FormRoot } from "../../../../../../components/forms/FormRoot.tsx" +import { useTabs } from "../../../../../../contexts/tabs/useTabs.tsx" +import { getResponseBodyFromAPI } from "../../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../../utilities/invalidateData.ts" +import { BalanceSheetsSelect } from "../../balanceSheets/BalanceSheetsSelect.tsx" +import { AccountSelect } from "../AccountSelect.tsx" + +export function UpdateOneAccount(props: { + account: v.InferOutput + children: JSX.Element +}) { + const { openPanelTab, closeTab } = useTabs() + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/$idAccount/accountLayout.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/$idAccount/accountLayout.tsx deleted file mode 100644 index aa069281..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/$idAccount/accountLayout.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { readOneAccountRouteDefinition } from "@arrhes/application-metadata/routes" -import { ButtonOutlineContent, ButtonPlainContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconChevronLeft, IconDatabase, IconInfoCircle, IconPencil, IconTrash } from "@tabler/icons-react" -import { Outlet, useParams } from "@tanstack/react-router" -import { DataWrapper } from "../../../../../../components/layouts/dataWrapper.tsx" -import { Section } from "../../../../../../components/layouts/section/section.tsx" -import { Tab } from "../../../../../../components/layouts/tab/tab.tsx" -import { LinkButton } from "../../../../../../components/linkButton.tsx" -import { accountLayoutRoute } from "../../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/accountLayoutRoute.tsx" -import { DeleteOneAccount } from "./deleteOneAccount.tsx" -import { UpdateOneAccount } from "./updateOneAccount.tsx" - -export function AccountLayout() { - const params = useParams({ - from: accountLayoutRoute.id, - }) - - return ( - - - {(account) => { - return ( - <> - -
- - } - text="Retour" - /> - -
-
- - } - text="Modifier" - /> - - - } - title="Supprimer" - color="danger" - /> - -
-
- - , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/comptes/$idAccount", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - idAccount: params.idAccount, - }, - }, - { - label: "Métadonnées", - icon: , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/comptes/$idAccount/métadonnées", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - idAccount: params.idAccount, - }, - }, - ]} - /> - - - - ) - }} -
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/$idAccount/accountMetadataTab.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/$idAccount/accountMetadataTab.tsx deleted file mode 100644 index ce20df4b..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/$idAccount/accountMetadataTab.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { readOneAccountRouteDefinition } from "@arrhes/application-metadata/routes" -import { FormatDateTime, FormatText } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { useParams } from "@tanstack/react-router" -import { DataBlock } from "../../../../../../components/layouts/dataBlock/dataBlock.tsx" -import { DataWrapper } from "../../../../../../components/layouts/dataWrapper.tsx" -import { Section } from "../../../../../../components/layouts/section/section.tsx" -import { accountLayoutRoute } from "../../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/accountLayoutRoute.tsx" - -export function AccountMetadataTab() { - const params = useParams({ - from: accountLayoutRoute.id, - }) - - return ( - - {(account) => { - return ( - - - - - - - - - - - - {account.id} - - - - - ) - }} - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/$idAccount/accountPage.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/$idAccount/accountPage.tsx deleted file mode 100644 index 1321e2e5..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/$idAccount/accountPage.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { readOneAccountRouteDefinition } from "@arrhes/application-metadata/routes" -import { FormatBoolean, FormatText } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { useParams } from "@tanstack/react-router" -import { DataBlock } from "../../../../../../components/layouts/dataBlock/dataBlock.tsx" -import { DataWrapper } from "../../../../../../components/layouts/dataWrapper.tsx" -import { Section } from "../../../../../../components/layouts/section/section.tsx" -import { accountLayoutRoute } from "../../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/accountLayoutRoute.tsx" - -export function AccountPage() { - const params = useParams({ - from: accountLayoutRoute.id, - }) - - return ( - - {(account) => { - return ( - - - - - - {account.number.toString()} - - - {account.label} - - - - - - - - ) - }} - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/$idAccount/deleteOneAccount.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/$idAccount/deleteOneAccount.tsx deleted file mode 100644 index 65b104b4..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/$idAccount/deleteOneAccount.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { deleteOneAccountRouteDefinition, readAllAccountsRouteDefinition } from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { toast } from "@arrhes/ui" -import { IconTrash } from "@tabler/icons-react" -import type { ComponentPropsWithRef, ReactElement } from "react" -import type * as v from "valibot" -import { ConfirmationModal } from "../../../../../../components/overlays/dialog/confirmationModal.tsx" -import { applicationRouter } from "../../../../../../routes/applicationRouter.tsx" -import { getResponseBodyFromAPI } from "../../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../../utilities/invalidateData.ts" - -export function DeleteOneAccount(props: { - account: v.InferOutput - children: ReactElement> -}) { - async function onSubmit() { - const deleteResponse = await getResponseBodyFromAPI({ - routeDefinition: deleteOneAccountRouteDefinition, - body: { - idAccount: props.account.id, - idYear: props.account.idYear, - }, - }) - - if (deleteResponse.ok === false) { - toast({ - title: "Erreur lors de la suppression du compte", - variant: "error", - }) - return - } - - await invalidateData({ - routeDefinition: readAllAccountsRouteDefinition, - body: { - idYear: props.account.idYear, - }, - }) - - toast({ - title: "Compte supprimé", - variant: "success", - }) - - applicationRouter.navigate({ - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/comptes", - params: { - idOrganization: props.account.idOrganization, - idYear: props.account.idYear, - }, - }) - } - - return ( - - Cette action supprimera le compte et toutes les données associées. -
- Cette action est irréversible. - - } - submitButtonProps={{ - text: "Supprimer le compte", - color: "danger", - leftIcon: , - }} - onSubmit={onSubmit} - > - {props.children} -
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/$idAccount/updateOneAccount.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/$idAccount/updateOneAccount.tsx deleted file mode 100644 index 302de26c..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/$idAccount/updateOneAccount.tsx +++ /dev/null @@ -1,295 +0,0 @@ -import { - readAllAccountsRouteDefinition, - readOneAccountRouteDefinition, - updateOneAccountRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { InputText, InputToggle, toast } from "@arrhes/ui" -import { IconPlus } from "@tabler/icons-react" -import { type JSX, useState } from "react" -import { Fragment } from "react/jsx-runtime" -import type * as v from "valibot" -import { FormControl } from "../../../../../../components/forms/formControl.tsx" -import { FormError } from "../../../../../../components/forms/formError.tsx" -import { FormField } from "../../../../../../components/forms/formField.tsx" -import { FormItem } from "../../../../../../components/forms/formItem.tsx" -import { FormLabel } from "../../../../../../components/forms/formLabel.tsx" -import { FormRoot } from "../../../../../../components/forms/formRoot.tsx" -import { Drawer } from "../../../../../../components/overlays/drawer/drawer.tsx" -import { getResponseBodyFromAPI } from "../../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../../utilities/invalidateData.ts" -import { BalanceSheetsSelect } from "../../balanceSheets/balanceSheetSelect.tsx" -import { AccountSelect } from "../accountSelect.tsx" - -export function UpdateOneAccount(props: { - account: v.InferOutput - children: JSX.Element -}) { - const [open, setOpen] = useState(false) - - return ( - - {props.children} - - - - , - text: "Modifier le compte", - }} - onSubmit={async (data) => { - const updateAccountResponse = await getResponseBodyFromAPI({ - routeDefinition: updateOneAccountRouteDefinition, - body: data, - }) - if (updateAccountResponse.ok === false) { - toast({ - title: "Impossible de modifier le compte", - variant: "error", - }) - return false - } - - toast({ - title: "Compte modifié avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await Promise.all([ - invalidateData({ - routeDefinition: readAllAccountsRouteDefinition, - body: { - idYear: props.account.idYear, - }, - }), - invalidateData({ - routeDefinition: readOneAccountRouteDefinition, - body: { - idAccount: props.account.id, - idYear: props.account.idYear, - }, - }), - ]) - - setOpen(false) - }} - > - {(form) => ( - - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - - )} - - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/accountSelect.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/AccountSelect.tsx similarity index 100% rename from packages/website/src/features/dashboard/$idYear/yearSettings/accounts/accountSelect.tsx rename to packages/website/src/features/dashboard/$idYear/yearSettings/accounts/AccountSelect.tsx diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/AccountsPage.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/AccountsPage.tsx new file mode 100644 index 00000000..a3250f7d --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/AccountsPage.tsx @@ -0,0 +1,88 @@ +import { ButtonPlainContent, InputDebounced, InputText } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPlus } from "@tabler/icons-react" +import { useParams } from "@tanstack/react-router" +import { useCallback, useState, useTransition } from "react" +import { Box } from "../../../../../components/layouts/Box.tsx" +import { Page } from "../../../../../components/layouts/page/page.tsx" +import { Section } from "../../../../../components/layouts/section/section.tsx" +import { AccountsTable } from "./AccountsTable.tsx" +import { CreateOneAccount } from "./CreateOneAccount.tsx" + +export function AccountsPage({ + idOrganization: idOrganizationProp, + idYear: idYearProp, +}: { + idOrganization?: string + idYear?: string +} = {}) { + const params = useParams({ + strict: false, + }) as { + idOrganization?: string + idYear?: string + } + const idOrganization = idOrganizationProp ?? params.idOrganization ?? "" + const idYear = idYearProp ?? params.idYear ?? "" + + const [globalFilter, setGlobalFilter] = useState("") + const [, startTransition] = useTransition() + + const handleFilterChange = useCallback((value: string | undefined) => { + startTransition(() => { + setGlobalFilter(value ?? "") + }) + }, []) + + return ( + + + + +
+ + } + text="Ajouter un compte" + /> + +
+ + + + + + +
+
+
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/AccountsTable.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/AccountsTable.tsx new file mode 100644 index 00000000..0d581e0f --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/AccountsTable.tsx @@ -0,0 +1,145 @@ +import { readAllAccountsRouteDefinition } from "@arrhes/application-metadata/routes" +import { CircularLoader, FormatError } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconListNumbers } from "@tabler/icons-react" +import { useNavigate } from "@tanstack/react-router" +import { type MouseEvent, useCallback, useMemo } from "react" +import { EmptyState } from "../../../../../components/layouts/EmptyState.tsx" +import { Virtualizer } from "../../../../../components/layouts/Virtualizer.tsx" +import { useDataFromAPI } from "../../../../../utilities/useHTTPData.ts" +import { ACCOUNT_ITEM_HEIGHT, AccountItem } from "./accountItem.tsx" +import { sortAccounts } from "./sortAccounts.tsx" + +export function AccountsTable(props: { idOrganization: string; idYear: string; globalFilter: string }) { + const navigate = useNavigate() + + const response = useDataFromAPI({ + routeDefinition: readAllAccountsRouteDefinition, + body: { + idYear: props.idYear, + }, + }) + + const structuredAccounts = useMemo(() => { + if (!response.data) return [] + + const normalizedFilter = props.globalFilter.trim().toLowerCase() + const filtered = + normalizedFilter === "" + ? response.data + : response.data.filter((account) => { + const text = `${account.number} ${account.label}`.toLowerCase() + return text.includes(normalizedFilter) + }) + + const sorted = [ + ...filtered, + ].sort((a, b) => a.number.toString().localeCompare(b.number.toString())) + + return sortAccounts({ + accounts: sorted, + }) + }, [ + response.data, + props.globalFilter, + ]) + + const hrefBase = `/dashboard/organisations/${props.idOrganization}/exercices/${props.idYear}/param%C3%A8tres/comptes/` + + const handleContainerClick = useCallback( + (e: MouseEvent) => { + if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return + + const link = (e.target as HTMLElement).closest("a[data-account-link]") as HTMLAnchorElement | null + if (!link) return + + e.preventDefault() + const href = link.getAttribute("href") + if (!href) return + + const idAccount = href.split("/").pop() + if (!idAccount) return + + navigate({ + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/comptes/$idAccount", + params: { + idOrganization: props.idOrganization, + idYear: props.idYear, + idAccount, + }, + }) + }, + [ + navigate, + props.idOrganization, + props.idYear, + ], + ) + + const renderAccount = useCallback( + (sortedAccount: (typeof structuredAccounts)[number]) => ( + + ), + [ + hrefBase, + ], + ) + + return ( +
+ {response.data === undefined ? ( + response.isPending ? ( +
+ +
+ ) : ( + + ) + ) : structuredAccounts.length === 0 ? ( + } + title={props.globalFilter ? "Aucun compte trouvé" : "Aucun compte"} + subtitle={props.globalFilter ? undefined : "Ajoutez un compte pour commencer"} + /> + ) : ( +
+ + {renderAccount} + +
+ )} +
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/CreateOneAccount.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/CreateOneAccount.tsx new file mode 100644 index 00000000..fd3156c4 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/CreateOneAccount.tsx @@ -0,0 +1,302 @@ +import { createOneAccountRouteDefinition, readAllAccountsRouteDefinition } from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, InputText, InputToggle, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPlus } from "@tabler/icons-react" +import type { JSX } from "react" +import { Fragment } from "react/jsx-runtime" +import type * as v from "valibot" +import { FormControl } from "../../../../../components/forms/FormControl.tsx" +import { FormError } from "../../../../../components/forms/FormError.tsx" +import { FormField } from "../../../../../components/forms/FormField.tsx" +import { FormItem } from "../../../../../components/forms/FormItem.tsx" +import { FormLabel } from "../../../../../components/forms/FormLabel.tsx" +import { FormRoot } from "../../../../../components/forms/FormRoot.tsx" +import { useTabs } from "../../../../../contexts/tabs/useTabs.tsx" +import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../utilities/invalidateData.ts" +import { BalanceSheetsSelect } from "../balanceSheets/BalanceSheetsSelect.tsx" +import { AccountSelect } from "./AccountSelect.tsx" + +export function CreateOneAccount(props: { + idOrganization: v.InferOutput["id"] + idYear: v.InferOutput["id"] + children: JSX.Element +}) { + const { openPanelTab, closeTab } = useTabs() + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/accountsPage.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/accountsPage.tsx deleted file mode 100644 index 1a424e66..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/accountsPage.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { ButtonPlainContent, InputDebounced, InputText } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconPlus } from "@tabler/icons-react" -import { useParams } from "@tanstack/react-router" -import { useCallback, useState, useTransition } from "react" -import { Box } from "../../../../../components/layouts/box.tsx" -import { Page } from "../../../../../components/layouts/page/page.tsx" -import { Section } from "../../../../../components/layouts/section/section.tsx" -import { accountsRoute } from "../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/accounts/accountsRoute.tsx" -import { AccountsTable } from "./accountsTable.tsx" -import { CreateOneAccount } from "./createOneAccount.tsx" - -export function AccountsPage() { - const params = useParams({ - from: accountsRoute.id, - }) - - const [globalFilter, setGlobalFilter] = useState("") - const [, startTransition] = useTransition() - - const handleFilterChange = useCallback((value: string | undefined) => { - startTransition(() => { - setGlobalFilter(value ?? "") - }) - }, []) - - return ( - - - - -
- - } - text="Ajouter un compte" - /> - -
- - - - - - -
-
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/accountsTable.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/accountsTable.tsx deleted file mode 100644 index fa27a4d0..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/accountsTable.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { readAllAccountsRouteDefinition } from "@arrhes/application-metadata/routes" -import { CircularLoader, FormatError } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconListNumbers } from "@tabler/icons-react" -import { useNavigate } from "@tanstack/react-router" -import { type MouseEvent, useCallback, useMemo } from "react" -import { EmptyState } from "../../../../../components/layouts/emptyState.tsx" -import { Virtualizer } from "../../../../../components/layouts/virtualizer.tsx" -import { useDataFromAPI } from "../../../../../utilities/useHTTPData.ts" -import { ACCOUNT_ITEM_HEIGHT, AccountItem } from "./accountItem.tsx" -import { sortAccounts } from "./sortAccounts.tsx" - -export function AccountsTable(props: { idOrganization: string; idYear: string; globalFilter: string }) { - const navigate = useNavigate() - - const response = useDataFromAPI({ - routeDefinition: readAllAccountsRouteDefinition, - body: { - idYear: props.idYear, - }, - }) - - const structuredAccounts = useMemo(() => { - if (!response.data) return [] - - const normalizedFilter = props.globalFilter.trim().toLowerCase() - const filtered = - normalizedFilter === "" - ? response.data - : response.data.filter((account) => { - const text = `${account.number} ${account.label}`.toLowerCase() - return text.includes(normalizedFilter) - }) - - const sorted = [ - ...filtered, - ].sort((a, b) => a.number.toString().localeCompare(b.number.toString())) - - return sortAccounts({ - accounts: sorted, - }) - }, [ - response.data, - props.globalFilter, - ]) - - const hrefBase = `/dashboard/organisations/${props.idOrganization}/exercices/${props.idYear}/param%C3%A8tres/comptes/` - - const handleContainerClick = useCallback( - (e: MouseEvent) => { - if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return - - const link = (e.target as HTMLElement).closest("a[data-account-link]") as HTMLAnchorElement | null - if (!link) return - - e.preventDefault() - const href = link.getAttribute("href") - if (!href) return - - const idAccount = href.split("/").pop() - if (!idAccount) return - - navigate({ - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/comptes/$idAccount", - params: { - idOrganization: props.idOrganization, - idYear: props.idYear, - idAccount, - }, - }) - }, - [ - navigate, - props.idOrganization, - props.idYear, - ], - ) - - const renderAccount = useCallback( - (sortedAccount: (typeof structuredAccounts)[number]) => ( - - ), - [ - hrefBase, - ], - ) - - return ( -
- {response.data === undefined ? ( - response.isPending ? ( -
- -
- ) : ( - - ) - ) : structuredAccounts.length === 0 ? ( - } - title={props.globalFilter ? "Aucun compte trouvé" : "Aucun compte"} - subtitle={props.globalFilter ? undefined : "Ajoutez un compte pour commencer"} - /> - ) : ( -
- - {renderAccount} - -
- )} -
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/createOneAccount.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/createOneAccount.tsx deleted file mode 100644 index d79d9f2c..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/accounts/createOneAccount.tsx +++ /dev/null @@ -1,284 +0,0 @@ -import { createOneAccountRouteDefinition, readAllAccountsRouteDefinition } from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { InputText, InputToggle, toast } from "@arrhes/ui" -import { IconPlus } from "@tabler/icons-react" -import { type JSX, useState } from "react" -import { Fragment } from "react/jsx-runtime" -import type * as v from "valibot" -import { FormControl } from "../../../../../components/forms/formControl.tsx" -import { FormError } from "../../../../../components/forms/formError.tsx" -import { FormField } from "../../../../../components/forms/formField.tsx" -import { FormItem } from "../../../../../components/forms/formItem.tsx" -import { FormLabel } from "../../../../../components/forms/formLabel.tsx" -import { FormRoot } from "../../../../../components/forms/formRoot.tsx" -import { Drawer } from "../../../../../components/overlays/drawer/drawer.tsx" -import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../utilities/invalidateData.ts" -import { BalanceSheetsSelect } from "../balanceSheets/balanceSheetSelect.tsx" -import { AccountSelect } from "./accountSelect.tsx" - -export function CreateOneAccount(props: { - idOrganization: v.InferOutput["id"] - idYear: v.InferOutput["id"] - children: JSX.Element -}) { - const [open, setOpen] = useState(false) - - return ( - - {props.children} - - - - , - text: "Ajouter le compte", - }} - onSubmit={async (data) => { - const createAccountResponse = await getResponseBodyFromAPI({ - routeDefinition: createOneAccountRouteDefinition, - body: data, - }) - if (createAccountResponse.ok === false) { - toast({ - title: "Impossible d'ajouter le compte", - variant: "error", - }) - return false - } - - toast({ - title: "Compte ajouté avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await invalidateData({ - routeDefinition: readAllAccountsRouteDefinition, - body: { - idYear: props.idYear, - }, - }) - - setOpen(false) - }} - > - {(form) => ( - - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - - )} - - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/BalanceSheetLayout.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/BalanceSheetLayout.tsx new file mode 100644 index 00000000..e6f1c667 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/BalanceSheetLayout.tsx @@ -0,0 +1,117 @@ +import { readOneBalanceSheetRouteDefinition } from "@arrhes/application-metadata/routes" +import { ButtonOutlineContent, ButtonPlainContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconChevronLeft, IconDatabase, IconInfoCircle, IconPencil, IconTrash } from "@tabler/icons-react" +import { Outlet, useParams } from "@tanstack/react-router" +import { LinkButton } from "../../../../../../components/LinkButton.tsx" +import { DataWrapper } from "../../../../../../components/layouts/DataWrapper.tsx" +import { Section } from "../../../../../../components/layouts/section/section.tsx" +import { Tab } from "../../../../../../components/layouts/tab/tab.tsx" + +import { DeleteOneBalanceSheet } from "./DeleteOneBalanceSheet.tsx" +import { UpdateOneBalanceSheet } from "./UpdateOneBalanceSheet.tsx" + +export function BalanceSheetLayout() { + const params = useParams({ + strict: false, + }) as { + idYear: string + idBalanceSheet: string + idOrganization: string + } + + return ( + + + {(balanceSheet) => { + return ( + <> + +
+ + } + text="Retour" + /> + +
+
+ + } + text="Modifier" + /> + + + } + color="danger" + /> + +
+
+ + , + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/bilan/$idBalanceSheet", + params: { + idOrganization: params.idOrganization, + idYear: params.idYear, + idBalanceSheet: params.idBalanceSheet, + }, + }, + { + label: "Métadonnées", + icon: , + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/bilan/$idBalanceSheet/métadonnées", + params: { + idOrganization: params.idOrganization, + idYear: params.idYear, + idBalanceSheet: params.idBalanceSheet, + }, + }, + ]} + /> + + + + ) + }} +
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/BalanceSheetMetadataTab.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/BalanceSheetMetadataTab.tsx new file mode 100644 index 00000000..7b9f0b02 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/BalanceSheetMetadataTab.tsx @@ -0,0 +1,51 @@ +import { readOneBalanceSheetRouteDefinition } from "@arrhes/application-metadata/routes" +import { FormatDateTime, FormatText } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { useParams } from "@tanstack/react-router" +import { DataWrapper } from "../../../../../../components/layouts/DataWrapper.tsx" +import { DataBlock } from "../../../../../../components/layouts/dataBlock/dataBlock.tsx" +import { Section } from "../../../../../../components/layouts/section/section.tsx" + +export function BalanceSheetMetadataTab() { + const params = useParams({ + strict: false, + }) as { + idYear: string + idBalanceSheet: string + } + + return ( + + {(balanceSheet) => { + return ( + + + + + + + + + + + + {balanceSheet.id} + + + + + ) + }} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/BalanceSheetPage.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/BalanceSheetPage.tsx new file mode 100644 index 00000000..44186c53 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/BalanceSheetPage.tsx @@ -0,0 +1,48 @@ +import { readOneBalanceSheetRouteDefinition } from "@arrhes/application-metadata/routes" +import { FormatText } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { useParams } from "@tanstack/react-router" +import { DataWrapper } from "../../../../../../components/layouts/DataWrapper.tsx" +import { DataBlock } from "../../../../../../components/layouts/dataBlock/dataBlock.tsx" +import { Section } from "../../../../../../components/layouts/section/section.tsx" + +export function BalanceSheetPage() { + const params = useParams({ + strict: false, + }) as { + idYear: string + idBalanceSheet: string + } + + return ( + + {(balanceSheet) => { + return ( + + + + + + {balanceSheet.number.toString()} + + + {balanceSheet.label} + + + + + ) + }} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/DeleteOneBalanceSheet.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/DeleteOneBalanceSheet.tsx new file mode 100644 index 00000000..c77160b3 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/DeleteOneBalanceSheet.tsx @@ -0,0 +1,97 @@ +import { + deleteOneBalanceSheetRouteDefinition, + readAllBalanceSheetsRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, ButtonOutlineContent, ButtonPlainContent, Dialog, toast, useModalStore } from "@arrhes/ui" +import { type ComponentPropsWithRef, type ReactElement, useId } from "react" +import type * as v from "valibot" +import { applicationRouter } from "../../../../../../routes/applicationRouter.tsx" +import { getResponseBodyFromAPI } from "../../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../../utilities/invalidateData.ts" + +export function DeleteOneBalanceSheet(props: { + balanceSheet: v.InferOutput + children: ReactElement> +}) { + const modalId = useId() + const { open: openModal, close: closeModal } = useModalStore() + + async function onSubmit() { + const deleteResponse = await getResponseBodyFromAPI({ + routeDefinition: deleteOneBalanceSheetRouteDefinition, + body: { + idBalanceSheet: props.balanceSheet.id, + idYear: props.balanceSheet.idYear, + }, + }) + + if (deleteResponse.ok === false) { + toast({ + title: "Erreur lors de la suppression de la ligne de bilan", + variant: "error", + }) + return + } + + await invalidateData({ + routeDefinition: readAllBalanceSheetsRouteDefinition, + body: { + idYear: props.balanceSheet.idYear, + }, + }) + + toast({ + title: "Ligne de bilan supprimée", + variant: "success", + }) + + applicationRouter.navigate({ + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/bilan", + params: { + idOrganization: props.balanceSheet.idOrganization, + idYear: props.balanceSheet.idYear, + }, + }) + } + + return ( + + + + , + ) + } + > + {props.children} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/UpdateOneBalanceSheet.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/UpdateOneBalanceSheet.tsx new file mode 100644 index 00000000..75dfa238 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/UpdateOneBalanceSheet.tsx @@ -0,0 +1,208 @@ +import { + readAllBalanceSheetsRouteDefinition, + readOneBalanceSheetRouteDefinition, + updateOneBalanceSheetRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, InputText, InputToggle, toast } from "@arrhes/ui" +import { css, cx } from "@arrhes/ui/utilities/cn.js" +import { IconPlus } from "@tabler/icons-react" +import type { JSX } from "react" +import { Fragment } from "react/jsx-runtime" +import type * as v from "valibot" +import { FormControl } from "../../../../../../components/forms/FormControl.tsx" +import { FormError } from "../../../../../../components/forms/FormError.tsx" +import { FormField } from "../../../../../../components/forms/FormField.tsx" +import { FormItem } from "../../../../../../components/forms/FormItem.tsx" +import { FormLabel } from "../../../../../../components/forms/FormLabel.tsx" +import { FormRoot } from "../../../../../../components/forms/FormRoot.tsx" +import { useTabs } from "../../../../../../contexts/tabs/useTabs.tsx" +import { getResponseBodyFromAPI } from "../../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../../utilities/invalidateData.ts" +import { BalanceSheetsSelect } from "../BalanceSheetsSelect.tsx" + +export function UpdateOneBalanceSheet(props: { + balanceSheet: v.InferOutput + children: JSX.Element + className?: string +}) { + const { openPanelTab, closeTab } = useTabs() + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/balanceSheetLayout.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/balanceSheetLayout.tsx deleted file mode 100644 index 794ecba5..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/balanceSheetLayout.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { readOneBalanceSheetRouteDefinition } from "@arrhes/application-metadata/routes" -import { ButtonOutlineContent, ButtonPlainContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconChevronLeft, IconDatabase, IconInfoCircle, IconPencil, IconTrash } from "@tabler/icons-react" -import { Outlet, useParams } from "@tanstack/react-router" -import { DataWrapper } from "../../../../../../components/layouts/dataWrapper.tsx" -import { Section } from "../../../../../../components/layouts/section/section.tsx" -import { Tab } from "../../../../../../components/layouts/tab/tab.tsx" -import { LinkButton } from "../../../../../../components/linkButton.tsx" -import { balanceSheetLayoutRoute } from "../../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/balanceSheetLayoutRoute.tsx" -import { DeleteOneBalanceSheet } from "./deleteOneBalanceSheet.tsx" -import { UpdateOneBalanceSheet } from "./updateOneBalanceSheet.tsx" - -export function BalanceSheetLayout() { - const params = useParams({ - from: balanceSheetLayoutRoute.id, - }) - - return ( - - - {(balanceSheet) => { - return ( - <> - -
- - } - text="Retour" - /> - -
-
- - } - text="Modifier" - /> - - - } - color="danger" - /> - -
-
- - , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/bilan/$idBalanceSheet", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - idBalanceSheet: params.idBalanceSheet, - }, - }, - { - label: "Métadonnées", - icon: , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/bilan/$idBalanceSheet/métadonnées", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - idBalanceSheet: params.idBalanceSheet, - }, - }, - ]} - /> - - - - ) - }} -
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/balanceSheetMetadataTab.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/balanceSheetMetadataTab.tsx deleted file mode 100644 index 4c734497..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/balanceSheetMetadataTab.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { readOneBalanceSheetRouteDefinition } from "@arrhes/application-metadata/routes" -import { FormatDateTime, FormatText } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { useParams } from "@tanstack/react-router" -import { DataBlock } from "../../../../../../components/layouts/dataBlock/dataBlock.tsx" -import { DataWrapper } from "../../../../../../components/layouts/dataWrapper.tsx" -import { Section } from "../../../../../../components/layouts/section/section.tsx" -import { balanceSheetLayoutRoute } from "../../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/balanceSheetLayoutRoute.tsx" - -export function BalanceSheetMetadataTab() { - const params = useParams({ - from: balanceSheetLayoutRoute.id, - }) - - return ( - - {(balanceSheet) => { - return ( - - - - - - - - - - - - {balanceSheet.id} - - - - - ) - }} - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/balanceSheetPage.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/balanceSheetPage.tsx deleted file mode 100644 index 3112b94d..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/balanceSheetPage.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { readOneBalanceSheetRouteDefinition } from "@arrhes/application-metadata/routes" -import { FormatText } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { useParams } from "@tanstack/react-router" -import { DataBlock } from "../../../../../../components/layouts/dataBlock/dataBlock.tsx" -import { DataWrapper } from "../../../../../../components/layouts/dataWrapper.tsx" -import { Section } from "../../../../../../components/layouts/section/section.tsx" -import { balanceSheetLayoutRoute } from "../../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/balanceSheetLayoutRoute.tsx" - -export function BalanceSheetPage() { - const params = useParams({ - from: balanceSheetLayoutRoute.id, - }) - - return ( - - {(balanceSheet) => { - return ( - - - - - - {balanceSheet.number.toString()} - - - {balanceSheet.label} - - - - - ) - }} - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/deleteOneBalanceSheet.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/deleteOneBalanceSheet.tsx deleted file mode 100644 index 278170f9..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/deleteOneBalanceSheet.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { - deleteOneBalanceSheetRouteDefinition, - readAllBalanceSheetsRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { toast } from "@arrhes/ui" -import type { ComponentPropsWithRef, ReactElement } from "react" -import type * as v from "valibot" -import { ConfirmationModal } from "../../../../../../components/overlays/dialog/confirmationModal.tsx" -import { applicationRouter } from "../../../../../../routes/applicationRouter.tsx" -import { getResponseBodyFromAPI } from "../../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../../utilities/invalidateData.ts" - -export function DeleteOneBalanceSheet(props: { - balanceSheet: v.InferOutput - children: ReactElement> -}) { - async function onSubmit() { - const deleteResponse = await getResponseBodyFromAPI({ - routeDefinition: deleteOneBalanceSheetRouteDefinition, - body: { - idBalanceSheet: props.balanceSheet.id, - idYear: props.balanceSheet.idYear, - }, - }) - - if (deleteResponse.ok === false) { - toast({ - title: "Erreur lors de la suppression de la ligne de bilan", - variant: "error", - }) - return - } - - await invalidateData({ - routeDefinition: readAllBalanceSheetsRouteDefinition, - body: { - idYear: props.balanceSheet.idYear, - }, - }) - - toast({ - title: "Ligne de bilan supprimée", - variant: "success", - }) - - applicationRouter.navigate({ - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/bilan", - params: { - idOrganization: props.balanceSheet.idOrganization, - idYear: props.balanceSheet.idYear, - }, - }) - } - - return ( - - Cette action supprimera la ligne de bilan et toutes les données associées. -
- Cette action est irréversible. - - } - submitButtonProps={{ - color: "danger", - text: "Supprimer la ligne de bilan", - }} - onSubmit={onSubmit} - > - {props.children} -
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/updateOneBalanceSheet.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/updateOneBalanceSheet.tsx deleted file mode 100644 index 9827d01b..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/updateOneBalanceSheet.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import { - readAllBalanceSheetsRouteDefinition, - readOneBalanceSheetRouteDefinition, - updateOneBalanceSheetRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { InputText, InputToggle, toast } from "@arrhes/ui" -import { IconPlus } from "@tabler/icons-react" -import { type JSX, useState } from "react" -import { Fragment } from "react/jsx-runtime" -import type * as v from "valibot" -import { FormControl } from "../../../../../../components/forms/formControl.tsx" -import { FormError } from "../../../../../../components/forms/formError.tsx" -import { FormField } from "../../../../../../components/forms/formField.tsx" -import { FormItem } from "../../../../../../components/forms/formItem.tsx" -import { FormLabel } from "../../../../../../components/forms/formLabel.tsx" -import { FormRoot } from "../../../../../../components/forms/formRoot.tsx" -import { Drawer } from "../../../../../../components/overlays/drawer/drawer.tsx" -import { getResponseBodyFromAPI } from "../../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../../utilities/invalidateData.ts" -import { BalanceSheetsSelect } from "../balanceSheetSelect.tsx" - -export function UpdateOneBalanceSheet(props: { - balanceSheet: v.InferOutput - children: JSX.Element -}) { - const [open, setOpen] = useState(false) - - return ( - - {props.children} - - - - , - text: "Modifier la ligne de bilan", - }} - onSubmit={async (data) => { - const updateBalanceSheetResponse = await getResponseBodyFromAPI({ - routeDefinition: updateOneBalanceSheetRouteDefinition, - body: data, - }) - if (updateBalanceSheetResponse.ok === false) { - toast({ - title: "Impossible de modifier la ligne de bilan", - variant: "error", - }) - return false - } - - toast({ - title: "Ligne de bilan modifiée avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await Promise.all([ - invalidateData({ - routeDefinition: readAllBalanceSheetsRouteDefinition, - body: { - idYear: props.balanceSheet.idYear, - }, - }), - invalidateData({ - routeDefinition: readOneBalanceSheetRouteDefinition, - body: { - idBalanceSheet: props.balanceSheet.id, - idYear: props.balanceSheet.idYear, - }, - }), - ]) - - setOpen(false) - }} - > - {(form) => ( - - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - - )} - - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/BalanceSheetItem.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/BalanceSheetItem.tsx new file mode 100644 index 00000000..0c69a138 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/BalanceSheetItem.tsx @@ -0,0 +1,44 @@ +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { type ComponentProps, Fragment } from "react" +import type * as v from "valibot" +import { BalanceSheetRow } from "./BalanceSheetRow.tsx" +import { getBalanceSheetChildren } from "./getBalanceSheetChildren.tsx" + +export function BalanceSheetItem(props: { + idOrganization: v.InferOutput["id"] + idYear: v.InferOutput["id"] + balanceSheet: v.InferOutput + balanceSheetChildren: Array> + level: number + className?: ComponentProps<"div">["className"] +}) { + return ( + + + {props.balanceSheetChildren + .filter((balanceSheet) => balanceSheet.idBalanceSheetParent === props.balanceSheet.id) + .map((balanceSheet) => { + const balanceSheetChildren = getBalanceSheetChildren({ + balanceSheet: balanceSheet, + balanceSheets: props.balanceSheetChildren, + }) + + return ( + + ) + })} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/BalanceSheetRow.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/BalanceSheetRow.tsx new file mode 100644 index 00000000..929ffda9 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/BalanceSheetRow.tsx @@ -0,0 +1,69 @@ +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { css } from "@arrhes/ui/utilities/cn.js" +import type { ComponentProps } from "react" +import type * as v from "valibot" +import { ACCOUNT_ITEM_HEIGHT, INDENT_PER_LEVEL } from "../accounts/accountItem.tsx" +import { UpdateOneBalanceSheet } from "./$idBalanceSheet/UpdateOneBalanceSheet.tsx" + +export function BalanceSheetRow(props: { + idOrganization: v.InferOutput["id"] + idYear: v.InferOutput["id"] + balanceSheet: v.InferOutput + level: number + className?: ComponentProps<"div">["className"] +}) { + return ( + +
+ + {props.balanceSheet.number} + + + {props.balanceSheet.label} + +
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/BalanceSheetTable.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/BalanceSheetTable.tsx new file mode 100644 index 00000000..b54e5004 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/BalanceSheetTable.tsx @@ -0,0 +1,94 @@ +import { readAllBalanceSheetsRouteDefinition } from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconScale } from "@tabler/icons-react" +import type * as v from "valibot" +import { DataWrapper } from "../../../../../components/layouts/DataWrapper.tsx" +import { EmptyState } from "../../../../../components/layouts/EmptyState.tsx" +import { BalanceSheetItem } from "./BalanceSheetItem.tsx" +import { getBalanceSheetChildren } from "./getBalanceSheetChildren.tsx" + +export function BalanceSheetTable(props: { + idOrganization: v.InferOutput["id"] + idYear: v.InferOutput["id"] + side: "asset" | "liability" + globalFilter: string +}) { + const normalizedGlobalFilter = props.globalFilter.trim().toLowerCase() + + return ( + + {(balanceSheets) => { + const sidedBalanceSheets = balanceSheets.filter((balanceSheet) => balanceSheet.side === props.side) + + const filteredBalanceSheets = sidedBalanceSheets + .filter((balanceSheet) => balanceSheet.idBalanceSheetParent === null) + .filter((balanceSheet) => { + if (normalizedGlobalFilter.length === 0) { + return true + } + + return `${balanceSheet.number} ${balanceSheet.label}` + .toLowerCase() + .includes(normalizedGlobalFilter) + }) + .sort((a, b) => Number(a.number) - Number(b.number)) + + return ( +
+
+ {filteredBalanceSheets.length === 0 && ( + } + title={props.globalFilter ? "Aucune ligne trouvée" : "Aucune ligne de bilan"} + subtitle={props.globalFilter ? undefined : "Ajoutez une ligne pour commencer"} + /> + )} + {filteredBalanceSheets.map((balanceSheet) => { + const balanceSheetChildren = getBalanceSheetChildren({ + balanceSheet: balanceSheet, + balanceSheets: sidedBalanceSheets, + }) + + return ( + + ) + })} +
+
+ ) + }} +
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/BalanceSheetsPage.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/BalanceSheetsPage.tsx new file mode 100644 index 00000000..64557d15 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/BalanceSheetsPage.tsx @@ -0,0 +1,121 @@ +import { ButtonPlainContent, InputDebounced, InputText } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPlus, IconScale } from "@tabler/icons-react" +import { useParams, useRouterState } from "@tanstack/react-router" +import { useCallback, useState, useTransition } from "react" +import { Box } from "../../../../../components/layouts/Box.tsx" +import { Page } from "../../../../../components/layouts/page/page.tsx" +import { Section } from "../../../../../components/layouts/section/section.tsx" +import { Tab } from "../../../../../components/layouts/tab/tab.tsx" +import { BalanceSheetTable } from "./BalanceSheetTable.tsx" +import { CreateOneBalanceSheet } from "./CreateOneBalanceSheet.tsx" + +export function BalanceSheetsPage({ + idOrganization: idOrganizationProp, + idYear: idYearProp, +}: { + idOrganization?: string + idYear?: string +} = {}) { + const params = useParams({ + strict: false, + }) as { + idOrganization?: string + idYear?: string + } + const idOrganization = idOrganizationProp ?? params.idOrganization ?? "" + const idYear = idYearProp ?? params.idYear ?? "" + const [globalFilter, setGlobalFilter] = useState("") + const [, startTransition] = useTransition() + const pathname = useRouterState({ + select: (state) => state.location.pathname, + }) + const side = pathname.endsWith("/passif") ? "liability" : "asset" + + const handleFilterChange = useCallback((value: string | undefined) => { + startTransition(() => { + setGlobalFilter(value ?? "") + }) + }, []) + + return ( + + + + + , + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/bilan/actif", + params: { + idOrganization: idOrganization, + idYear: idYear, + }, + }, + { + label: "Passif", + icon: , + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/bilan/passif", + params: { + idOrganization: idOrganization, + idYear: idYear, + }, + }, + ]} + /> + + +
+ + } + text="Ajouter une ligne de bilan" + /> + +
+ + + + + + +
+
+
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/balanceSheetSelect.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/BalanceSheetsSelect.tsx similarity index 100% rename from packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/balanceSheetSelect.tsx rename to packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/BalanceSheetsSelect.tsx diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/CreateOneBalanceSheet.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/CreateOneBalanceSheet.tsx new file mode 100644 index 00000000..a6afae1a --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/CreateOneBalanceSheet.tsx @@ -0,0 +1,196 @@ +import { + createOneBalanceSheetRouteDefinition, + readAllBalanceSheetsRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, InputText, InputToggle, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPlus } from "@tabler/icons-react" +import type { JSX } from "react" +import { Fragment } from "react/jsx-runtime" +import type * as v from "valibot" +import { FormControl } from "../../../../../components/forms/FormControl.tsx" +import { FormError } from "../../../../../components/forms/FormError.tsx" +import { FormField } from "../../../../../components/forms/FormField.tsx" +import { FormItem } from "../../../../../components/forms/FormItem.tsx" +import { FormLabel } from "../../../../../components/forms/FormLabel.tsx" +import { FormRoot } from "../../../../../components/forms/FormRoot.tsx" +import { useTabs } from "../../../../../contexts/tabs/useTabs.tsx" +import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../utilities/invalidateData.ts" +import { BalanceSheetsSelect } from "./BalanceSheetsSelect.tsx" + +export function CreateOneBalanceSheet(props: { + idOrganization: v.InferOutput["id"] + idYear: v.InferOutput["id"] + children: JSX.Element +}) { + const { openPanelTab, closeTab } = useTabs() + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/balanceSheetItem.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/balanceSheetItem.tsx deleted file mode 100644 index 80dcb70a..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/balanceSheetItem.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { type ComponentProps, Fragment } from "react" -import type * as v from "valibot" -import { BalanceSheetRow } from "./balanceSheetRow.tsx" -import { getBalanceSheetChildren } from "./getBalanceSheetChildren.tsx" - -export function BalanceSheetItem(props: { - idOrganization: v.InferOutput["id"] - idYear: v.InferOutput["id"] - balanceSheet: v.InferOutput - balanceSheetChildren: Array> - level: number - className?: ComponentProps<"div">["className"] -}) { - return ( - - - {props.balanceSheetChildren - .filter((balanceSheet) => balanceSheet.idBalanceSheetParent === props.balanceSheet.id) - .map((balanceSheet) => { - const balanceSheetChildren = getBalanceSheetChildren({ - balanceSheet: balanceSheet, - balanceSheets: props.balanceSheetChildren, - }) - - return ( - - ) - })} - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/balanceSheetRow.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/balanceSheetRow.tsx deleted file mode 100644 index 1e879a3e..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/balanceSheetRow.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { css } from "@arrhes/ui/utilities/cn.js" -import type { ComponentProps } from "react" -import type * as v from "valibot" -import { LinkButton } from "../../../../../components/linkButton.tsx" -import { ACCOUNT_ITEM_HEIGHT, INDENT_PER_LEVEL } from "../accounts/accountItem.tsx" - -export function BalanceSheetRow(props: { - idOrganization: v.InferOutput["id"] - idYear: v.InferOutput["id"] - balanceSheet: v.InferOutput - level: number - className?: ComponentProps<"div">["className"] -}) { - return ( - -
- - {props.balanceSheet.number} - - - {props.balanceSheet.label} - -
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/balanceSheetTable.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/balanceSheetTable.tsx deleted file mode 100644 index 5e4a96e0..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/balanceSheetTable.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { readAllBalanceSheetsRouteDefinition } from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconScale } from "@tabler/icons-react" -import type * as v from "valibot" -import { DataWrapper } from "../../../../../components/layouts/dataWrapper.tsx" -import { EmptyState } from "../../../../../components/layouts/emptyState.tsx" -import { BalanceSheetItem } from "./balanceSheetItem.tsx" -import { getBalanceSheetChildren } from "./getBalanceSheetChildren.tsx" - -export function BalanceSheetTable(props: { - idOrganization: v.InferOutput["id"] - idYear: v.InferOutput["id"] - side: "asset" | "liability" - globalFilter: string -}) { - const normalizedGlobalFilter = props.globalFilter.trim().toLowerCase() - - return ( - - {(balanceSheets) => { - const sidedBalanceSheets = balanceSheets.filter((balanceSheet) => balanceSheet.side === props.side) - - const filteredBalanceSheets = sidedBalanceSheets - .filter((balanceSheet) => balanceSheet.idBalanceSheetParent === null) - .filter((balanceSheet) => { - if (normalizedGlobalFilter.length === 0) { - return true - } - - return `${balanceSheet.number} ${balanceSheet.label}` - .toLowerCase() - .includes(normalizedGlobalFilter) - }) - .sort((a, b) => Number(a.number) - Number(b.number)) - - return ( -
-
- {filteredBalanceSheets.length === 0 && ( - } - title={props.globalFilter ? "Aucune ligne trouvée" : "Aucune ligne de bilan"} - subtitle={props.globalFilter ? undefined : "Ajoutez une ligne pour commencer"} - /> - )} - {filteredBalanceSheets.map((balanceSheet) => { - const balanceSheetChildren = getBalanceSheetChildren({ - balanceSheet: balanceSheet, - balanceSheets: sidedBalanceSheets, - }) - - return ( - - ) - })} -
-
- ) - }} -
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/balanceSheetsPage.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/balanceSheetsPage.tsx deleted file mode 100644 index 7ccba1a3..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/balanceSheetsPage.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { ButtonPlainContent, InputDebounced, InputText } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconPlus, IconScale } from "@tabler/icons-react" -import { useParams, useRouterState } from "@tanstack/react-router" -import { useCallback, useState, useTransition } from "react" -import { Box } from "../../../../../components/layouts/box.tsx" -import { Page } from "../../../../../components/layouts/page/page.tsx" -import { Section } from "../../../../../components/layouts/section/section.tsx" -import { Tab } from "../../../../../components/layouts/tab/tab.tsx" -import { balanceSheetsLayoutRoute } from "../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/balanceSheetsLayoutRoute.tsx" -import { BalanceSheetTable } from "./balanceSheetTable.tsx" -import { CreateOneBalanceSheet } from "./createOneBalanceSheet.tsx" - -export function BalanceSheetsPage() { - const params = useParams({ - from: balanceSheetsLayoutRoute.id, - }) - const [globalFilter, setGlobalFilter] = useState("") - const [, startTransition] = useTransition() - const pathname = useRouterState({ - select: (state) => state.location.pathname, - }) - const side = pathname.endsWith("/passif") ? "liability" : "asset" - - const handleFilterChange = useCallback((value: string | undefined) => { - startTransition(() => { - setGlobalFilter(value ?? "") - }) - }, []) - - return ( - - - - - , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/bilan/actif", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - }, - }, - { - label: "Passif", - icon: , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/bilan/passif", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - }, - }, - ]} - /> - - -
- - } - text="Ajouter une ligne de bilan" - /> - -
- - - - - - -
-
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/createOneBalanceSheet.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/createOneBalanceSheet.tsx deleted file mode 100644 index 8e032029..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/balanceSheets/createOneBalanceSheet.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import { - createOneBalanceSheetRouteDefinition, - readAllBalanceSheetsRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { InputText, InputToggle, toast } from "@arrhes/ui" -import { IconPlus } from "@tabler/icons-react" -import { type JSX, useState } from "react" -import { Fragment } from "react/jsx-runtime" -import type * as v from "valibot" -import { FormControl } from "../../../../../components/forms/formControl.tsx" -import { FormError } from "../../../../../components/forms/formError.tsx" -import { FormField } from "../../../../../components/forms/formField.tsx" -import { FormItem } from "../../../../../components/forms/formItem.tsx" -import { FormLabel } from "../../../../../components/forms/formLabel.tsx" -import { FormRoot } from "../../../../../components/forms/formRoot.tsx" -import { Drawer } from "../../../../../components/overlays/drawer/drawer.tsx" -import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../utilities/invalidateData.ts" -import { BalanceSheetsSelect } from "./balanceSheetSelect.tsx" - -export function CreateOneBalanceSheet(props: { - idOrganization: v.InferOutput["id"] - idYear: v.InferOutput["id"] - children: JSX.Element -}) { - const [open, setOpen] = useState(false) - - return ( - - {props.children} - - - - , - text: "Ajouter la ligne de bilan", - }} - onSubmit={async (data) => { - const createBalanceSheetResponse = await getResponseBodyFromAPI({ - routeDefinition: createOneBalanceSheetRouteDefinition, - body: data, - }) - if (createBalanceSheetResponse.ok === false) { - toast({ - title: "Impossible d'ajouter la ligne de bilan", - variant: "error", - }) - return false - } - - toast({ - title: "Ligne de bilan ajouté avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await invalidateData({ - routeDefinition: readAllBalanceSheetsRouteDefinition, - body: { - idYear: props.idYear, - }, - }) - - setOpen(false) - }} - > - {(form) => ( - - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - - )} - - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/deleteOneYear.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/deleteOneYear.tsx deleted file mode 100644 index dd44c2e1..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/deleteOneYear.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { deleteOneYearRouteDefinition, readAllYearsRouteDefinition } from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { toast } from "@arrhes/ui" -import type { ComponentPropsWithRef, ReactElement } from "react" -import type * as v from "valibot" -import { ConfirmationModal } from "../../../../components/overlays/dialog/confirmationModal.tsx" -import { applicationRouter } from "../../../../routes/applicationRouter.tsx" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../utilities/invalidateData.ts" - -export function DeleteOneYear(props: { - year: v.InferOutput - children: ReactElement> -}) { - async function onSubmit() { - const deleteResponse = await getResponseBodyFromAPI({ - routeDefinition: deleteOneYearRouteDefinition, - body: { - idYear: props.year.id, - }, - }) - - if (deleteResponse.ok === false) { - toast({ - title: "Erreur lors de la suppression de l'exercice", - variant: "error", - }) - return - } - - await invalidateData({ - routeDefinition: readAllYearsRouteDefinition, - body: {}, - }) - - toast({ - title: "Exercice supprimé", - variant: "success", - }) - - applicationRouter.navigate({ - to: "/dashboard/organisations/$idOrganization/exercices", - params: { - idOrganization: props.year.idOrganization, - }, - }) - } - - return ( - - Cette action supprimera l'exercice et toutes les données associées. -
- Cette action est irréversible. - - } - submitButtonProps={{ - color: "danger", - text: "Supprimer l'exercice", - }} - onSubmit={onSubmit} - > - {props.children} -
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/DeleteOneIncomeStatement.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/DeleteOneIncomeStatement.tsx new file mode 100644 index 00000000..bc3cf8db --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/DeleteOneIncomeStatement.tsx @@ -0,0 +1,97 @@ +import { + deleteOneIncomeStatementRouteDefinition, + readAllIncomeStatementsRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, ButtonOutlineContent, ButtonPlainContent, Dialog, toast, useModalStore } from "@arrhes/ui" +import { type ComponentPropsWithRef, type ReactElement, useId } from "react" +import type * as v from "valibot" +import { applicationRouter } from "../../../../../../routes/applicationRouter.tsx" +import { getResponseBodyFromAPI } from "../../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../../utilities/invalidateData.ts" + +export function DeleteOneIncomeStatement(props: { + incomeStatement: v.InferOutput + children: ReactElement> +}) { + const modalId = useId() + const { open: openModal, close: closeModal } = useModalStore() + + async function onSubmit() { + const deleteResponse = await getResponseBodyFromAPI({ + routeDefinition: deleteOneIncomeStatementRouteDefinition, + body: { + idIncomeStatement: props.incomeStatement.id, + idYear: props.incomeStatement.idYear, + }, + }) + + if (deleteResponse.ok === false) { + toast({ + title: "Erreur lors de la suppression de la ligne de compte de résultat", + variant: "error", + }) + return + } + + await invalidateData({ + routeDefinition: readAllIncomeStatementsRouteDefinition, + body: { + idYear: props.incomeStatement.idYear, + }, + }) + + toast({ + title: "Ligne de compte de résultat supprimée", + variant: "success", + }) + + applicationRouter.navigate({ + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/compte-de-résultat", + params: { + idOrganization: props.incomeStatement.idOrganization, + idYear: props.incomeStatement.idYear, + }, + }) + } + + return ( + + + + , + ) + } + > + {props.children} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/IncomeStatementLayout.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/IncomeStatementLayout.tsx new file mode 100644 index 00000000..650432e9 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/IncomeStatementLayout.tsx @@ -0,0 +1,117 @@ +import { readOneIncomeStatementRouteDefinition } from "@arrhes/application-metadata/routes" +import { ButtonOutlineContent, ButtonPlainContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconChevronLeft, IconDatabase, IconInfoCircle, IconPencil, IconTrash } from "@tabler/icons-react" +import { Outlet, useParams } from "@tanstack/react-router" +import { LinkButton } from "../../../../../../components/LinkButton.tsx" +import { DataWrapper } from "../../../../../../components/layouts/DataWrapper.tsx" +import { Section } from "../../../../../../components/layouts/section/section.tsx" +import { Tab } from "../../../../../../components/layouts/tab/tab.tsx" + +import { DeleteOneIncomeStatement } from "./DeleteOneIncomeStatement.tsx" +import { UpdateOneIncomeStatement } from "./UpdateOneIncomeStatement.tsx" + +export function IncomeStatementLayout() { + const params = useParams({ + strict: false, + }) as { + idYear: string + idIncomeStatement: string + idOrganization: string + } + + return ( + + + {(incomeStatement) => { + return ( + <> + +
+ + } + text="Retour" + /> + +
+
+ + } + text="Modifier" + /> + + + } + color="danger" + /> + +
+
+ + , + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/compte-de-résultat/$idIncomeStatement", + params: { + idOrganization: params.idOrganization, + idYear: params.idYear, + idIncomeStatement: params.idIncomeStatement, + }, + }, + { + label: "Métadonnées", + icon: , + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/compte-de-résultat/$idIncomeStatement/métadonnées", + params: { + idOrganization: params.idOrganization, + idYear: params.idYear, + idIncomeStatement: params.idIncomeStatement, + }, + }, + ]} + /> + + + + ) + }} +
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/IncomeStatementMetadataTab.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/IncomeStatementMetadataTab.tsx new file mode 100644 index 00000000..f013b70c --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/IncomeStatementMetadataTab.tsx @@ -0,0 +1,51 @@ +import { readOneIncomeStatementRouteDefinition } from "@arrhes/application-metadata/routes" +import { FormatDateTime, FormatText } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { useParams } from "@tanstack/react-router" +import { DataWrapper } from "../../../../../../components/layouts/DataWrapper.tsx" +import { DataBlock } from "../../../../../../components/layouts/dataBlock/dataBlock.tsx" +import { Section } from "../../../../../../components/layouts/section/section.tsx" + +export function IncomeStatementMetadataTab() { + const params = useParams({ + strict: false, + }) as { + idYear: string + idIncomeStatement: string + } + + return ( + + {(incomeStatement) => { + return ( + + + + + + + + + + + + {incomeStatement.id} + + + + + ) + }} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/IncomeStatementPage.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/IncomeStatementPage.tsx new file mode 100644 index 00000000..7d4f496c --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/IncomeStatementPage.tsx @@ -0,0 +1,48 @@ +import { readOneIncomeStatementRouteDefinition } from "@arrhes/application-metadata/routes" +import { FormatText } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { useParams } from "@tanstack/react-router" +import { DataWrapper } from "../../../../../../components/layouts/DataWrapper.tsx" +import { DataBlock } from "../../../../../../components/layouts/dataBlock/dataBlock.tsx" +import { Section } from "../../../../../../components/layouts/section/section.tsx" + +export function IncomeStatementPage() { + const params = useParams({ + strict: false, + }) as { + idYear: string + idIncomeStatement: string + } + + return ( + + {(incomeStatement) => { + return ( + + + + + + {incomeStatement.number.toString()} + + + {incomeStatement.label} + + + + + ) + }} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/UpdateOneIncomeStatement.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/UpdateOneIncomeStatement.tsx new file mode 100644 index 00000000..8628af9d --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/UpdateOneIncomeStatement.tsx @@ -0,0 +1,188 @@ +import { + readAllIncomeStatementsRouteDefinition, + readOneIncomeStatementRouteDefinition, + updateOneIncomeStatementRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, InputText, toast } from "@arrhes/ui" +import { css, cx } from "@arrhes/ui/utilities/cn.js" +import { IconPlus } from "@tabler/icons-react" +import type { JSX } from "react" +import { Fragment } from "react/jsx-runtime" +import type * as v from "valibot" +import { FormControl } from "../../../../../../components/forms/FormControl.tsx" +import { FormError } from "../../../../../../components/forms/FormError.tsx" +import { FormField } from "../../../../../../components/forms/FormField.tsx" +import { FormItem } from "../../../../../../components/forms/FormItem.tsx" +import { FormLabel } from "../../../../../../components/forms/FormLabel.tsx" +import { FormRoot } from "../../../../../../components/forms/FormRoot.tsx" +import { InputDataCombobox } from "../../../../../../components/InputDataCombobox.tsx" +import { useTabs } from "../../../../../../contexts/tabs/useTabs.tsx" +import { getResponseBodyFromAPI } from "../../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../../utilities/invalidateData.ts" + +export function UpdateOneIncomeStatement(props: { + incomeStatement: v.InferOutput + children: JSX.Element + className?: string +}) { + const { openPanelTab, closeTab } = useTabs() + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/deleteOneIncomeStatement.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/deleteOneIncomeStatement.tsx deleted file mode 100644 index cc696511..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/deleteOneIncomeStatement.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { - deleteOneIncomeStatementRouteDefinition, - readAllIncomeStatementsRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { toast } from "@arrhes/ui" -import type { ComponentPropsWithRef, ReactElement } from "react" -import type * as v from "valibot" -import { ConfirmationModal } from "../../../../../../components/overlays/dialog/confirmationModal.tsx" -import { applicationRouter } from "../../../../../../routes/applicationRouter.tsx" -import { getResponseBodyFromAPI } from "../../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../../utilities/invalidateData.ts" - -export function DeleteOneIncomeStatement(props: { - incomeStatement: v.InferOutput - children: ReactElement> -}) { - async function onSubmit() { - const deleteResponse = await getResponseBodyFromAPI({ - routeDefinition: deleteOneIncomeStatementRouteDefinition, - body: { - idIncomeStatement: props.incomeStatement.id, - idYear: props.incomeStatement.idYear, - }, - }) - - if (deleteResponse.ok === false) { - toast({ - title: "Erreur lors de la suppression de la ligne de compte de résultat", - variant: "error", - }) - return - } - - await invalidateData({ - routeDefinition: readAllIncomeStatementsRouteDefinition, - body: { - idYear: props.incomeStatement.idYear, - }, - }) - - toast({ - title: "Ligne de compte de résultat supprimée", - variant: "success", - }) - - applicationRouter.navigate({ - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/compte-de-résultat", - params: { - idOrganization: props.incomeStatement.idOrganization, - idYear: props.incomeStatement.idYear, - }, - }) - } - - return ( - - Cette action supprimera la ligne de compte de résultat et toutes les données associées. -
- Cette action est irréversible. - - } - submitButtonProps={{ - color: "danger", - text: "Supprimer la ligne de compte de résultat", - }} - onSubmit={onSubmit} - > - {props.children} -
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/incomeStatementLayout.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/incomeStatementLayout.tsx deleted file mode 100644 index c20a6212..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/incomeStatementLayout.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { readOneIncomeStatementRouteDefinition } from "@arrhes/application-metadata/routes" -import { ButtonOutlineContent, ButtonPlainContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconChevronLeft, IconDatabase, IconInfoCircle, IconPencil, IconTrash } from "@tabler/icons-react" -import { Outlet, useParams } from "@tanstack/react-router" -import { DataWrapper } from "../../../../../../components/layouts/dataWrapper.tsx" -import { Section } from "../../../../../../components/layouts/section/section.tsx" -import { Tab } from "../../../../../../components/layouts/tab/tab.tsx" -import { LinkButton } from "../../../../../../components/linkButton.tsx" -import { incomeStatementLayoutRoute } from "../../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/incomeStatementLayoutRoute.tsx" -import { DeleteOneIncomeStatement } from "./deleteOneIncomeStatement.tsx" -import { UpdateOneIncomeStatement } from "./updateOneIncomeStatement.tsx" - -export function IncomeStatementLayout() { - const params = useParams({ - from: incomeStatementLayoutRoute.id, - }) - - return ( - - - {(incomeStatement) => { - return ( - <> - -
- - } - text="Retour" - /> - -
-
- - } - text="Modifier" - /> - - - } - color="danger" - /> - -
-
- - , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/compte-de-résultat/$idIncomeStatement", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - idIncomeStatement: params.idIncomeStatement, - }, - }, - { - label: "Métadonnées", - icon: , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/compte-de-résultat/$idIncomeStatement/métadonnées", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - idIncomeStatement: params.idIncomeStatement, - }, - }, - ]} - /> - - - - ) - }} -
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/incomeStatementMetadataTab.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/incomeStatementMetadataTab.tsx deleted file mode 100644 index f73eae8a..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/incomeStatementMetadataTab.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { readOneIncomeStatementRouteDefinition } from "@arrhes/application-metadata/routes" -import { FormatDateTime, FormatText } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { useParams } from "@tanstack/react-router" -import { DataBlock } from "../../../../../../components/layouts/dataBlock/dataBlock.tsx" -import { DataWrapper } from "../../../../../../components/layouts/dataWrapper.tsx" -import { Section } from "../../../../../../components/layouts/section/section.tsx" -import { incomeStatementLayoutRoute } from "../../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/incomeStatementLayoutRoute.tsx" - -export function IncomeStatementMetadataTab() { - const params = useParams({ - from: incomeStatementLayoutRoute.id, - }) - - return ( - - {(incomeStatement) => { - return ( - - - - - - - - - - - - {incomeStatement.id} - - - - - ) - }} - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/incomeStatementPage.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/incomeStatementPage.tsx deleted file mode 100644 index f5469c89..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/incomeStatementPage.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { readOneIncomeStatementRouteDefinition } from "@arrhes/application-metadata/routes" -import { FormatText } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { useParams } from "@tanstack/react-router" -import { DataBlock } from "../../../../../../components/layouts/dataBlock/dataBlock.tsx" -import { DataWrapper } from "../../../../../../components/layouts/dataWrapper.tsx" -import { Section } from "../../../../../../components/layouts/section/section.tsx" -import { incomeStatementLayoutRoute } from "../../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/incomeStatementLayoutRoute.tsx" - -export function IncomeStatementPage() { - const params = useParams({ - from: incomeStatementLayoutRoute.id, - }) - - return ( - - {(incomeStatement) => { - return ( - - - - - - {incomeStatement.number.toString()} - - - {incomeStatement.label} - - - - - ) - }} - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/updateOneIncomeStatement.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/updateOneIncomeStatement.tsx deleted file mode 100644 index a4b76782..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/updateOneIncomeStatement.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import { - readAllIncomeStatementsRouteDefinition, - readOneIncomeStatementRouteDefinition, - updateOneIncomeStatementRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { InputText, toast } from "@arrhes/ui" -import { IconPlus } from "@tabler/icons-react" -import { type JSX, useState } from "react" -import { Fragment } from "react/jsx-runtime" -import type * as v from "valibot" -import { FormControl } from "../../../../../../components/forms/formControl.tsx" -import { FormError } from "../../../../../../components/forms/formError.tsx" -import { FormField } from "../../../../../../components/forms/formField.tsx" -import { FormItem } from "../../../../../../components/forms/formItem.tsx" -import { FormLabel } from "../../../../../../components/forms/formLabel.tsx" -import { FormRoot } from "../../../../../../components/forms/formRoot.tsx" -import { InputDataCombobox } from "../../../../../../components/inputDataCombobox.tsx" -import { Drawer } from "../../../../../../components/overlays/drawer/drawer.tsx" -import { getResponseBodyFromAPI } from "../../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../../utilities/invalidateData.ts" - -export function UpdateOneIncomeStatement(props: { - incomeStatement: v.InferOutput - children: JSX.Element -}) { - const [open, setOpen] = useState(false) - - return ( - - {props.children} - - - - , - text: "Modifier la ligne de compte de résultat", - }} - onSubmit={async (data) => { - const updateIncomeStatementResponse = await getResponseBodyFromAPI({ - routeDefinition: updateOneIncomeStatementRouteDefinition, - body: data, - }) - if (updateIncomeStatementResponse.ok === false) { - toast({ - title: "Impossible de modifier la ligne de compte de résultat", - variant: "error", - }) - return false - } - - toast({ - title: "Ligne de compte de résultat modifiée avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await Promise.all([ - invalidateData({ - routeDefinition: readAllIncomeStatementsRouteDefinition, - body: { - idYear: props.incomeStatement.idYear, - }, - }), - invalidateData({ - routeDefinition: readOneIncomeStatementRouteDefinition, - body: { - idIncomeStatement: props.incomeStatement.id, - idYear: props.incomeStatement.idYear, - }, - }), - ]) - - setOpen(false) - }} - > - {(form) => ( - - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - ({ - key: incomeStatement.id, - label: `(${incomeStatement.number}) ${incomeStatement.label}`, - })} - /> - - - - )} - /> - - )} - - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/CreateOneIncomeStatement.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/CreateOneIncomeStatement.tsx new file mode 100644 index 00000000..558e65f2 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/CreateOneIncomeStatement.tsx @@ -0,0 +1,175 @@ +import { + createOneIncomeStatementRouteDefinition, + readAllIncomeStatementsRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, InputText, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPlus } from "@tabler/icons-react" +import type { JSX } from "react" +import { Fragment } from "react/jsx-runtime" +import type * as v from "valibot" +import { FormControl } from "../../../../../components/forms/FormControl.tsx" +import { FormError } from "../../../../../components/forms/FormError.tsx" +import { FormField } from "../../../../../components/forms/FormField.tsx" +import { FormItem } from "../../../../../components/forms/FormItem.tsx" +import { FormLabel } from "../../../../../components/forms/FormLabel.tsx" +import { FormRoot } from "../../../../../components/forms/FormRoot.tsx" +import { InputDataCombobox } from "../../../../../components/InputDataCombobox.tsx" +import { useTabs } from "../../../../../contexts/tabs/useTabs.tsx" +import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../utilities/invalidateData.ts" + +export function CreateOneIncomeStatement(props: { + idOrganization: v.InferOutput["id"] + idYear: v.InferOutput["id"] + children: JSX.Element +}) { + const { openPanelTab, closeTab } = useTabs() + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/IncomeStatementItem.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/IncomeStatementItem.tsx new file mode 100644 index 00000000..e607f1ba --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/IncomeStatementItem.tsx @@ -0,0 +1,44 @@ +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { type ComponentProps, Fragment } from "react" +import type * as v from "valibot" +import { getIncomeStatementChildren } from "./getIncomeStatementChildren.tsx" +import { IncomeStatementRow } from "./IncomeStatementRow.tsx" + +export function IncomeStatementItem(props: { + idOrganization: v.InferOutput["id"] + idYear: v.InferOutput["id"] + incomeStatement: v.InferOutput + incomeStatementChildren: Array> + level: number + className?: ComponentProps<"div">["className"] +}) { + return ( + + + {props.incomeStatementChildren + .filter((incomeStatement) => incomeStatement.idIncomeStatementParent === props.incomeStatement.id) + .map((incomeStatement) => { + const children = getIncomeStatementChildren({ + incomeStatement: incomeStatement, + incomeStatements: props.incomeStatementChildren, + }) + + return ( + + ) + })} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/IncomeStatementRow.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/IncomeStatementRow.tsx new file mode 100644 index 00000000..83506a4e --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/IncomeStatementRow.tsx @@ -0,0 +1,69 @@ +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { css } from "@arrhes/ui/utilities/cn.js" +import type { ComponentProps } from "react" +import type * as v from "valibot" +import { ACCOUNT_ITEM_HEIGHT, INDENT_PER_LEVEL } from "../accounts/accountItem.tsx" +import { UpdateOneIncomeStatement } from "./$idIncomeStatement/UpdateOneIncomeStatement.tsx" + +export function IncomeStatementRow(props: { + idOrganization: v.InferOutput["id"] + idYear: v.InferOutput["id"] + incomeStatement: v.InferOutput + level: number + className?: ComponentProps<"div">["className"] +}) { + return ( + +
+ + {props.incomeStatement.number} + + + {props.incomeStatement.label} + +
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/IncomeStatementsPage.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/IncomeStatementsPage.tsx new file mode 100644 index 00000000..bf1c5c63 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/IncomeStatementsPage.tsx @@ -0,0 +1,109 @@ +import { ButtonPlainContent, InputDebounced, InputText } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconCalculator, IconPlus, IconReportMoney } from "@tabler/icons-react" +import { useParams } from "@tanstack/react-router" +import { useCallback, useState, useTransition } from "react" +import { Box } from "../../../../../components/layouts/Box.tsx" +import { Section } from "../../../../../components/layouts/section/section.tsx" +import { Tab } from "../../../../../components/layouts/tab/tab.tsx" +import { CreateOneIncomeStatement } from "./CreateOneIncomeStatement.tsx" +import { IncomeStatementsTable } from "./IncomeStatementsTable.tsx" + +export function IncomeStatementsPage({ + idOrganization: idOrganizationProp, + idYear: idYearProp, +}: { + idOrganization?: string + idYear?: string +} = {}) { + const params = useParams({ + strict: false, + }) as { + idOrganization?: string + idYear?: string + } + const idOrganization = idOrganizationProp ?? params.idOrganization ?? "" + const idYear = idYearProp ?? params.idYear ?? "" + const [globalFilter, setGlobalFilter] = useState("") + const [, startTransition] = useTransition() + + const handleFilterChange = useCallback((value: string | undefined) => { + startTransition(() => { + setGlobalFilter(value ?? "") + }) + }, []) + + return ( + + + , + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/compte-de-résultat", + params: { + idOrganization: idOrganization, + idYear: idYear, + }, + }, + { + label: "Calculs", + icon: , + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/compte-de-résultat/calculs", + params: { + idOrganization: idOrganization, + idYear: idYear, + }, + }, + ]} + /> + + +
+ + } + text="Ajouter une ligne de compte de résultat" + /> + +
+ + + + + + +
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/IncomeStatementsTable.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/IncomeStatementsTable.tsx new file mode 100644 index 00000000..efd036a6 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/IncomeStatementsTable.tsx @@ -0,0 +1,95 @@ +import { readAllIncomeStatementsRouteDefinition } from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconReportMoney } from "@tabler/icons-react" +import type * as v from "valibot" +import { DataWrapper } from "../../../../../components/layouts/DataWrapper.tsx" +import { EmptyState } from "../../../../../components/layouts/EmptyState.tsx" +import { getIncomeStatementChildren } from "./getIncomeStatementChildren.tsx" +import { IncomeStatementItem } from "./IncomeStatementItem.tsx" + +export function IncomeStatementsTable(props: { + idOrganization: v.InferOutput["id"] + idYear: v.InferOutput["id"] + globalFilter: string +}) { + const normalizedGlobalFilter = props.globalFilter.trim().toLowerCase() + + return ( + + {(incomeStatements) => { + const filteredIncomeStatements = incomeStatements + .filter((incomeStatement) => incomeStatement.idIncomeStatementParent === null) + .filter((incomeStatement) => { + if (normalizedGlobalFilter.length === 0) { + return true + } + + return `${incomeStatement.number} ${incomeStatement.label}` + .toLowerCase() + .includes(normalizedGlobalFilter) + }) + .sort((a, b) => Number(a.number) - Number(b.number)) + + return ( +
+
+ {filteredIncomeStatements.length === 0 && ( + } + title={ + props.globalFilter + ? "Aucune ligne trouvée" + : "Aucune ligne de compte de résultat" + } + subtitle={props.globalFilter ? undefined : "Ajoutez une ligne pour commencer"} + /> + )} + {filteredIncomeStatements.map((incomeStatement) => { + const incomeStatementChildren = getIncomeStatementChildren({ + incomeStatement: incomeStatement, + incomeStatements: incomeStatements, + }) + + return ( + + ) + })} +
+
+ ) + }} +
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/ComputationLayout.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/ComputationLayout.tsx new file mode 100644 index 00000000..143bda78 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/ComputationLayout.tsx @@ -0,0 +1,127 @@ +import { readOneComputationRouteDefinition } from "@arrhes/application-metadata/routes" +import { ButtonOutlineContent, ButtonPlainContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconChevronLeft, IconDatabase, IconInfoCircle, IconList, IconPencil, IconTrash } from "@tabler/icons-react" +import { Outlet, useParams } from "@tanstack/react-router" +import { LinkButton } from "../../../../../../../components/LinkButton.tsx" +import { DataWrapper } from "../../../../../../../components/layouts/DataWrapper.tsx" +import { Section } from "../../../../../../../components/layouts/section/section.tsx" +import { Tab } from "../../../../../../../components/layouts/tab/tab.tsx" + +import { DeleteOneComputation } from "./DeleteOneComputation.tsx" +import { UpdateOneComputation } from "./UpdateOneComputation.tsx" + +export function ComputationLayout() { + const params = useParams({ + strict: false, + }) as { + idYear: string + idComputation: string + idOrganization: string + } + + return ( + + + {(computation) => { + return ( + <> + +
+ + } + text="Retour" + /> + +
+
+ + } + text="Modifier" + /> + + + } + color="danger" + /> + +
+
+ + , + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/compte-de-résultat/calculs/$idComputation", + params: { + idOrganization: params.idOrganization, + idYear: params.idYear, + idComputation: params.idComputation, + }, + }, + { + label: "Postes", + icon: , + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/compte-de-résultat/calculs/$idComputation/postes", + params: { + idOrganization: params.idOrganization, + idYear: params.idYear, + idComputation: params.idComputation, + }, + }, + { + label: "Métadonnées", + icon: , + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/compte-de-résultat/calculs/$idComputation/métadonnées", + params: { + idOrganization: params.idOrganization, + idYear: params.idYear, + idComputation: params.idComputation, + }, + }, + ]} + /> + + + + ) + }} +
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/ComputationMetadataTab.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/ComputationMetadataTab.tsx new file mode 100644 index 00000000..5d370d69 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/ComputationMetadataTab.tsx @@ -0,0 +1,51 @@ +import { readOneComputationRouteDefinition } from "@arrhes/application-metadata/routes" +import { FormatDateTime, FormatText } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { useParams } from "@tanstack/react-router" +import { DataWrapper } from "../../../../../../../components/layouts/DataWrapper.tsx" +import { DataBlock } from "../../../../../../../components/layouts/dataBlock/dataBlock.tsx" +import { Section } from "../../../../../../../components/layouts/section/section.tsx" + +export function ComputationMetadataTab() { + const params = useParams({ + strict: false, + }) as { + idYear: string + idComputation: string + } + + return ( + + {(computation) => { + return ( + + + + + + + + + + + + {computation.id} + + + + + ) + }} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/ComputationPage.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/ComputationPage.tsx new file mode 100644 index 00000000..0a05cd5c --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/ComputationPage.tsx @@ -0,0 +1,48 @@ +import { readOneComputationRouteDefinition } from "@arrhes/application-metadata/routes" +import { FormatText } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { useParams } from "@tanstack/react-router" +import { DataWrapper } from "../../../../../../../components/layouts/DataWrapper.tsx" +import { DataBlock } from "../../../../../../../components/layouts/dataBlock/dataBlock.tsx" +import { Section } from "../../../../../../../components/layouts/section/section.tsx" + +export function ComputationPage() { + const params = useParams({ + strict: false, + }) as { + idYear: string + idComputation: string + } + + return ( + + {(computation) => { + return ( + + + + + + {computation.number} + + + {computation.label} + + + + + ) + }} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/ComputationPostesTab.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/ComputationPostesTab.tsx new file mode 100644 index 00000000..f3f809a9 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/ComputationPostesTab.tsx @@ -0,0 +1,53 @@ +import { readOneComputationRouteDefinition } from "@arrhes/application-metadata/routes" +import { ButtonPlainContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPlus } from "@tabler/icons-react" +import { useParams } from "@tanstack/react-router" +import { DataWrapper } from "../../../../../../../components/layouts/DataWrapper.tsx" +import { Section } from "../../../../../../../components/layouts/section/section.tsx" + +import { ComputationIncomeStatementsTable } from "./computationIncomeStatements/ComputationIncomeStatementsTable.tsx" +import { CreateOneComputationIncomeStatement } from "./computationIncomeStatements/CreateOneComputationIncomeStatement.tsx" + +export function ComputationPostesTab() { + const params = useParams({ + strict: false, + }) as { + idYear: string + idComputation: string + } + + return ( + + {(computation) => { + return ( + +
+ + } + text="Ajouter" + /> + +
+ +
+ ) + }} +
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/DeleteOneComputation.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/DeleteOneComputation.tsx new file mode 100644 index 00000000..4d2f5070 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/DeleteOneComputation.tsx @@ -0,0 +1,97 @@ +import { + deleteOneComputationRouteDefinition, + readAllComputationsRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, ButtonOutlineContent, ButtonPlainContent, Dialog, toast, useModalStore } from "@arrhes/ui" +import { type ComponentPropsWithRef, type ReactElement, useId } from "react" +import type * as v from "valibot" +import { applicationRouter } from "../../../../../../../routes/applicationRouter.tsx" +import { getResponseBodyFromAPI } from "../../../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../../../utilities/invalidateData.ts" + +export function DeleteOneComputation(props: { + computation: v.InferOutput + children: ReactElement> +}) { + const modalId = useId() + const { open: openModal, close: closeModal } = useModalStore() + + async function onSubmit() { + const deleteResponse = await getResponseBodyFromAPI({ + routeDefinition: deleteOneComputationRouteDefinition, + body: { + idComputation: props.computation.id, + idYear: props.computation.idYear, + }, + }) + + if (deleteResponse.ok === false) { + toast({ + title: "Erreur lors de la suppression de la ligne de calcul", + variant: "error", + }) + return + } + + await invalidateData({ + routeDefinition: readAllComputationsRouteDefinition, + body: { + idYear: props.computation.idYear, + }, + }) + + toast({ + title: "Ligne de calcul supprimée", + variant: "success", + }) + + applicationRouter.navigate({ + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/compte-de-résultat", + params: { + idOrganization: props.computation.idOrganization, + idYear: props.computation.idYear, + }, + }) + } + + return ( + + + + , + ) + } + > + {props.children} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/UpdateOneComputation.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/UpdateOneComputation.tsx new file mode 100644 index 00000000..742836f0 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/UpdateOneComputation.tsx @@ -0,0 +1,155 @@ +import { + readAllComputationsRouteDefinition, + readOneComputationRouteDefinition, + updateOneComputationRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, InputText, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPlus } from "@tabler/icons-react" +import type { JSX } from "react" +import { Fragment } from "react/jsx-runtime" +import type * as v from "valibot" +import { FormControl } from "../../../../../../../components/forms/FormControl.tsx" +import { FormError } from "../../../../../../../components/forms/FormError.tsx" +import { FormField } from "../../../../../../../components/forms/FormField.tsx" +import { FormItem } from "../../../../../../../components/forms/FormItem.tsx" +import { FormLabel } from "../../../../../../../components/forms/FormLabel.tsx" +import { FormRoot } from "../../../../../../../components/forms/FormRoot.tsx" +import { useTabs } from "../../../../../../../contexts/tabs/useTabs.tsx" +import { getResponseBodyFromAPI } from "../../../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../../../utilities/invalidateData.ts" + +export function UpdateOneComputation(props: { + computation: v.InferOutput + children: JSX.Element +}) { + const { openPanelTab, closeTab } = useTabs() + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/ComputationIncomeStatementLayout.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/ComputationIncomeStatementLayout.tsx new file mode 100644 index 00000000..e7b9b71d --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/ComputationIncomeStatementLayout.tsx @@ -0,0 +1,125 @@ +import { readOneComputationIncomeStatementRouteDefinition } from "@arrhes/application-metadata/routes" +import { ButtonOutlineContent, ButtonPlainContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconChevronLeft, IconDatabase, IconInfoCircle, IconPencil, IconTrash } from "@tabler/icons-react" +import { Outlet, useParams } from "@tanstack/react-router" +import { LinkButton } from "../../../../../../../../../components/LinkButton.tsx" +import { DataWrapper } from "../../../../../../../../../components/layouts/DataWrapper.tsx" +import { Section } from "../../../../../../../../../components/layouts/section/section.tsx" +import { Tab } from "../../../../../../../../../components/layouts/tab/tab.tsx" + +import { DeleteOneComputationIncomeStatement } from "./DeleteOneComputationIncomeStatement.tsx" +import { UpdateOneComputationIncomeStatement } from "./UpdateOneComputationIncomeStatement.tsx" + +export function ComputationIncomeStatementLayout() { + const params = useParams({ + strict: false, + }) as { + idYear: string + idComputationIncomeStatement: string + idOrganization: string + idComputation: string + } + + return ( + + + {(computationIncomeStatement) => { + return ( + <> + +
+ + } + text="Retour" + /> + +
+
+ + } + text="Modifier" + /> + + + } + color="danger" + /> + +
+
+ + , + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/compte-de-résultat/calculs/$idComputation/postes/$idComputationIncomeStatement", + params: { + idOrganization: params.idOrganization, + idYear: params.idYear, + idComputation: params.idComputation, + idComputationIncomeStatement: params.idComputationIncomeStatement, + }, + }, + { + label: "Métadonnées", + icon: , + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/compte-de-résultat/calculs/$idComputation/postes/$idComputationIncomeStatement/métadonnées", + params: { + idOrganization: params.idOrganization, + idYear: params.idYear, + idComputation: params.idComputation, + idComputationIncomeStatement: params.idComputationIncomeStatement, + }, + }, + ]} + /> + + + + ) + }} +
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/ComputationIncomeStatementMetadataTab.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/ComputationIncomeStatementMetadataTab.tsx new file mode 100644 index 00000000..9c177b8f --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/ComputationIncomeStatementMetadataTab.tsx @@ -0,0 +1,51 @@ +import { readOneComputationIncomeStatementRouteDefinition } from "@arrhes/application-metadata/routes" +import { FormatDateTime, FormatText } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { useParams } from "@tanstack/react-router" +import { DataWrapper } from "../../../../../../../../../components/layouts/DataWrapper.tsx" +import { DataBlock } from "../../../../../../../../../components/layouts/dataBlock/dataBlock.tsx" +import { Section } from "../../../../../../../../../components/layouts/section/section.tsx" + +export function ComputationIncomeStatementMetadataTab() { + const params = useParams({ + strict: false, + }) as { + idYear: string + idComputationIncomeStatement: string + } + + return ( + + {(computationIncomeStatement) => { + return ( + + + + + + + + + + + + {computationIncomeStatement.id} + + + + + ) + }} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/ComputationIncomeStatementPage.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/ComputationIncomeStatementPage.tsx new file mode 100644 index 00000000..59be6be9 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/ComputationIncomeStatementPage.tsx @@ -0,0 +1,67 @@ +import { + readOneComputationIncomeStatementRouteDefinition, + readOneComputationRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { Chip, FormatText } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { useParams } from "@tanstack/react-router" +import { DataWrapper } from "../../../../../../../../../components/layouts/DataWrapper.tsx" +import { DataBlock } from "../../../../../../../../../components/layouts/dataBlock/dataBlock.tsx" +import { Section } from "../../../../../../../../../components/layouts/section/section.tsx" + +export function ComputationIncomeStatementPage() { + const params = useParams({ + strict: false, + }) as { + idYear: string + idComputationIncomeStatement: string + } + + return ( + + {(computationIncomeStatement) => { + return ( + + + + + + + {(computation) => ( + {`${computation.number} - ${computation.label}`} + )} + + + + + + + + + ) + }} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/DeleteOneComputationIncomeStatement.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/DeleteOneComputationIncomeStatement.tsx new file mode 100644 index 00000000..810d6aba --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/DeleteOneComputationIncomeStatement.tsx @@ -0,0 +1,97 @@ +import { + deleteOneComputationIncomeStatementRouteDefinition, + readAllComputationIncomeStatementsRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, ButtonOutlineContent, ButtonPlainContent, Dialog, toast, useModalStore } from "@arrhes/ui" +import { type ComponentPropsWithRef, type ReactElement, useId } from "react" +import type * as v from "valibot" +import { applicationRouter } from "../../../../../../../../../routes/applicationRouter.tsx" +import { getResponseBodyFromAPI } from "../../../../../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../../../../../utilities/invalidateData.ts" + +export function DeleteOneComputationIncomeStatement(props: { + computationIncomeStatement: v.InferOutput + children: ReactElement> +}) { + const modalId = useId() + const { open: openModal, close: closeModal } = useModalStore() + + async function onSubmit() { + const deleteResponse = await getResponseBodyFromAPI({ + routeDefinition: deleteOneComputationIncomeStatementRouteDefinition, + body: { + idComputationIncomeStatement: props.computationIncomeStatement.id, + idYear: props.computationIncomeStatement.idYear, + }, + }) + + if (deleteResponse.ok === false) { + toast({ + title: "Erreur lors de la suppression de la ligne de compte de résultat", + variant: "error", + }) + return + } + + await invalidateData({ + routeDefinition: readAllComputationIncomeStatementsRouteDefinition, + body: { + idYear: props.computationIncomeStatement.idYear, + }, + }) + + toast({ + title: "Ligne de compte de résultat supprimée", + variant: "success", + }) + + applicationRouter.navigate({ + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/compte-de-résultat", + params: { + idOrganization: props.computationIncomeStatement.idOrganization, + idYear: props.computationIncomeStatement.idYear, + }, + }) + } + + return ( + + + + , + ) + } + > + {props.children} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/UpdateOneComputationIncomeStatement.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/UpdateOneComputationIncomeStatement.tsx new file mode 100644 index 00000000..ab0d0b14 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/UpdateOneComputationIncomeStatement.tsx @@ -0,0 +1,175 @@ +import { + readAllComputationIncomeStatementsRouteDefinition, + readAllIncomeStatementsRouteDefinition, + readOneComputationIncomeStatementRouteDefinition, + updateOneComputationIncomeStatementRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, InputToggle, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPlus } from "@tabler/icons-react" +import type { JSX } from "react" +import { Fragment } from "react/jsx-runtime" +import type * as v from "valibot" +import { FormControl } from "../../../../../../../../../components/forms/FormControl.tsx" +import { FormError } from "../../../../../../../../../components/forms/FormError.tsx" +import { FormField } from "../../../../../../../../../components/forms/FormField.tsx" +import { FormItem } from "../../../../../../../../../components/forms/FormItem.tsx" +import { FormLabel } from "../../../../../../../../../components/forms/FormLabel.tsx" +import { FormRoot } from "../../../../../../../../../components/forms/FormRoot.tsx" +import { InputDataCombobox } from "../../../../../../../../../components/InputDataCombobox.tsx" +import { useTabs } from "../../../../../../../../../contexts/tabs/useTabs.tsx" +import { getResponseBodyFromAPI } from "../../../../../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../../../../../utilities/invalidateData.ts" + +export function UpdateOneComputationIncomeStatement(props: { + computationIncomeStatement: v.InferOutput + children: JSX.Element +}) { + const { openPanelTab, closeTab } = useTabs() + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/computationIncomeStatementLayout.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/computationIncomeStatementLayout.tsx deleted file mode 100644 index 92e635f2..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/computationIncomeStatementLayout.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { readOneComputationIncomeStatementRouteDefinition } from "@arrhes/application-metadata/routes" -import { ButtonOutlineContent, ButtonPlainContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconChevronLeft, IconDatabase, IconInfoCircle, IconPencil, IconTrash } from "@tabler/icons-react" -import { Outlet, useParams } from "@tanstack/react-router" -import { DataWrapper } from "../../../../../../../../../components/layouts/dataWrapper.tsx" -import { Section } from "../../../../../../../../../components/layouts/section/section.tsx" -import { Tab } from "../../../../../../../../../components/layouts/tab/tab.tsx" -import { LinkButton } from "../../../../../../../../../components/linkButton.tsx" -import { computationIncomeStatementLayoutRoute } from "../../../../../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/$idComputationIncomeStatement/computationIncomeStatementLayoutRoute.tsx" -import { DeleteOneComputationIncomeStatement } from "./deleteOneComputationIncomeStatement.tsx" -import { UpdateOneComputationIncomeStatement } from "./updateOneComputationIncomeStatement.tsx" - -export function ComputationIncomeStatementLayout() { - const params = useParams({ - from: computationIncomeStatementLayoutRoute.id, - }) - - return ( - - - {(computationIncomeStatement) => { - return ( - <> - -
- - } - text="Retour" - /> - -
-
- - } - text="Modifier" - /> - - - } - color="danger" - /> - -
-
- - , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/compte-de-résultat/calculs/$idComputation/postes/$idComputationIncomeStatement", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - idComputation: params.idComputation, - idComputationIncomeStatement: params.idComputationIncomeStatement, - }, - }, - { - label: "Métadonnées", - icon: , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/compte-de-résultat/calculs/$idComputation/postes/$idComputationIncomeStatement/métadonnées", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - idComputation: params.idComputation, - idComputationIncomeStatement: params.idComputationIncomeStatement, - }, - }, - ]} - /> - - - - ) - }} -
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/computationIncomeStatementMetadataTab.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/computationIncomeStatementMetadataTab.tsx deleted file mode 100644 index 3030133a..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/computationIncomeStatementMetadataTab.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { readOneComputationIncomeStatementRouteDefinition } from "@arrhes/application-metadata/routes" -import { FormatDateTime, FormatText } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { useParams } from "@tanstack/react-router" -import { DataBlock } from "../../../../../../../../../components/layouts/dataBlock/dataBlock.tsx" -import { DataWrapper } from "../../../../../../../../../components/layouts/dataWrapper.tsx" -import { Section } from "../../../../../../../../../components/layouts/section/section.tsx" -import { computationIncomeStatementLayoutRoute } from "../../../../../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/$idComputationIncomeStatement/computationIncomeStatementLayoutRoute.tsx" - -export function ComputationIncomeStatementMetadataTab() { - const params = useParams({ - from: computationIncomeStatementLayoutRoute.id, - }) - - return ( - - {(computationIncomeStatement) => { - return ( - - - - - - - - - - - - {computationIncomeStatement.id} - - - - - ) - }} - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/computationIncomeStatementPage.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/computationIncomeStatementPage.tsx deleted file mode 100644 index 4d6c5033..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/computationIncomeStatementPage.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { - readOneComputationIncomeStatementRouteDefinition, - readOneComputationRouteDefinition, -} from "@arrhes/application-metadata/routes" -import { Chip, FormatText } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { useParams } from "@tanstack/react-router" -import { DataBlock } from "../../../../../../../../../components/layouts/dataBlock/dataBlock.tsx" -import { DataWrapper } from "../../../../../../../../../components/layouts/dataWrapper.tsx" -import { Section } from "../../../../../../../../../components/layouts/section/section.tsx" -import { computationIncomeStatementLayoutRoute } from "../../../../../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/$idComputationIncomeStatement/computationIncomeStatementLayoutRoute.tsx" - -export function ComputationIncomeStatementPage() { - const params = useParams({ - from: computationIncomeStatementLayoutRoute.id, - }) - - return ( - - {(computationIncomeStatement) => { - return ( - - - - - - - {(computation) => ( - {`${computation.number} - ${computation.label}`} - )} - - - - - - - - - ) - }} - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/deleteOneComputationIncomeStatement.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/deleteOneComputationIncomeStatement.tsx deleted file mode 100644 index b1cb1579..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/deleteOneComputationIncomeStatement.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { - deleteOneComputationIncomeStatementRouteDefinition, - readAllComputationIncomeStatementsRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { toast } from "@arrhes/ui" -import type { ComponentPropsWithRef, ReactElement } from "react" -import type * as v from "valibot" -import { ConfirmationModal } from "../../../../../../../../../components/overlays/dialog/confirmationModal.tsx" -import { applicationRouter } from "../../../../../../../../../routes/applicationRouter.tsx" -import { getResponseBodyFromAPI } from "../../../../../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../../../../../utilities/invalidateData.ts" - -export function DeleteOneComputationIncomeStatement(props: { - computationIncomeStatement: v.InferOutput - children: ReactElement> -}) { - async function onSubmit() { - const deleteResponse = await getResponseBodyFromAPI({ - routeDefinition: deleteOneComputationIncomeStatementRouteDefinition, - body: { - idComputationIncomeStatement: props.computationIncomeStatement.id, - idYear: props.computationIncomeStatement.idYear, - }, - }) - - if (deleteResponse.ok === false) { - toast({ - title: "Erreur lors de la suppression de la ligne de compte de résultat", - variant: "error", - }) - return - } - - await invalidateData({ - routeDefinition: readAllComputationIncomeStatementsRouteDefinition, - body: { - idYear: props.computationIncomeStatement.idYear, - }, - }) - - toast({ - title: "Ligne de compte de résultat supprimée", - variant: "success", - }) - - applicationRouter.navigate({ - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/compte-de-résultat", - params: { - idOrganization: props.computationIncomeStatement.idOrganization, - idYear: props.computationIncomeStatement.idYear, - }, - }) - } - - return ( - - Cette action supprimera la ligne de compte de résultat et toutes les données associées. -
- Cette action est irréversible. - - } - submitButtonProps={{ - color: "danger", - text: "Supprimer la ligne de compte de résultat", - }} - onSubmit={onSubmit} - > - {props.children} -
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/updateOneComputationIncomeStatement.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/updateOneComputationIncomeStatement.tsx deleted file mode 100644 index 4e08765c..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/updateOneComputationIncomeStatement.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import { - readAllComputationIncomeStatementsRouteDefinition, - readAllIncomeStatementsRouteDefinition, - readOneComputationIncomeStatementRouteDefinition, - updateOneComputationIncomeStatementRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { InputToggle, toast } from "@arrhes/ui" -import { IconPlus } from "@tabler/icons-react" -import { type JSX, useState } from "react" -import { Fragment } from "react/jsx-runtime" -import type * as v from "valibot" -import { FormControl } from "../../../../../../../../../components/forms/formControl.tsx" -import { FormError } from "../../../../../../../../../components/forms/formError.tsx" -import { FormField } from "../../../../../../../../../components/forms/formField.tsx" -import { FormItem } from "../../../../../../../../../components/forms/formItem.tsx" -import { FormLabel } from "../../../../../../../../../components/forms/formLabel.tsx" -import { FormRoot } from "../../../../../../../../../components/forms/formRoot.tsx" -import { InputDataCombobox } from "../../../../../../../../../components/inputDataCombobox.tsx" -import { Drawer } from "../../../../../../../../../components/overlays/drawer/drawer.tsx" -import { getResponseBodyFromAPI } from "../../../../../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../../../../../utilities/invalidateData.ts" - -export function UpdateOneComputationIncomeStatement(props: { - computationIncomeStatement: v.InferOutput - children: JSX.Element -}) { - const [open, setOpen] = useState(false) - - return ( - - {props.children} - - - - , - text: "Modifier le terme du calcul", - }} - onSubmit={async (data) => { - const updateComputationIncomeStatementResponse = await getResponseBodyFromAPI({ - routeDefinition: updateOneComputationIncomeStatementRouteDefinition, - body: data, - }) - if (updateComputationIncomeStatementResponse.ok === false) { - toast({ - title: "Impossible de modifier le terme du calcul", - variant: "error", - }) - return false - } - - toast({ - title: "Terme du calcul modifié avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await Promise.all([ - invalidateData({ - routeDefinition: readAllComputationIncomeStatementsRouteDefinition, - body: { - idYear: props.computationIncomeStatement.idYear, - }, - }), - invalidateData({ - routeDefinition: readOneComputationIncomeStatementRouteDefinition, - body: { - idComputationIncomeStatement: props.computationIncomeStatement.id, - idYear: props.computationIncomeStatement.idYear, - }, - }), - ]) - - setOpen(false) - }} - > - {(form) => ( - - ( - - - - ({ - key: incomeStatement.id, - label: `${incomeStatement.number} - ${incomeStatement.label}`, - })} - /> - - - - )} - /> - ( - - - - - - - - )} - /> - - )} - - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/ComputationIncomeStatementsTable.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/ComputationIncomeStatementsTable.tsx new file mode 100644 index 00000000..665c7962 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/ComputationIncomeStatementsTable.tsx @@ -0,0 +1,92 @@ +import { + readAllComputationIncomeStatementsRouteDefinition, + readOneIncomeStatementRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { ButtonGhostContent, Chip, FormatDateTime, FormatText } from "@arrhes/ui" +import { IconEye } from "@tabler/icons-react" +import type * as v from "valibot" +import { LinkButton } from "../../../../../../../../components/LinkButton.tsx" +import { DataTable } from "../../../../../../../../components/layouts/DataTable.tsx" +import { DataWrapper } from "../../../../../../../../components/layouts/DataWrapper.tsx" + +export function ComputationIncomeStatementsTable(props: { + computation: v.InferOutput +}) { + return ( + + {(computationIncomeStatements) => { + return ( + ( + + } + text={undefined} + /> + + ), + enableSorting: false, + enableGlobalFilter: false, + }, + { + accessorKey: "idStatement", + header: "Poste du compte de résultat", + cell: ({ row }) => ( + + {(incomeStatement) => ( + + {`${incomeStatement.number} - ${incomeStatement.label}`} + + )} + + ), + filterFn: "includesString", + }, + { + accessorKey: "operation", + header: "Opération", + cell: ({ row }) => ( + + ), + filterFn: "includesString", + }, + { + accessorKey: "createdAt", + header: "Ajouté le", + cell: ({ row }) => , + filterFn: "includesString", + }, + ]} + /> + ) + }} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/CreateOneComputationIncomeStatement.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/CreateOneComputationIncomeStatement.tsx new file mode 100644 index 00000000..95cdd103 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/CreateOneComputationIncomeStatement.tsx @@ -0,0 +1,165 @@ +import { + createOneComputationIncomeStatementRouteDefinition, + readAllComputationIncomeStatementsRouteDefinition, + readAllIncomeStatementsRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, InputToggle, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPlus } from "@tabler/icons-react" +import type { JSX } from "react" +import { Fragment } from "react/jsx-runtime" +import type * as v from "valibot" +import { FormControl } from "../../../../../../../../components/forms/FormControl.tsx" +import { FormError } from "../../../../../../../../components/forms/FormError.tsx" +import { FormField } from "../../../../../../../../components/forms/FormField.tsx" +import { FormItem } from "../../../../../../../../components/forms/FormItem.tsx" +import { FormLabel } from "../../../../../../../../components/forms/FormLabel.tsx" +import { FormRoot } from "../../../../../../../../components/forms/FormRoot.tsx" +import { InputDataCombobox } from "../../../../../../../../components/InputDataCombobox.tsx" +import { useTabs } from "../../../../../../../../contexts/tabs/useTabs.tsx" +import { getResponseBodyFromAPI } from "../../../../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../../../../utilities/invalidateData.ts" + +export function CreateOneComputationIncomeStatement(props: { + computation: v.InferOutput + children: JSX.Element +}) { + const { openPanelTab, closeTab } = useTabs() + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/computationIncomeStatementTable.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/computationIncomeStatementTable.tsx deleted file mode 100644 index 58fe6999..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/computationIncomeStatementTable.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { - readAllComputationIncomeStatementsRouteDefinition, - readOneIncomeStatementRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { ButtonGhostContent, Chip, FormatDateTime, FormatText } from "@arrhes/ui" -import { IconEye } from "@tabler/icons-react" -import type * as v from "valibot" -import { DataTable } from "../../../../../../../../components/layouts/dataTable.tsx" -import { DataWrapper } from "../../../../../../../../components/layouts/dataWrapper.tsx" -import { LinkButton } from "../../../../../../../../components/linkButton.tsx" - -export function ComputationIncomeStatementsTable(props: { - computation: v.InferOutput -}) { - return ( - - {(computationIncomeStatements) => { - return ( - ( - - } - text={undefined} - /> - - ), - enableSorting: false, - enableGlobalFilter: false, - }, - { - accessorKey: "idStatement", - header: "Poste du compte de résultat", - cell: ({ row }) => ( - - {(incomeStatement) => ( - - {`${incomeStatement.number} - ${incomeStatement.label}`} - - )} - - ), - filterFn: "includesString", - }, - { - accessorKey: "operation", - header: "Opération", - cell: ({ row }) => ( - - ), - filterFn: "includesString", - }, - { - accessorKey: "createdAt", - header: "Ajouté le", - cell: ({ row }) => , - filterFn: "includesString", - }, - ]} - /> - ) - }} - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/createOneComputationIncomeStatement.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/createOneComputationIncomeStatement.tsx deleted file mode 100644 index df4d71d7..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/createOneComputationIncomeStatement.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { - createOneComputationIncomeStatementRouteDefinition, - readAllComputationIncomeStatementsRouteDefinition, - readAllIncomeStatementsRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { InputToggle, toast } from "@arrhes/ui" -import { IconPlus } from "@tabler/icons-react" -import { type JSX, useState } from "react" -import { Fragment } from "react/jsx-runtime" -import type * as v from "valibot" -import { FormControl } from "../../../../../../../../components/forms/formControl.tsx" -import { FormError } from "../../../../../../../../components/forms/formError.tsx" -import { FormField } from "../../../../../../../../components/forms/formField.tsx" -import { FormItem } from "../../../../../../../../components/forms/formItem.tsx" -import { FormLabel } from "../../../../../../../../components/forms/formLabel.tsx" -import { FormRoot } from "../../../../../../../../components/forms/formRoot.tsx" -import { InputDataCombobox } from "../../../../../../../../components/inputDataCombobox.tsx" -import { Drawer } from "../../../../../../../../components/overlays/drawer/drawer.tsx" -import { getResponseBodyFromAPI } from "../../../../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../../../../utilities/invalidateData.ts" - -export function CreateOneComputationIncomeStatement(props: { - computation: v.InferOutput - children: JSX.Element -}) { - const [open, setOpen] = useState(false) - - return ( - - {props.children} - - - - , - text: "Ajouter le terme du calcul", - }} - onSubmit={async (data) => { - const createComputationIncomeStatementResponse = await getResponseBodyFromAPI({ - routeDefinition: createOneComputationIncomeStatementRouteDefinition, - body: data, - }) - if (createComputationIncomeStatementResponse.ok === false) { - toast({ - title: "Impossible d'ajouter le terme du calcul", - variant: "error", - }) - return false - } - - toast({ - title: "Terme du calcul ajouté avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await invalidateData({ - routeDefinition: readAllComputationIncomeStatementsRouteDefinition, - body: { - idYear: props.computation.idYear, - }, - }) - - setOpen(false) - }} - > - {(form) => ( - - ( - - - - ({ - key: incomeStatement.id, - label: `${incomeStatement.number} - ${incomeStatement.label}`, - })} - /> - - - - )} - /> - ( - - - - - - - - )} - /> - - )} - - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationLayout.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationLayout.tsx deleted file mode 100644 index 1b6025e2..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationLayout.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { readOneComputationRouteDefinition } from "@arrhes/application-metadata/routes" -import { ButtonOutlineContent, ButtonPlainContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconChevronLeft, IconDatabase, IconInfoCircle, IconList, IconPencil, IconTrash } from "@tabler/icons-react" -import { Outlet, useParams } from "@tanstack/react-router" -import { DataWrapper } from "../../../../../../../components/layouts/dataWrapper.tsx" -import { Section } from "../../../../../../../components/layouts/section/section.tsx" -import { Tab } from "../../../../../../../components/layouts/tab/tab.tsx" -import { LinkButton } from "../../../../../../../components/linkButton.tsx" -import { computationLayoutRoute } from "../../../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationLayoutRoute.tsx" -import { DeleteOneComputation } from "./deleteOneComputation.tsx" -import { UpdateOneComputation } from "./updateOneComputation.tsx" - -export function ComputationLayout() { - const params = useParams({ - from: computationLayoutRoute.id, - }) - - return ( - - - {(computation) => { - return ( - <> - -
- - } - text="Retour" - /> - -
-
- - } - text="Modifier" - /> - - - } - color="danger" - /> - -
-
- - , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/compte-de-résultat/calculs/$idComputation", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - idComputation: params.idComputation, - }, - }, - { - label: "Postes", - icon: , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/compte-de-résultat/calculs/$idComputation/postes", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - idComputation: params.idComputation, - }, - }, - { - label: "Métadonnées", - icon: , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/compte-de-résultat/calculs/$idComputation/métadonnées", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - idComputation: params.idComputation, - }, - }, - ]} - /> - - - - ) - }} -
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationMetadataTab.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationMetadataTab.tsx deleted file mode 100644 index 7ee9628c..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationMetadataTab.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { readOneComputationRouteDefinition } from "@arrhes/application-metadata/routes" -import { FormatDateTime, FormatText } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { useParams } from "@tanstack/react-router" -import { DataBlock } from "../../../../../../../components/layouts/dataBlock/dataBlock.tsx" -import { DataWrapper } from "../../../../../../../components/layouts/dataWrapper.tsx" -import { Section } from "../../../../../../../components/layouts/section/section.tsx" -import { computationLayoutRoute } from "../../../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationLayoutRoute.tsx" - -export function ComputationMetadataTab() { - const params = useParams({ - from: computationLayoutRoute.id, - }) - - return ( - - {(computation) => { - return ( - - - - - - - - - - - - {computation.id} - - - - - ) - }} - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationPage.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationPage.tsx deleted file mode 100644 index 386e470b..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationPage.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { readOneComputationRouteDefinition } from "@arrhes/application-metadata/routes" -import { FormatText } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { useParams } from "@tanstack/react-router" -import { DataBlock } from "../../../../../../../components/layouts/dataBlock/dataBlock.tsx" -import { DataWrapper } from "../../../../../../../components/layouts/dataWrapper.tsx" -import { Section } from "../../../../../../../components/layouts/section/section.tsx" -import { computationLayoutRoute } from "../../../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationLayoutRoute.tsx" - -export function ComputationPage() { - const params = useParams({ - from: computationLayoutRoute.id, - }) - - return ( - - {(computation) => { - return ( - - - - - - {computation.number} - - - {computation.label} - - - - - ) - }} - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationPostesTab.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationPostesTab.tsx deleted file mode 100644 index b34577bd..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationPostesTab.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { readOneComputationRouteDefinition } from "@arrhes/application-metadata/routes" -import { ButtonPlainContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconPlus } from "@tabler/icons-react" -import { useParams } from "@tanstack/react-router" -import { DataWrapper } from "../../../../../../../components/layouts/dataWrapper.tsx" -import { Section } from "../../../../../../../components/layouts/section/section.tsx" -import { computationLayoutRoute } from "../../../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationLayoutRoute.tsx" -import { ComputationIncomeStatementsTable } from "./computationIncomeStatements/computationIncomeStatementTable.tsx" -import { CreateOneComputationIncomeStatement } from "./computationIncomeStatements/createOneComputationIncomeStatement.tsx" - -export function ComputationPostesTab() { - const params = useParams({ - from: computationLayoutRoute.id, - }) - - return ( - - {(computation) => { - return ( - -
- - } - text="Ajouter" - /> - -
- -
- ) - }} -
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/deleteOneComputation.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/deleteOneComputation.tsx deleted file mode 100644 index e95244ea..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/deleteOneComputation.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { - deleteOneComputationRouteDefinition, - readAllComputationsRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { toast } from "@arrhes/ui" -import type { ComponentPropsWithRef, ReactElement } from "react" -import type * as v from "valibot" -import { ConfirmationModal } from "../../../../../../../components/overlays/dialog/confirmationModal.tsx" -import { applicationRouter } from "../../../../../../../routes/applicationRouter.tsx" -import { getResponseBodyFromAPI } from "../../../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../../../utilities/invalidateData.ts" - -export function DeleteOneComputation(props: { - computation: v.InferOutput - children: ReactElement> -}) { - async function onSubmit() { - const deleteResponse = await getResponseBodyFromAPI({ - routeDefinition: deleteOneComputationRouteDefinition, - body: { - idComputation: props.computation.id, - idYear: props.computation.idYear, - }, - }) - - if (deleteResponse.ok === false) { - toast({ - title: "Erreur lors de la suppression de la ligne de calcul", - variant: "error", - }) - return - } - - await invalidateData({ - routeDefinition: readAllComputationsRouteDefinition, - body: { - idYear: props.computation.idYear, - }, - }) - - toast({ - title: "Ligne de calcul supprimée", - variant: "success", - }) - - applicationRouter.navigate({ - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/compte-de-résultat", - params: { - idOrganization: props.computation.idOrganization, - idYear: props.computation.idYear, - }, - }) - } - - return ( - - Cette action supprimera la ligne de calcul et toutes les données associées. -
- Cette action est irréversible. - - } - submitButtonProps={{ - color: "danger", - text: "Supprimer la ligne de calcul", - }} - onSubmit={onSubmit} - > - {props.children} -
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/updateOneComputation.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/updateOneComputation.tsx deleted file mode 100644 index a67a457a..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/updateOneComputation.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { - readAllComputationsRouteDefinition, - readOneComputationRouteDefinition, - updateOneComputationRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { InputText, toast } from "@arrhes/ui" -import { IconPlus } from "@tabler/icons-react" -import { type JSX, useState } from "react" -import { Fragment } from "react/jsx-runtime" -import type * as v from "valibot" -import { FormControl } from "../../../../../../../components/forms/formControl.tsx" -import { FormError } from "../../../../../../../components/forms/formError.tsx" -import { FormField } from "../../../../../../../components/forms/formField.tsx" -import { FormItem } from "../../../../../../../components/forms/formItem.tsx" -import { FormLabel } from "../../../../../../../components/forms/formLabel.tsx" -import { FormRoot } from "../../../../../../../components/forms/formRoot.tsx" -import { Drawer } from "../../../../../../../components/overlays/drawer/drawer.tsx" -import { getResponseBodyFromAPI } from "../../../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../../../utilities/invalidateData.ts" - -export function UpdateOneComputation(props: { - computation: v.InferOutput - children: JSX.Element -}) { - const [open, setOpen] = useState(false) - - return ( - - {props.children} - - - - , - text: "Modifier la ligne de calcul", - }} - onSubmit={async (data) => { - const updateComputationResponse = await getResponseBodyFromAPI({ - routeDefinition: updateOneComputationRouteDefinition, - body: data, - }) - if (updateComputationResponse.ok === false) { - toast({ - title: "Impossible de modifier la ligne de calcul", - variant: "error", - }) - return false - } - - toast({ - title: "Ligne de calcul modifiée avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await Promise.all([ - invalidateData({ - routeDefinition: readAllComputationsRouteDefinition, - body: { - idYear: props.computation.idYear, - }, - }), - invalidateData({ - routeDefinition: readOneComputationRouteDefinition, - body: { - idComputation: props.computation.id, - idYear: props.computation.idYear, - }, - }), - ]) - - setOpen(false) - }} - > - {(form) => ( - - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - - )} - - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/ComputationsPage.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/ComputationsPage.tsx new file mode 100644 index 00000000..6e8cead1 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/ComputationsPage.tsx @@ -0,0 +1,80 @@ +import { ButtonPlainContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconCalculator, IconPlus, IconReportMoney } from "@tabler/icons-react" +import { useParams } from "@tanstack/react-router" +import { Box } from "../../../../../../components/layouts/Box.tsx" +import { Section } from "../../../../../../components/layouts/section/section.tsx" +import { Tab } from "../../../../../../components/layouts/tab/tab.tsx" + +import { ComputationsTable } from "./ComputationsTable.tsx" +import { CreateOneComputation } from "./CreateOneComputation.tsx" + +export function ComputationsPage() { + const params = useParams({ + strict: false, + }) as { + idYear: string + idOrganization: string + } + + return ( + + + , + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/compte-de-résultat", + params: { + idOrganization: params.idOrganization, + idYear: params.idYear, + }, + }, + { + label: "Calculs", + icon: , + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/compte-de-résultat/calculs", + params: { + idOrganization: params.idOrganization, + idYear: params.idYear, + }, + }, + ]} + /> + + +
+ + } + text="Ajouter une ligne de calcul" + /> + +
+ + + +
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/ComputationsTable.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/ComputationsTable.tsx new file mode 100644 index 00000000..ce0abba3 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/ComputationsTable.tsx @@ -0,0 +1,104 @@ +import { readAllComputationsRouteDefinition } from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconCalculator } from "@tabler/icons-react" +import type * as v from "valibot" +import { LinkButton } from "../../../../../../components/LinkButton.tsx" +import { DataWrapper } from "../../../../../../components/layouts/DataWrapper.tsx" +import { EmptyState } from "../../../../../../components/layouts/EmptyState.tsx" + +export function ComputationsTable(props: { + idOrganization: v.InferOutput["id"] + idYear: v.InferOutput["id"] +}) { + return ( + + {(computations) => { + if (computations.length === 0) { + return ( + } + title="Aucune ligne de calcul" + subtitle="Ajoutez une ligne de calcul pour commencer" + /> + ) + } + return ( +
+ {computations.map((computation) => ( + +
+ + {computation.number} + + + {computation.label} + +
+
+ ))} +
+ ) + }} +
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/CreateOneComputation.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/CreateOneComputation.tsx new file mode 100644 index 00000000..8de86bc8 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/CreateOneComputation.tsx @@ -0,0 +1,145 @@ +import { + createOneComputationRouteDefinition, + readAllComputationsRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, InputText, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPlus } from "@tabler/icons-react" +import type { JSX } from "react" +import { Fragment } from "react/jsx-runtime" +import type * as v from "valibot" +import { FormControl } from "../../../../../../components/forms/FormControl.tsx" +import { FormError } from "../../../../../../components/forms/FormError.tsx" +import { FormField } from "../../../../../../components/forms/FormField.tsx" +import { FormItem } from "../../../../../../components/forms/FormItem.tsx" +import { FormLabel } from "../../../../../../components/forms/FormLabel.tsx" +import { FormRoot } from "../../../../../../components/forms/FormRoot.tsx" +import { useTabs } from "../../../../../../contexts/tabs/useTabs.tsx" +import { getResponseBodyFromAPI } from "../../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../../utilities/invalidateData.ts" + +export function CreateOneComputation(props: { + idOrganization: v.InferOutput["id"] + idYear: v.InferOutput["id"] + children: JSX.Element +}) { + const { openPanelTab, closeTab } = useTabs() + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/computationsPage.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/computationsPage.tsx deleted file mode 100644 index deb912b6..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/computationsPage.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { ButtonPlainContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconCalculator, IconPlus, IconReportMoney } from "@tabler/icons-react" -import { useParams } from "@tanstack/react-router" -import { Box } from "../../../../../../components/layouts/box.tsx" -import { Section } from "../../../../../../components/layouts/section/section.tsx" -import { Tab } from "../../../../../../components/layouts/tab/tab.tsx" -import { incomeStatementsLayoutRoute } from "../../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/incomeStatementsLayoutRoute.tsx" -import { ComputationsTable } from "./computationsTable.tsx" -import { CreateOneComputation } from "./createOneComputation.tsx" - -export function ComputationsPage() { - const params = useParams({ - from: incomeStatementsLayoutRoute.id, - }) - - return ( - - - , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/compte-de-résultat", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - }, - }, - { - label: "Calculs", - icon: , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/compte-de-résultat/calculs", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - }, - }, - ]} - /> - - -
- - } - text="Ajouter une ligne de calcul" - /> - -
- - - -
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/computationsTable.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/computationsTable.tsx deleted file mode 100644 index e5ef855a..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/computationsTable.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { readAllComputationsRouteDefinition } from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconCalculator } from "@tabler/icons-react" -import type * as v from "valibot" -import { DataWrapper } from "../../../../../../components/layouts/dataWrapper.tsx" -import { EmptyState } from "../../../../../../components/layouts/emptyState.tsx" -import { LinkButton } from "../../../../../../components/linkButton.tsx" - -export function ComputationsTable(props: { - idOrganization: v.InferOutput["id"] - idYear: v.InferOutput["id"] -}) { - return ( - - {(computations) => { - if (computations.length === 0) { - return ( - } - title="Aucune ligne de calcul" - subtitle="Ajoutez une ligne de calcul pour commencer" - /> - ) - } - return ( -
- {computations.map((computation) => ( - -
- - {computation.number} - - - {computation.label} - -
-
- ))} -
- ) - }} -
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/createOneComputation.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/createOneComputation.tsx deleted file mode 100644 index dfafe27f..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/computations/createOneComputation.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { - createOneComputationRouteDefinition, - readAllComputationsRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { InputText, toast } from "@arrhes/ui" -import { IconPlus } from "@tabler/icons-react" -import { type JSX, useState } from "react" -import { Fragment } from "react/jsx-runtime" -import type * as v from "valibot" -import { FormControl } from "../../../../../../components/forms/formControl.tsx" -import { FormError } from "../../../../../../components/forms/formError.tsx" -import { FormField } from "../../../../../../components/forms/formField.tsx" -import { FormItem } from "../../../../../../components/forms/formItem.tsx" -import { FormLabel } from "../../../../../../components/forms/formLabel.tsx" -import { FormRoot } from "../../../../../../components/forms/formRoot.tsx" -import { Drawer } from "../../../../../../components/overlays/drawer/drawer.tsx" -import { getResponseBodyFromAPI } from "../../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../../utilities/invalidateData.ts" - -export function CreateOneComputation(props: { - idOrganization: v.InferOutput["id"] - idYear: v.InferOutput["id"] - children: JSX.Element -}) { - const [open, setOpen] = useState(false) - - return ( - - {props.children} - - - - , - text: "Ajouter la ligne de calcul", - }} - onSubmit={async (data) => { - const createComputationResponse = await getResponseBodyFromAPI({ - routeDefinition: createOneComputationRouteDefinition, - body: data, - }) - if (createComputationResponse.ok === false) { - toast({ - title: "Impossible d'ajouter la ligne de calcul", - variant: "error", - }) - return false - } - - toast({ - title: "Ligne de calcul ajouté avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await invalidateData({ - routeDefinition: readAllComputationsRouteDefinition, - body: { - idYear: props.idYear, - }, - }) - - setOpen(false) - }} - > - {(form) => ( - - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - - )} - - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/createOneIncomeStatement.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/createOneIncomeStatement.tsx deleted file mode 100644 index 1c941156..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/createOneIncomeStatement.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import { - createOneIncomeStatementRouteDefinition, - readAllIncomeStatementsRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { InputText, toast } from "@arrhes/ui" -import { IconPlus } from "@tabler/icons-react" -import { type JSX, useState } from "react" -import { Fragment } from "react/jsx-runtime" -import type * as v from "valibot" -import { FormControl } from "../../../../../components/forms/formControl.tsx" -import { FormError } from "../../../../../components/forms/formError.tsx" -import { FormField } from "../../../../../components/forms/formField.tsx" -import { FormItem } from "../../../../../components/forms/formItem.tsx" -import { FormLabel } from "../../../../../components/forms/formLabel.tsx" -import { FormRoot } from "../../../../../components/forms/formRoot.tsx" -import { InputDataCombobox } from "../../../../../components/inputDataCombobox.tsx" -import { Drawer } from "../../../../../components/overlays/drawer/drawer.tsx" -import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../utilities/invalidateData.ts" - -export function CreateOneIncomeStatement(props: { - idOrganization: v.InferOutput["id"] - idYear: v.InferOutput["id"] - children: JSX.Element -}) { - const [open, setOpen] = useState(false) - - return ( - - {props.children} - - - - , - text: "Ajouter la ligne de compte de résultat", - }} - onSubmit={async (data) => { - const createIncomeStatementResponse = await getResponseBodyFromAPI({ - routeDefinition: createOneIncomeStatementRouteDefinition, - body: data, - }) - if (createIncomeStatementResponse.ok === false) { - toast({ - title: "Impossible d'ajouter la ligne de compte de résultat", - variant: "error", - }) - return false - } - - toast({ - title: "Ligne de compte de résultat ajouté avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await invalidateData({ - routeDefinition: readAllIncomeStatementsRouteDefinition, - body: { - idYear: props.idYear, - }, - }) - - setOpen(false) - }} - > - {(form) => ( - - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - ({ - key: incomeStatement.id, - label: `(${incomeStatement.number}) ${incomeStatement.label}`, - })} - /> - - - - )} - /> - - )} - - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/incomeStatementItem.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/incomeStatementItem.tsx deleted file mode 100644 index 62d07e43..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/incomeStatementItem.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { type ComponentProps, Fragment } from "react" -import type * as v from "valibot" -import { getIncomeStatementChildren } from "./getIncomeStatementChildren.tsx" -import { IncomeStatementRow } from "./incomeStatementRow.tsx" - -export function IncomeStatementItem(props: { - idOrganization: v.InferOutput["id"] - idYear: v.InferOutput["id"] - incomeStatement: v.InferOutput - incomeStatementChildren: Array> - level: number - className?: ComponentProps<"div">["className"] -}) { - return ( - - - {props.incomeStatementChildren - .filter((incomeStatement) => incomeStatement.idIncomeStatementParent === props.incomeStatement.id) - .map((incomeStatement) => { - const children = getIncomeStatementChildren({ - incomeStatement: incomeStatement, - incomeStatements: props.incomeStatementChildren, - }) - - return ( - - ) - })} - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/incomeStatementRow.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/incomeStatementRow.tsx deleted file mode 100644 index b67a95d2..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/incomeStatementRow.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { css } from "@arrhes/ui/utilities/cn.js" -import type { ComponentProps } from "react" -import type * as v from "valibot" -import { LinkButton } from "../../../../../components/linkButton.tsx" -import { ACCOUNT_ITEM_HEIGHT, INDENT_PER_LEVEL } from "../accounts/accountItem.tsx" - -export function IncomeStatementRow(props: { - idOrganization: v.InferOutput["id"] - idYear: v.InferOutput["id"] - incomeStatement: v.InferOutput - level: number - className?: ComponentProps<"div">["className"] -}) { - return ( - -
- - {props.incomeStatement.number} - - - {props.incomeStatement.label} - -
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/incomeStatementsPage.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/incomeStatementsPage.tsx deleted file mode 100644 index fb6196d0..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/incomeStatementsPage.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { ButtonPlainContent, InputDebounced, InputText } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconCalculator, IconPlus, IconReportMoney } from "@tabler/icons-react" -import { useParams } from "@tanstack/react-router" -import { useCallback, useState, useTransition } from "react" -import { Box } from "../../../../../components/layouts/box.tsx" -import { Section } from "../../../../../components/layouts/section/section.tsx" -import { Tab } from "../../../../../components/layouts/tab/tab.tsx" -import { incomeStatementsLayoutRoute } from "../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/incomeStatementsLayoutRoute.tsx" -import { CreateOneIncomeStatement } from "./createOneIncomeStatement.tsx" -import { IncomeStatementsTable } from "./incomeStatementsTable.tsx" - -export function IncomeStatementsPage() { - const params = useParams({ - from: incomeStatementsLayoutRoute.id, - }) - const [globalFilter, setGlobalFilter] = useState("") - const [, startTransition] = useTransition() - - const handleFilterChange = useCallback((value: string | undefined) => { - startTransition(() => { - setGlobalFilter(value ?? "") - }) - }, []) - - return ( - - - , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/compte-de-résultat", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - }, - }, - { - label: "Calculs", - icon: , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/compte-de-résultat/calculs", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - }, - }, - ]} - /> - - -
- - } - text="Ajouter une ligne de compte de résultat" - /> - -
- - - - - - -
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/incomeStatementsTable.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/incomeStatementsTable.tsx deleted file mode 100644 index 7bb46afe..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/incomeStatements/incomeStatementsTable.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { readAllIncomeStatementsRouteDefinition } from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconReportMoney } from "@tabler/icons-react" -import type * as v from "valibot" -import { DataWrapper } from "../../../../../components/layouts/dataWrapper.tsx" -import { EmptyState } from "../../../../../components/layouts/emptyState.tsx" -import { getIncomeStatementChildren } from "./getIncomeStatementChildren.tsx" -import { IncomeStatementItem } from "./incomeStatementItem.tsx" - -export function IncomeStatementsTable(props: { - idOrganization: v.InferOutput["id"] - idYear: v.InferOutput["id"] - globalFilter: string -}) { - const normalizedGlobalFilter = props.globalFilter.trim().toLowerCase() - - return ( - - {(incomeStatements) => { - const filteredIncomeStatements = incomeStatements - .filter((incomeStatement) => incomeStatement.idIncomeStatementParent === null) - .filter((incomeStatement) => { - if (normalizedGlobalFilter.length === 0) { - return true - } - - return `${incomeStatement.number} ${incomeStatement.label}` - .toLowerCase() - .includes(normalizedGlobalFilter) - }) - .sort((a, b) => Number(a.number) - Number(b.number)) - - return ( -
-
- {filteredIncomeStatements.length === 0 && ( - } - title={ - props.globalFilter - ? "Aucune ligne trouvée" - : "Aucune ligne de compte de résultat" - } - subtitle={props.globalFilter ? undefined : "Ajoutez une ligne pour commencer"} - /> - )} - {filteredIncomeStatements.map((incomeStatement) => { - const incomeStatementChildren = getIncomeStatementChildren({ - incomeStatement: incomeStatement, - incomeStatements: incomeStatements, - }) - - return ( - - ) - })} -
-
- ) - }} -
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/journals/$idJournal/DeleteOneJournal.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/journals/$idJournal/DeleteOneJournal.tsx new file mode 100644 index 00000000..c692758a --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/journals/$idJournal/DeleteOneJournal.tsx @@ -0,0 +1,94 @@ +import { deleteOneJournalRouteDefinition, readAllJournalsRouteDefinition } from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, ButtonOutlineContent, ButtonPlainContent, Dialog, toast, useModalStore } from "@arrhes/ui" +import { type ComponentPropsWithRef, type ReactElement, useId } from "react" +import type * as v from "valibot" +import { applicationRouter } from "../../../../../../routes/applicationRouter.tsx" +import { getResponseBodyFromAPI } from "../../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../../utilities/invalidateData.ts" + +export function DeleteOneJournal(props: { + journal: v.InferOutput + children: ReactElement> +}) { + const modalId = useId() + const { open: openModal, close: closeModal } = useModalStore() + + async function onSubmit() { + const deleteResponse = await getResponseBodyFromAPI({ + routeDefinition: deleteOneJournalRouteDefinition, + body: { + idJournal: props.journal.id, + idYear: props.journal.idYear, + }, + }) + + if (deleteResponse.ok === false) { + toast({ + title: "Erreur lors de la suppression du journal", + variant: "error", + }) + return + } + + await invalidateData({ + routeDefinition: readAllJournalsRouteDefinition, + body: { + idYear: props.journal.idYear, + }, + }) + + toast({ + title: "Journal supprimé", + variant: "success", + }) + + applicationRouter.navigate({ + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/journaux", + params: { + idOrganization: props.journal.idOrganization, + idYear: props.journal.idYear, + }, + }) + } + + return ( + + + + , + ) + } + > + {props.children} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/journals/$idJournal/JournalLayout.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/journals/$idJournal/JournalLayout.tsx new file mode 100644 index 00000000..50b180c1 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/journals/$idJournal/JournalLayout.tsx @@ -0,0 +1,118 @@ +import { readOneJournalRouteDefinition } from "@arrhes/application-metadata/routes" +import { ButtonOutlineContent, ButtonPlainContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconChevronLeft, IconDatabase, IconInfoCircle, IconPencil, IconTrash } from "@tabler/icons-react" +import { Outlet, useParams } from "@tanstack/react-router" +import { LinkButton } from "../../../../../../components/LinkButton.tsx" +import { DataWrapper } from "../../../../../../components/layouts/DataWrapper.tsx" +import { Section } from "../../../../../../components/layouts/section/section.tsx" +import { Tab } from "../../../../../../components/layouts/tab/tab.tsx" + +import { DeleteOneJournal } from "./DeleteOneJournal.tsx" +import { UpdateOneJournal } from "./UpdateOneJournal.tsx" + +export function JournalLayout() { + const params = useParams({ + strict: false, + }) as { + idYear: string + idJournal: string + idOrganization: string + } + + return ( + + + {(journal) => { + return ( + <> + +
+ + } + text="Retour" + /> + +
+
+ + } + text="Modifier" + /> + + + } + title="Supprimer" + color="danger" + /> + +
+
+ + , + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/journaux/$idJournal", + params: { + idOrganization: params.idOrganization, + idYear: params.idYear, + idJournal: params.idJournal, + }, + }, + { + label: "Métadonnées", + icon: , + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/journaux/$idJournal/métadonnées", + params: { + idOrganization: params.idOrganization, + idYear: params.idYear, + idJournal: params.idJournal, + }, + }, + ]} + /> + + + + ) + }} +
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/journals/$idJournal/JournalMetadataTab.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/journals/$idJournal/JournalMetadataTab.tsx new file mode 100644 index 00000000..66146b55 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/journals/$idJournal/JournalMetadataTab.tsx @@ -0,0 +1,51 @@ +import { readOneJournalRouteDefinition } from "@arrhes/application-metadata/routes" +import { FormatDateTime, FormatText } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { useParams } from "@tanstack/react-router" +import { DataWrapper } from "../../../../../../components/layouts/DataWrapper.tsx" +import { DataBlock } from "../../../../../../components/layouts/dataBlock/dataBlock.tsx" +import { Section } from "../../../../../../components/layouts/section/section.tsx" + +export function JournalMetadataTab() { + const params = useParams({ + strict: false, + }) as { + idYear: string + idJournal: string + } + + return ( + + {(journal) => { + return ( + + + + + + + + + + + + {journal.id} + + + + + ) + }} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/journals/$idJournal/JournalPage.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/journals/$idJournal/JournalPage.tsx new file mode 100644 index 00000000..c1cf469a --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/journals/$idJournal/JournalPage.tsx @@ -0,0 +1,48 @@ +import { readOneJournalRouteDefinition } from "@arrhes/application-metadata/routes" +import { FormatText } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { useParams } from "@tanstack/react-router" +import { DataWrapper } from "../../../../../../components/layouts/DataWrapper.tsx" +import { DataBlock } from "../../../../../../components/layouts/dataBlock/dataBlock.tsx" +import { Section } from "../../../../../../components/layouts/section/section.tsx" + +export function JournalPage() { + const params = useParams({ + strict: false, + }) as { + idYear: string + idJournal: string + } + + return ( + + {(journal) => { + return ( + + + + + + {journal.code} + + + {journal.label} + + + + + ) + }} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/journals/$idJournal/UpdateOneJournal.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/journals/$idJournal/UpdateOneJournal.tsx new file mode 100644 index 00000000..4d701ac7 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/journals/$idJournal/UpdateOneJournal.tsx @@ -0,0 +1,156 @@ +import { + readAllJournalsRouteDefinition, + readOneJournalRouteDefinition, + updateOneJournalRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, InputText, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPlus } from "@tabler/icons-react" +import type { JSX } from "react" +import { Fragment } from "react/jsx-runtime" +import type * as v from "valibot" +import { FormControl } from "../../../../../../components/forms/FormControl.tsx" +import { FormError } from "../../../../../../components/forms/FormError.tsx" +import { FormField } from "../../../../../../components/forms/FormField.tsx" +import { FormItem } from "../../../../../../components/forms/FormItem.tsx" +import { FormLabel } from "../../../../../../components/forms/FormLabel.tsx" +import { FormRoot } from "../../../../../../components/forms/FormRoot.tsx" +import { useTabs } from "../../../../../../contexts/tabs/useTabs.tsx" +import { getResponseBodyFromAPI } from "../../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../../utilities/invalidateData.ts" + +export function UpdateOneJournal(props: { + journal: v.InferOutput + children: JSX.Element +}) { + const { openPanelTab, closeTab } = useTabs() + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/journals/$idJournal/deleteOneJournal.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/journals/$idJournal/deleteOneJournal.tsx deleted file mode 100644 index a4d03994..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/journals/$idJournal/deleteOneJournal.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { deleteOneJournalRouteDefinition, readAllJournalsRouteDefinition } from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { toast } from "@arrhes/ui" -import type { ComponentPropsWithRef, ReactElement } from "react" -import type * as v from "valibot" -import { ConfirmationModal } from "../../../../../../components/overlays/dialog/confirmationModal.tsx" -import { applicationRouter } from "../../../../../../routes/applicationRouter.tsx" -import { getResponseBodyFromAPI } from "../../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../../utilities/invalidateData.ts" - -export function DeleteOneJournal(props: { - journal: v.InferOutput - children: ReactElement> -}) { - async function onSubmit() { - const deleteResponse = await getResponseBodyFromAPI({ - routeDefinition: deleteOneJournalRouteDefinition, - body: { - idJournal: props.journal.id, - idYear: props.journal.idYear, - }, - }) - - if (deleteResponse.ok === false) { - toast({ - title: "Erreur lors de la suppression du journal", - variant: "error", - }) - return - } - - await invalidateData({ - routeDefinition: readAllJournalsRouteDefinition, - body: { - idYear: props.journal.idYear, - }, - }) - - toast({ - title: "Journal supprimé", - variant: "success", - }) - - applicationRouter.navigate({ - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/journaux", - params: { - idOrganization: props.journal.idOrganization, - idYear: props.journal.idYear, - }, - }) - } - - return ( - - Cette action supprimera le journal et toutes les données associées. -
- Cette action est irréversible. - - } - submitButtonProps={{ - color: "danger", - text: "Supprimer le journal", - }} - onSubmit={onSubmit} - > - {props.children} -
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/journals/$idJournal/journalLayout.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/journals/$idJournal/journalLayout.tsx deleted file mode 100644 index 07dfe2fc..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/journals/$idJournal/journalLayout.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { readOneJournalRouteDefinition } from "@arrhes/application-metadata/routes" -import { ButtonOutlineContent, ButtonPlainContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconChevronLeft, IconDatabase, IconInfoCircle, IconPencil, IconTrash } from "@tabler/icons-react" -import { Outlet, useParams } from "@tanstack/react-router" -import { DataWrapper } from "../../../../../../components/layouts/dataWrapper.tsx" -import { Section } from "../../../../../../components/layouts/section/section.tsx" -import { Tab } from "../../../../../../components/layouts/tab/tab.tsx" -import { LinkButton } from "../../../../../../components/linkButton.tsx" -import { journalLayoutRoute } from "../../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/journalLayoutRoute.tsx" -import { DeleteOneJournal } from "./deleteOneJournal.tsx" -import { UpdateOneJournal } from "./updateOneJournal.tsx" - -export function JournalLayout() { - const params = useParams({ - from: journalLayoutRoute.id, - }) - - return ( - - - {(journal) => { - return ( - <> - -
- - } - text="Retour" - /> - -
-
- - } - text="Modifier" - /> - - - } - title="Supprimer" - color="danger" - /> - -
-
- - , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/journaux/$idJournal", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - idJournal: params.idJournal, - }, - }, - { - label: "Métadonnées", - icon: , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/journaux/$idJournal/métadonnées", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - idJournal: params.idJournal, - }, - }, - ]} - /> - - - - ) - }} -
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/journals/$idJournal/journalMetadataTab.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/journals/$idJournal/journalMetadataTab.tsx deleted file mode 100644 index 9ba1d079..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/journals/$idJournal/journalMetadataTab.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { readOneJournalRouteDefinition } from "@arrhes/application-metadata/routes" -import { FormatDateTime, FormatText } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { useParams } from "@tanstack/react-router" -import { DataBlock } from "../../../../../../components/layouts/dataBlock/dataBlock.tsx" -import { DataWrapper } from "../../../../../../components/layouts/dataWrapper.tsx" -import { Section } from "../../../../../../components/layouts/section/section.tsx" -import { journalLayoutRoute } from "../../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/journalLayoutRoute.tsx" - -export function JournalMetadataTab() { - const params = useParams({ - from: journalLayoutRoute.id, - }) - - return ( - - {(journal) => { - return ( - - - - - - - - - - - - {journal.id} - - - - - ) - }} - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/journals/$idJournal/journalPage.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/journals/$idJournal/journalPage.tsx deleted file mode 100644 index aa7fea66..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/journals/$idJournal/journalPage.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { readOneJournalRouteDefinition } from "@arrhes/application-metadata/routes" -import { FormatText } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { useParams } from "@tanstack/react-router" -import { DataBlock } from "../../../../../../components/layouts/dataBlock/dataBlock.tsx" -import { DataWrapper } from "../../../../../../components/layouts/dataWrapper.tsx" -import { Section } from "../../../../../../components/layouts/section/section.tsx" -import { journalLayoutRoute } from "../../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/journalLayoutRoute.tsx" - -export function JournalPage() { - const params = useParams({ - from: journalLayoutRoute.id, - }) - - return ( - - {(journal) => { - return ( - - - - - - {journal.code} - - - {journal.label} - - - - - ) - }} - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/journals/$idJournal/updateOneJournal.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/journals/$idJournal/updateOneJournal.tsx deleted file mode 100644 index 43b5dd3a..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/journals/$idJournal/updateOneJournal.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { - readAllJournalsRouteDefinition, - readOneJournalRouteDefinition, - updateOneJournalRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { InputText, toast } from "@arrhes/ui" -import { IconPlus } from "@tabler/icons-react" -import { type JSX, useState } from "react" -import { Fragment } from "react/jsx-runtime" -import type * as v from "valibot" -import { FormControl } from "../../../../../../components/forms/formControl.tsx" -import { FormError } from "../../../../../../components/forms/formError.tsx" -import { FormField } from "../../../../../../components/forms/formField.tsx" -import { FormItem } from "../../../../../../components/forms/formItem.tsx" -import { FormLabel } from "../../../../../../components/forms/formLabel.tsx" -import { FormRoot } from "../../../../../../components/forms/formRoot.tsx" -import { Drawer } from "../../../../../../components/overlays/drawer/drawer.tsx" -import { getResponseBodyFromAPI } from "../../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../../utilities/invalidateData.ts" - -export function UpdateOneJournal(props: { - journal: v.InferOutput - children: JSX.Element -}) { - const [open, setOpen] = useState(false) - - return ( - - {props.children} - - - - , - text: "Modifier le journal", - }} - onSubmit={async (data) => { - const updateJournalResponse = await getResponseBodyFromAPI({ - routeDefinition: updateOneJournalRouteDefinition, - body: data, - }) - if (updateJournalResponse.ok === false) { - toast({ - title: "Impossible de modifier le journal", - variant: "error", - }) - return false - } - - toast({ - title: "Journal modifié avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await Promise.all([ - invalidateData({ - routeDefinition: readAllJournalsRouteDefinition, - body: { - idYear: props.journal.idYear, - }, - }), - invalidateData({ - routeDefinition: readOneJournalRouteDefinition, - body: { - idJournal: props.journal.id, - idYear: props.journal.idYear, - }, - }), - ]) - - setOpen(false) - }} - > - {(form) => ( - - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - - )} - - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/journals/CreateOneJournal.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/journals/CreateOneJournal.tsx new file mode 100644 index 00000000..8020cc48 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/journals/CreateOneJournal.tsx @@ -0,0 +1,142 @@ +import { createOneJournalRouteDefinition, readAllJournalsRouteDefinition } from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, InputText, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPlus } from "@tabler/icons-react" +import type { JSX } from "react" +import { Fragment } from "react/jsx-runtime" +import type * as v from "valibot" +import { FormControl } from "../../../../../components/forms/FormControl.tsx" +import { FormError } from "../../../../../components/forms/FormError.tsx" +import { FormField } from "../../../../../components/forms/FormField.tsx" +import { FormItem } from "../../../../../components/forms/FormItem.tsx" +import { FormLabel } from "../../../../../components/forms/FormLabel.tsx" +import { FormRoot } from "../../../../../components/forms/FormRoot.tsx" +import { useTabs } from "../../../../../contexts/tabs/useTabs.tsx" +import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../utilities/invalidateData.ts" + +export function CreateOneJournal(props: { + idOrganization: v.InferOutput["id"] + idYear: v.InferOutput["id"] + children: JSX.Element +}) { + const { openPanelTab, closeTab } = useTabs() + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/journals/JournalListTableRow.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/journals/JournalListTableRow.tsx new file mode 100644 index 00000000..041fef62 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/journals/JournalListTableRow.tsx @@ -0,0 +1,86 @@ +import type { schemas } from "@arrhes/application-metadata/schemas" +import { Chip, formatDate, LinkContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import type * as v from "valibot" +import { ListTable } from "../../../../../components/layouts/listTable/listTable.tsx" +import { UpdateOneJournal } from "./$idJournal/UpdateOneJournal.tsx" + +export function JournalListTableRow(props: { journal: v.InferOutput }) { + const createdAt = formatDate(props.journal.createdAt) + + return ( + +
+
+
+ + + {props.journal.label ?? undefined} + + +
+
+ +
+
+
+ {`Ajouté le ${createdAt}`} +
+
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/journals/journalSelect.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/journals/JournalSelect.tsx similarity index 100% rename from packages/website/src/features/dashboard/$idYear/yearSettings/journals/journalSelect.tsx rename to packages/website/src/features/dashboard/$idYear/yearSettings/journals/JournalSelect.tsx diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/journals/JournalsListTable.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/journals/JournalsListTable.tsx new file mode 100644 index 00000000..41745445 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/journals/JournalsListTable.tsx @@ -0,0 +1,44 @@ +import { readAllJournalsRouteDefinition } from "@arrhes/application-metadata/routes" +import type { schemas } from "@arrhes/application-metadata/schemas" +import { IconBook } from "@tabler/icons-react" +import type * as v from "valibot" +import { DataWrapper } from "../../../../../components/layouts/DataWrapper.tsx" +import { EmptyState } from "../../../../../components/layouts/EmptyState.tsx" +import { ListTable } from "../../../../../components/layouts/listTable/listTable.tsx" +import { JournalListTableRow } from "./JournalListTableRow.tsx" + +export function JournalsListTable(props: { + idOrganization: v.InferOutput["id"] + idYear: v.InferOutput["id"] +}) { + return ( + + + {(journals) => { + const sortedJournals = journals.sort((a, b) => a.createdAt.localeCompare(b.createdAt)) + + if (sortedJournals.length === 0) { + return ( + } + title="Aucun journal" + subtitle="Créez un journal pour commencer" + /> + ) + } + return sortedJournals.map((journal) => ( + + )) + }} + + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/journals/JournalsPage.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/journals/JournalsPage.tsx new file mode 100644 index 00000000..d77b6c91 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/journals/JournalsPage.tsx @@ -0,0 +1,59 @@ +import { ButtonPlainContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPlus } from "@tabler/icons-react" +import { useParams } from "@tanstack/react-router" +import { Page } from "../../../../../components/layouts/page/page.tsx" +import { Section } from "../../../../../components/layouts/section/section.tsx" +import { CreateOneJournal } from "./CreateOneJournal.tsx" +import { JournalsListTable } from "./JournalsListTable.tsx" + +export function JournalsPage({ + idOrganization: idOrganizationProp, + idYear: idYearProp, +}: { + idOrganization?: string + idYear?: string +} = {}) { + const params = useParams({ + strict: false, + }) as { + idOrganization?: string + idYear?: string + } + const idOrganization = idOrganizationProp ?? params.idOrganization ?? "" + const idYear = idYearProp ?? params.idYear ?? "" + + return ( + + + + +
+ + } + text="Ajouter un journal" + /> + +
+ +
+
+
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/journals/createOneJournal.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/journals/createOneJournal.tsx deleted file mode 100644 index 6aeecce4..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/journals/createOneJournal.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { createOneJournalRouteDefinition, readAllJournalsRouteDefinition } from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { InputText, toast } from "@arrhes/ui" -import { IconPlus } from "@tabler/icons-react" -import { type JSX, useState } from "react" -import { Fragment } from "react/jsx-runtime" -import type * as v from "valibot" -import { FormControl } from "../../../../../components/forms/formControl.tsx" -import { FormError } from "../../../../../components/forms/formError.tsx" -import { FormField } from "../../../../../components/forms/formField.tsx" -import { FormItem } from "../../../../../components/forms/formItem.tsx" -import { FormLabel } from "../../../../../components/forms/formLabel.tsx" -import { FormRoot } from "../../../../../components/forms/formRoot.tsx" -import { Drawer } from "../../../../../components/overlays/drawer/drawer.tsx" -import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../utilities/invalidateData.ts" - -export function CreateOneJournal(props: { - idOrganization: v.InferOutput["id"] - idYear: v.InferOutput["id"] - children: JSX.Element -}) { - const [open, setOpen] = useState(false) - - return ( - - {props.children} - - - - , - text: "Ajouter le journal", - }} - onSubmit={async (data) => { - const createJournalResponse = await getResponseBodyFromAPI({ - routeDefinition: createOneJournalRouteDefinition, - body: data, - }) - if (createJournalResponse.ok === false) { - toast({ - title: "Impossible d'ajouter le journal", - variant: "error", - }) - return false - } - - toast({ - title: "Journal ajouté avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await invalidateData({ - routeDefinition: readAllJournalsRouteDefinition, - body: { - idYear: props.idYear, - }, - }) - - setOpen(false) - }} - > - {(form) => ( - - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - - )} - - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/journals/journalListTableRow.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/journals/journalListTableRow.tsx deleted file mode 100644 index 622fe58a..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/journals/journalListTableRow.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import type { schemas } from "@arrhes/application-metadata/schemas" -import { Chip, formatDate, LinkContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import type * as v from "valibot" -import { ListTable } from "../../../../../components/layouts/listTable/listTable.tsx" -import { LinkButton } from "../../../../../components/linkButton.tsx" - -export function JournalListTableRow(props: { journal: v.InferOutput }) { - const createdAt = formatDate(props.journal.createdAt) - - return ( - -
-
-
- - - {props.journal.label ?? undefined} - - -
-
- -
-
-
- {`Ajouté le ${createdAt}`} -
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/journals/journalsListTable.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/journals/journalsListTable.tsx deleted file mode 100644 index 9418c8ed..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/journals/journalsListTable.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { readAllJournalsRouteDefinition } from "@arrhes/application-metadata/routes" -import type { schemas } from "@arrhes/application-metadata/schemas" -import { IconBook } from "@tabler/icons-react" -import type * as v from "valibot" -import { DataWrapper } from "../../../../../components/layouts/dataWrapper.tsx" -import { EmptyState } from "../../../../../components/layouts/emptyState.tsx" -import { ListTable } from "../../../../../components/layouts/listTable/listTable.tsx" -import { JournalListTableRow } from "./journalListTableRow.tsx" - -export function JournalsListTable(props: { - idOrganization: v.InferOutput["id"] - idYear: v.InferOutput["id"] -}) { - return ( - - - {(journals) => { - const sortedJournals = journals.sort((a, b) => a.createdAt.localeCompare(b.createdAt)) - - if (sortedJournals.length === 0) { - return ( - } - title="Aucun journal" - subtitle="Créez un journal pour commencer" - /> - ) - } - return sortedJournals.map((journal) => ( - - )) - }} - - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/journals/journalsPage.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/journals/journalsPage.tsx deleted file mode 100644 index b086cb70..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/journals/journalsPage.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { ButtonPlainContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconPlus } from "@tabler/icons-react" -import { useParams } from "@tanstack/react-router" -import { Page } from "../../../../../components/layouts/page/page.tsx" -import { Section } from "../../../../../components/layouts/section/section.tsx" -import { journalsRoute } from "../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/journals/journalsRoute.tsx" -import { CreateOneJournal } from "./createOneJournal.tsx" -import { JournalsListTable } from "./journalsListTable.tsx" - -export function JournalsPage() { - const params = useParams({ - from: journalsRoute.id, - }) - - return ( - - - - -
- - } - text="Ajouter un journal" - /> - -
- -
-
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/tags/$idTag/DeleteOneTag.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/tags/$idTag/DeleteOneTag.tsx new file mode 100644 index 00000000..43909ba2 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/tags/$idTag/DeleteOneTag.tsx @@ -0,0 +1,94 @@ +import { deleteOneTagRouteDefinition, readAllTagsRouteDefinition } from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, ButtonOutlineContent, ButtonPlainContent, Dialog, toast, useModalStore } from "@arrhes/ui" +import { type ComponentPropsWithRef, type ReactElement, useId } from "react" +import type * as v from "valibot" +import { applicationRouter } from "../../../../../../routes/applicationRouter.tsx" +import { getResponseBodyFromAPI } from "../../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../../utilities/invalidateData.ts" + +export function DeleteOneTag(props: { + tag: v.InferOutput + children: ReactElement> +}) { + const modalId = useId() + const { open: openModal, close: closeModal } = useModalStore() + + async function onSubmit() { + const deleteResponse = await getResponseBodyFromAPI({ + routeDefinition: deleteOneTagRouteDefinition, + body: { + idTag: props.tag.id, + idYear: props.tag.idYear, + }, + }) + + if (deleteResponse.ok === false) { + toast({ + title: "Erreur lors de la suppression de la catégorie", + variant: "error", + }) + return + } + + await invalidateData({ + routeDefinition: readAllTagsRouteDefinition, + body: { + idYear: props.tag.idYear, + }, + }) + + toast({ + title: "Catégorie supprimée", + variant: "success", + }) + + applicationRouter.navigate({ + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/catégories", + params: { + idOrganization: props.tag.idOrganization, + idYear: props.tag.idYear, + }, + }) + } + + return ( + + + + , + ) + } + > + {props.children} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/tags/$idTag/TagLayout.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/tags/$idTag/TagLayout.tsx new file mode 100644 index 00000000..7ae9ea1d --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/tags/$idTag/TagLayout.tsx @@ -0,0 +1,118 @@ +import { readOneTagRouteDefinition } from "@arrhes/application-metadata/routes" +import { ButtonOutlineContent, ButtonPlainContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconChevronLeft, IconDatabase, IconInfoCircle, IconPencil, IconTrash } from "@tabler/icons-react" +import { Outlet, useParams } from "@tanstack/react-router" +import { LinkButton } from "../../../../../../components/LinkButton.tsx" +import { DataWrapper } from "../../../../../../components/layouts/DataWrapper.tsx" +import { Section } from "../../../../../../components/layouts/section/section.tsx" +import { Tab } from "../../../../../../components/layouts/tab/tab.tsx" + +import { DeleteOneTag } from "./DeleteOneTag.tsx" +import { UpdateOneTag } from "./UpdateOneTag.tsx" + +export function TagLayout() { + const params = useParams({ + strict: false, + }) as { + idYear: string + idTag: string + idOrganization: string + } + + return ( + + + {(tag) => { + return ( + <> + +
+ + } + text="Retour" + /> + +
+
+ + } + text="Modifier" + /> + + + } + title="Supprimer" + color="danger" + /> + +
+
+ + , + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/catégories/$idTag", + params: { + idOrganization: params.idOrganization, + idYear: params.idYear, + idTag: params.idTag, + }, + }, + { + label: "Métadonnées", + icon: , + to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/catégories/$idTag/métadonnées", + params: { + idOrganization: params.idOrganization, + idYear: params.idYear, + idTag: params.idTag, + }, + }, + ]} + /> + + + + ) + }} +
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/tags/$idTag/TagMetadataTab.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/tags/$idTag/TagMetadataTab.tsx new file mode 100644 index 00000000..a3152748 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/tags/$idTag/TagMetadataTab.tsx @@ -0,0 +1,51 @@ +import { readOneTagRouteDefinition } from "@arrhes/application-metadata/routes" +import { FormatDateTime, FormatText } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { useParams } from "@tanstack/react-router" +import { DataWrapper } from "../../../../../../components/layouts/DataWrapper.tsx" +import { DataBlock } from "../../../../../../components/layouts/dataBlock/dataBlock.tsx" +import { Section } from "../../../../../../components/layouts/section/section.tsx" + +export function TagMetadataTab() { + const params = useParams({ + strict: false, + }) as { + idYear: string + idTag: string + } + + return ( + + {(tag) => { + return ( + + + + + + + + + + + + {tag.id} + + + + + ) + }} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/tags/$idTag/TagPage.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/tags/$idTag/TagPage.tsx new file mode 100644 index 00000000..1421f806 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/tags/$idTag/TagPage.tsx @@ -0,0 +1,45 @@ +import { readOneTagRouteDefinition } from "@arrhes/application-metadata/routes" +import { FormatText } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { useParams } from "@tanstack/react-router" +import { DataWrapper } from "../../../../../../components/layouts/DataWrapper.tsx" +import { DataBlock } from "../../../../../../components/layouts/dataBlock/dataBlock.tsx" +import { Section } from "../../../../../../components/layouts/section/section.tsx" + +export function TagPage() { + const params = useParams({ + strict: false, + }) as { + idYear: string + idTag: string + } + + return ( + + {(tag) => { + return ( + + + + + + {tag.label} + + + + + ) + }} + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/tags/$idTag/UpdateOneTag.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/tags/$idTag/UpdateOneTag.tsx new file mode 100644 index 00000000..b93b6b86 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/tags/$idTag/UpdateOneTag.tsx @@ -0,0 +1,132 @@ +import { + readAllTagsRouteDefinition, + readOneTagRouteDefinition, + updateOneTagRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, InputText, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPlus } from "@tabler/icons-react" +import type { JSX } from "react" +import { Fragment } from "react/jsx-runtime" +import type * as v from "valibot" +import { FormControl } from "../../../../../../components/forms/FormControl.tsx" +import { FormError } from "../../../../../../components/forms/FormError.tsx" +import { FormField } from "../../../../../../components/forms/FormField.tsx" +import { FormItem } from "../../../../../../components/forms/FormItem.tsx" +import { FormLabel } from "../../../../../../components/forms/FormLabel.tsx" +import { FormRoot } from "../../../../../../components/forms/FormRoot.tsx" +import { useTabs } from "../../../../../../contexts/tabs/useTabs.tsx" +import { getResponseBodyFromAPI } from "../../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../../utilities/invalidateData.ts" + +export function UpdateOneTag(props: { tag: v.InferOutput; children: JSX.Element }) { + const { openPanelTab, closeTab } = useTabs() + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/tags/$idTag/deleteOneTag.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/tags/$idTag/deleteOneTag.tsx deleted file mode 100644 index f31113b0..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/tags/$idTag/deleteOneTag.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { deleteOneTagRouteDefinition, readAllTagsRouteDefinition } from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { toast } from "@arrhes/ui" -import type { ComponentPropsWithRef, ReactElement } from "react" -import type * as v from "valibot" -import { ConfirmationModal } from "../../../../../../components/overlays/dialog/confirmationModal.tsx" -import { applicationRouter } from "../../../../../../routes/applicationRouter.tsx" -import { getResponseBodyFromAPI } from "../../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../../utilities/invalidateData.ts" - -export function DeleteOneTag(props: { - tag: v.InferOutput - children: ReactElement> -}) { - async function onSubmit() { - const deleteResponse = await getResponseBodyFromAPI({ - routeDefinition: deleteOneTagRouteDefinition, - body: { - idTag: props.tag.id, - idYear: props.tag.idYear, - }, - }) - - if (deleteResponse.ok === false) { - toast({ - title: "Erreur lors de la suppression de la catégorie", - variant: "error", - }) - return - } - - await invalidateData({ - routeDefinition: readAllTagsRouteDefinition, - body: { - idYear: props.tag.idYear, - }, - }) - - toast({ - title: "Catégorie supprimée", - variant: "success", - }) - - applicationRouter.navigate({ - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/catégories", - params: { - idOrganization: props.tag.idOrganization, - idYear: props.tag.idYear, - }, - }) - } - - return ( - - Cette action supprimera la catégorie et toutes ses mentions associées. -
- Cette action est irréversible. - - } - submitButtonProps={{ - color: "danger", - text: "Supprimer la catégorie", - }} - onSubmit={onSubmit} - > - {props.children} -
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/tags/$idTag/tagLayout.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/tags/$idTag/tagLayout.tsx deleted file mode 100644 index 7a16b0f3..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/tags/$idTag/tagLayout.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { readOneTagRouteDefinition } from "@arrhes/application-metadata/routes" -import { ButtonOutlineContent, ButtonPlainContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconChevronLeft, IconDatabase, IconInfoCircle, IconPencil, IconTrash } from "@tabler/icons-react" -import { Outlet, useParams } from "@tanstack/react-router" -import { DataWrapper } from "../../../../../../components/layouts/dataWrapper.tsx" -import { Section } from "../../../../../../components/layouts/section/section.tsx" -import { Tab } from "../../../../../../components/layouts/tab/tab.tsx" -import { LinkButton } from "../../../../../../components/linkButton.tsx" -import { tagLayoutRoute } from "../../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/tagLayoutRoute.tsx" -import { DeleteOneTag } from "./deleteOneTag.tsx" -import { UpdateOneTag } from "./updateOneTag.tsx" - -export function TagLayout() { - const params = useParams({ - from: tagLayoutRoute.id, - }) - - return ( - - - {(tag) => { - return ( - <> - -
- - } - text="Retour" - /> - -
-
- - } - text="Modifier" - /> - - - } - title="Supprimer" - color="danger" - /> - -
-
- - , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/catégories/$idTag", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - idTag: params.idTag, - }, - }, - { - label: "Métadonnées", - icon: , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/catégories/$idTag/métadonnées", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - idTag: params.idTag, - }, - }, - ]} - /> - - - - ) - }} -
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/tags/$idTag/tagMetadataTab.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/tags/$idTag/tagMetadataTab.tsx deleted file mode 100644 index c7c6c4ad..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/tags/$idTag/tagMetadataTab.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { readOneTagRouteDefinition } from "@arrhes/application-metadata/routes" -import { FormatDateTime, FormatText } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { useParams } from "@tanstack/react-router" -import { DataBlock } from "../../../../../../components/layouts/dataBlock/dataBlock.tsx" -import { DataWrapper } from "../../../../../../components/layouts/dataWrapper.tsx" -import { Section } from "../../../../../../components/layouts/section/section.tsx" -import { tagLayoutRoute } from "../../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/tagLayoutRoute.tsx" - -export function TagMetadataTab() { - const params = useParams({ - from: tagLayoutRoute.id, - }) - - return ( - - {(tag) => { - return ( - - - - - - - - - - - - {tag.id} - - - - - ) - }} - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/tags/$idTag/tagPage.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/tags/$idTag/tagPage.tsx deleted file mode 100644 index f729a8aa..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/tags/$idTag/tagPage.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { readOneTagRouteDefinition } from "@arrhes/application-metadata/routes" -import { FormatText } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { useParams } from "@tanstack/react-router" -import { DataBlock } from "../../../../../../components/layouts/dataBlock/dataBlock.tsx" -import { DataWrapper } from "../../../../../../components/layouts/dataWrapper.tsx" -import { Section } from "../../../../../../components/layouts/section/section.tsx" -import { tagLayoutRoute } from "../../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/tagLayoutRoute.tsx" - -export function TagPage() { - const params = useParams({ - from: tagLayoutRoute.id, - }) - - return ( - - {(tag) => { - return ( - - - - - - {tag.label} - - - - - ) - }} - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/tags/$idTag/updateOneTag.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/tags/$idTag/updateOneTag.tsx deleted file mode 100644 index 6d5efb55..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/tags/$idTag/updateOneTag.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { - readAllTagsRouteDefinition, - readOneTagRouteDefinition, - updateOneTagRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { InputText, toast } from "@arrhes/ui" -import { IconPlus } from "@tabler/icons-react" -import { type JSX, useState } from "react" -import { Fragment } from "react/jsx-runtime" -import type * as v from "valibot" -import { FormControl } from "../../../../../../components/forms/formControl.tsx" -import { FormError } from "../../../../../../components/forms/formError.tsx" -import { FormField } from "../../../../../../components/forms/formField.tsx" -import { FormItem } from "../../../../../../components/forms/formItem.tsx" -import { FormLabel } from "../../../../../../components/forms/formLabel.tsx" -import { FormRoot } from "../../../../../../components/forms/formRoot.tsx" -import { Drawer } from "../../../../../../components/overlays/drawer/drawer.tsx" -import { getResponseBodyFromAPI } from "../../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../../utilities/invalidateData.ts" - -export function UpdateOneTag(props: { tag: v.InferOutput; children: JSX.Element }) { - const [open, setOpen] = useState(false) - - return ( - - {props.children} - - - - , - text: "Modifier la catégorie", - }} - onSubmit={async (data) => { - const updateTagResponse = await getResponseBodyFromAPI({ - routeDefinition: updateOneTagRouteDefinition, - body: data, - }) - if (updateTagResponse.ok === false) { - toast({ - title: "Impossible de modifier la catégorie", - variant: "error", - }) - return false - } - - toast({ - title: "Catégorie modifiée avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await Promise.all([ - invalidateData({ - routeDefinition: readAllTagsRouteDefinition, - body: { - idYear: props.tag.idYear, - }, - }), - invalidateData({ - routeDefinition: readOneTagRouteDefinition, - body: { - idTag: props.tag.id, - idYear: props.tag.idYear, - }, - }), - ]) - - setOpen(false) - }} - > - {(form) => ( - - ( - - - - - - - - )} - /> - - )} - - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/tags/CreateOneTag.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/tags/CreateOneTag.tsx new file mode 100644 index 00000000..60fcb433 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/tags/CreateOneTag.tsx @@ -0,0 +1,121 @@ +import { createOneTagRouteDefinition, readAllTagsRouteDefinition } from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, InputText, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPlus } from "@tabler/icons-react" +import type { JSX } from "react" +import { Fragment } from "react/jsx-runtime" +import type * as v from "valibot" +import { FormControl } from "../../../../../components/forms/FormControl.tsx" +import { FormError } from "../../../../../components/forms/FormError.tsx" +import { FormField } from "../../../../../components/forms/FormField.tsx" +import { FormItem } from "../../../../../components/forms/FormItem.tsx" +import { FormLabel } from "../../../../../components/forms/FormLabel.tsx" +import { FormRoot } from "../../../../../components/forms/FormRoot.tsx" +import { useTabs } from "../../../../../contexts/tabs/useTabs.tsx" +import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../../utilities/invalidateData.ts" + +export function CreateOneTag(props: { + idOrganization: v.InferOutput["id"] + idYear: v.InferOutput["id"] + children: JSX.Element +}) { + const { openPanelTab, closeTab } = useTabs() + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/tags/TagListTableRow.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/tags/TagListTableRow.tsx new file mode 100644 index 00000000..cc1867bf --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/tags/TagListTableRow.tsx @@ -0,0 +1,73 @@ +import type { schemas } from "@arrhes/application-metadata/schemas" +import { formatDate, LinkContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import type * as v from "valibot" +import { ListTable } from "../../../../../components/layouts/listTable/listTable.tsx" +import { UpdateOneTag } from "./$idTag/UpdateOneTag.tsx" + +export function TagListTableRow(props: { tag: v.InferOutput }) { + const createdAt = formatDate(props.tag.createdAt) + + return ( + +
+
+
+ + + {props.tag.label ?? undefined} + + +
+
+
+ {`Ajouté le ${createdAt}`} +
+
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/tags/tagSelect.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/tags/TagSelect.tsx similarity index 100% rename from packages/website/src/features/dashboard/$idYear/yearSettings/tags/tagSelect.tsx rename to packages/website/src/features/dashboard/$idYear/yearSettings/tags/TagSelect.tsx diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/tags/TagsListTable.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/tags/TagsListTable.tsx new file mode 100644 index 00000000..8e3e50a3 --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/tags/TagsListTable.tsx @@ -0,0 +1,44 @@ +import { readAllTagsRouteDefinition } from "@arrhes/application-metadata/routes" +import type { schemas } from "@arrhes/application-metadata/schemas" +import { IconTag } from "@tabler/icons-react" +import type * as v from "valibot" +import { DataWrapper } from "../../../../../components/layouts/DataWrapper.tsx" +import { EmptyState } from "../../../../../components/layouts/EmptyState.tsx" +import { ListTable } from "../../../../../components/layouts/listTable/listTable.tsx" +import { TagListTableRow } from "./TagListTableRow.tsx" + +export function TagsListTable(props: { + idOrganization: v.InferOutput["id"] + idYear: v.InferOutput["id"] +}) { + return ( + + + {(tags) => { + const sortedTags = tags.sort((a, b) => a.createdAt.localeCompare(b.createdAt)) + + if (sortedTags.length === 0) { + return ( + } + title="Aucune catégorie" + subtitle="Ajoutez une catégorie pour commencer" + /> + ) + } + return sortedTags.map((tag) => ( + + )) + }} + + + ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/tags/TagsPage.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/tags/TagsPage.tsx new file mode 100644 index 00000000..81cbfebf --- /dev/null +++ b/packages/website/src/features/dashboard/$idYear/yearSettings/tags/TagsPage.tsx @@ -0,0 +1,59 @@ +import { ButtonPlainContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPlus } from "@tabler/icons-react" +import { useParams } from "@tanstack/react-router" +import { Page } from "../../../../../components/layouts/page/page.tsx" +import { Section } from "../../../../../components/layouts/section/section.tsx" +import { CreateOneTag } from "./CreateOneTag.tsx" +import { TagsListTable } from "./TagsListTable.tsx" + +export function TagsPage({ + idOrganization: idOrganizationProp, + idYear: idYearProp, +}: { + idOrganization?: string + idYear?: string +} = {}) { + const params = useParams({ + strict: false, + }) as { + idOrganization?: string + idYear?: string + } + const idOrganization = idOrganizationProp ?? params.idOrganization ?? "" + const idYear = idYearProp ?? params.idYear ?? "" + + return ( + + + + +
+ + } + text="Ajouter une catégorie" + /> + +
+ +
+
+
+
+ ) +} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/tags/createOneTag.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/tags/createOneTag.tsx deleted file mode 100644 index 8dfcdf41..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/tags/createOneTag.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { createOneTagRouteDefinition, readAllTagsRouteDefinition } from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { InputText, toast } from "@arrhes/ui" -import { IconPlus } from "@tabler/icons-react" -import { type JSX, useState } from "react" -import { Fragment } from "react/jsx-runtime" -import type * as v from "valibot" -import { FormControl } from "../../../../../components/forms/formControl.tsx" -import { FormError } from "../../../../../components/forms/formError.tsx" -import { FormField } from "../../../../../components/forms/formField.tsx" -import { FormItem } from "../../../../../components/forms/formItem.tsx" -import { FormLabel } from "../../../../../components/forms/formLabel.tsx" -import { FormRoot } from "../../../../../components/forms/formRoot.tsx" -import { Drawer } from "../../../../../components/overlays/drawer/drawer.tsx" -import { getResponseBodyFromAPI } from "../../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../../utilities/invalidateData.ts" - -export function CreateOneTag(props: { - idOrganization: v.InferOutput["id"] - idYear: v.InferOutput["id"] - children: JSX.Element -}) { - const [open, setOpen] = useState(false) - - return ( - - {props.children} - - - - , - text: "Ajouter la catégorie", - }} - onSubmit={async (data) => { - const createTagResponse = await getResponseBodyFromAPI({ - routeDefinition: createOneTagRouteDefinition, - body: data, - }) - if (createTagResponse.ok === false) { - toast({ - title: "Impossible d'ajouter la catégorie", - variant: "error", - }) - return false - } - - toast({ - title: "Catégorie ajoutée avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await invalidateData({ - routeDefinition: readAllTagsRouteDefinition, - body: { - idYear: props.idYear, - }, - }) - - setOpen(false) - }} - > - {(form) => ( - - ( - - - - - - - - )} - /> - - )} - - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/tags/tagListTableRow.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/tags/tagListTableRow.tsx deleted file mode 100644 index a549d1c8..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/tags/tagListTableRow.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import type { schemas } from "@arrhes/application-metadata/schemas" -import { formatDate, LinkContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import type * as v from "valibot" -import { ListTable } from "../../../../../components/layouts/listTable/listTable.tsx" -import { LinkButton } from "../../../../../components/linkButton.tsx" - -export function TagListTableRow(props: { tag: v.InferOutput }) { - const createdAt = formatDate(props.tag.createdAt) - - return ( - -
-
-
- - - {props.tag.label ?? undefined} - - -
-
-
- {`Ajouté le ${createdAt}`} -
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/tags/tagsListTable.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/tags/tagsListTable.tsx deleted file mode 100644 index 65983af2..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/tags/tagsListTable.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { readAllTagsRouteDefinition } from "@arrhes/application-metadata/routes" -import type { schemas } from "@arrhes/application-metadata/schemas" -import { IconTag } from "@tabler/icons-react" -import type * as v from "valibot" -import { DataWrapper } from "../../../../../components/layouts/dataWrapper.tsx" -import { EmptyState } from "../../../../../components/layouts/emptyState.tsx" -import { ListTable } from "../../../../../components/layouts/listTable/listTable.tsx" -import { TagListTableRow } from "./tagListTableRow.tsx" - -export function TagsListTable(props: { - idOrganization: v.InferOutput["id"] - idYear: v.InferOutput["id"] -}) { - return ( - - - {(tags) => { - const sortedTags = tags.sort((a, b) => a.createdAt.localeCompare(b.createdAt)) - - if (sortedTags.length === 0) { - return ( - } - title="Aucune catégorie" - subtitle="Ajoutez une catégorie pour commencer" - /> - ) - } - return sortedTags.map((tag) => ( - - )) - }} - - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/tags/tagsPage.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/tags/tagsPage.tsx deleted file mode 100644 index c4dc2577..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/tags/tagsPage.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { ButtonPlainContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconPlus } from "@tabler/icons-react" -import { useParams } from "@tanstack/react-router" -import { Page } from "../../../../../components/layouts/page/page.tsx" -import { Section } from "../../../../../components/layouts/section/section.tsx" -import { tagsRoute } from "../../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/tags/tagsRoute.tsx" -import { CreateOneTag } from "./createOneTag.tsx" -import { TagsListTable } from "./tagsListTable.tsx" - -export function TagsPage() { - const params = useParams({ - from: tagsRoute.id, - }) - - return ( - - - - -
- - } - text="Ajouter une catégorie" - /> - -
- -
-
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/updateOneYear.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/updateOneYear.tsx deleted file mode 100644 index 3a1e1d51..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/updateOneYear.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { readOneYearRouteDefinition, updateOneYearRouteDefinition } from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { InputDate, InputText, toast } from "@arrhes/ui" -import { IconPencil } from "@tabler/icons-react" -import { type JSX, useState } from "react" -import { Fragment } from "react/jsx-runtime" -import type * as v from "valibot" -import { FormControl } from "../../../../components/forms/formControl.tsx" -import { FormError } from "../../../../components/forms/formError.tsx" -import { FormField } from "../../../../components/forms/formField.tsx" -import { FormItem } from "../../../../components/forms/formItem.tsx" -import { FormLabel } from "../../../../components/forms/formLabel.tsx" -import { FormRoot } from "../../../../components/forms/formRoot.tsx" -import { Drawer } from "../../../../components/overlays/drawer/drawer.tsx" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../utilities/invalidateData.ts" -import { YearSelect } from "../../$idOrganization/years/yearSelect.tsx" - -export function UpdateOneYear(props: { year: v.InferOutput; children: JSX.Element }) { - const [open, setOpen] = useState(false) - - return ( - - {props.children} - - - - , - text: "Modifier l'exercice", - }} - onSubmit={async (data) => { - const response = await getResponseBodyFromAPI({ - routeDefinition: updateOneYearRouteDefinition, - body: data, - }) - if (!response.ok) { - toast({ - title: "Impossible de modifier l'exercice", - variant: "error", - }) - return false - } - - toast({ - title: "Exercice modifié avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await invalidateData({ - routeDefinition: readOneYearRouteDefinition, - body: { - idYear: props.year.id, - }, - }) - - setOpen(false) - }} - > - {(form) => ( - - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - - )} - - - - - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/yearSettingsLayout.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/yearSettingsLayout.tsx deleted file mode 100644 index d7da14b2..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/yearSettingsLayout.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { - IconBook, - IconFileReport, - IconHome, - IconListNumbers, - IconReportMoney, - IconScale, - IconSettings, - IconTag, -} from "@tabler/icons-react" -import { useParams } from "@tanstack/react-router" -import { SubPageLayout } from "../../../../components/layouts/subPageLayout.tsx" -import { yearSettingsLayoutRoute } from "../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/yearSettingsLayoutRoute.tsx" - -export function YearSettingsLayout() { - const params = useParams({ - from: yearSettingsLayoutRoute.id, - }) - - return ( - , - items: [ - { - label: "Général", - icon: , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - }, - }, - { - label: "Plan des comptes", - icon: , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/comptes", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - }, - }, - { - label: "Journaux", - icon: , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/journaux", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - }, - }, - { - label: "Catégories", - icon: , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/catégories", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - }, - }, - ], - }, - documents: { - title: "Documents comptables", - icon: , - items: [ - { - label: "Bilan", - icon: , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/bilan", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - }, - }, - { - label: "Compte de résultat", - icon: , - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/compte-de-résultat", - params: { - idOrganization: params.idOrganization, - idYear: params.idYear, - }, - }, - ], - }, - }} - /> - ) -} diff --git a/packages/website/src/features/dashboard/$idYear/yearSettings/yearSettingsPage.tsx b/packages/website/src/features/dashboard/$idYear/yearSettings/yearSettingsPage.tsx deleted file mode 100644 index 8face1c0..00000000 --- a/packages/website/src/features/dashboard/$idYear/yearSettings/yearSettingsPage.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { readOneYearRouteDefinition } from "@arrhes/application-metadata/routes" -import { ButtonOutlineContent } from "@arrhes/ui" -import { IconPencil, IconTrash } from "@tabler/icons-react" -import { useParams } from "@tanstack/react-router" -import { DataWrapper } from "../../../../components/layouts/dataWrapper.tsx" -import { Page } from "../../../../components/layouts/page/page.tsx" -import { PageRoot } from "../../../../components/layouts/page/pageRoot.tsx" -import { SettingsSection } from "../../../../components/layouts/settingsSection/settingsSection.tsx" -import { yearSettingsRoute } from "../../../../routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/yearSettingsRoute.tsx" -import { DeleteOneYear } from "./deleteOneYear.tsx" -import { UpdateOneYear } from "./updateOneYear.tsx" - -export function YearSettingsPage() { - const params = useParams({ - from: yearSettingsRoute.id, - }) - - return ( - - - - {(year) => { - return ( - <> - - - - - } - text="Modifier" - /> - - - - - - - - } - text="Supprimer" - color="danger" - /> - - - - - ) - }} - - - - ) -} diff --git a/packages/website/src/features/dashboard/DashboardDefaultPage.tsx b/packages/website/src/features/dashboard/DashboardDefaultPage.tsx new file mode 100644 index 00000000..fdab5321 --- /dev/null +++ b/packages/website/src/features/dashboard/DashboardDefaultPage.tsx @@ -0,0 +1,40 @@ +import { Kbd } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" + +export function DashboardDefaultPage() { + return ( +
+

+ Aucun onglet ouvert +

+

+ Ouvrez un nouvel onglet avec le raccourci clavier Ctrl+K ou la barre de recherche en haut de + la page. +

+
+ ) +} diff --git a/packages/website/src/features/dashboard/DashboardNotFoundPage.tsx b/packages/website/src/features/dashboard/DashboardNotFoundPage.tsx new file mode 100644 index 00000000..49407951 --- /dev/null +++ b/packages/website/src/features/dashboard/DashboardNotFoundPage.tsx @@ -0,0 +1,83 @@ +import { ButtonOutlineContent } from "@arrhes/ui/components/buttons/ButtonOutlineContent.js" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconHome } from "@tabler/icons-react" +import { LinkButton } from "../../components/LinkButton.js" + +export function DashboardNotFoundPage() { + return ( +
+
+ + 404 + +
+

+ Page introuvable +

+

+ La page que vous recherchez n'existe pas ou a été déplacée. +

+
+
+ + } + /> + +
+
+
+ ) +} diff --git a/packages/website/src/features/dashboard/OrganizationContextSelect.tsx b/packages/website/src/features/dashboard/OrganizationContextSelect.tsx new file mode 100644 index 00000000..9d3a4b6f --- /dev/null +++ b/packages/website/src/features/dashboard/OrganizationContextSelect.tsx @@ -0,0 +1,99 @@ +import { Button, ButtonGhostContent, ButtonPlainContent, Separator } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconChevronDown } from "@tabler/icons-react" +import { useState } from "react" +import { getAllMyOrganizationsRouteDefinition } from "../../../../metadata/src/routes/dashboard/auth/index.js" +import { Popover } from "../../components/overlays/popover/popover.js" +import { useDataFromAPI } from "../../utilities/useHTTPData.js" +import { AddNewOrganization } from "./organizations/AddNewOrganization.js" + +export function OrganizationContextSelect(props: { value: string | null; onChange: (v: string | null) => void }) { + const [open, setOpen] = useState(false) + const organizationUsersData = useDataFromAPI({ + routeDefinition: getAllMyOrganizationsRouteDefinition, + body: {}, + }) + + const options = (organizationUsersData.data ?? []).map((organizationUser) => ({ + key: organizationUser.organization.id, + label: organizationUser.organization.name, + })) + const selectedLabel = options.find((option) => option.key === props.value)?.label + + if (options.length === 0) { + return ( + + + + ) + } + + return ( + + + + + + {options.map((option) => ( + + ))} + +
setOpen(false)}> + + + +
+
+
+ ) +} diff --git a/packages/website/src/features/dashboard/YearContextSelect.tsx b/packages/website/src/features/dashboard/YearContextSelect.tsx new file mode 100644 index 00000000..bc120458 --- /dev/null +++ b/packages/website/src/features/dashboard/YearContextSelect.tsx @@ -0,0 +1,113 @@ +import { readAllYearsRouteDefinition } from "@arrhes/application-metadata" +import { Button, ButtonGhostContent, ButtonPlainContent, Separator } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconChevronDown } from "@tabler/icons-react" +import { useState } from "react" +import { Popover } from "../../components/overlays/popover/popover.js" +import { useDataFromAPI } from "../../utilities/useHTTPData.js" +import { CreateOneYear } from "./$idOrganization/years/CreateOneYear.js" + +export function YearContextSelect(props: { + value: string | null + onChange: (v: string | null) => void + idOrganizationSelected: string | null +}) { + const [open, setOpen] = useState(false) + const yearsData = useDataFromAPI({ + routeDefinition: readAllYearsRouteDefinition, + body: { + idOrganization: props.idOrganizationSelected ?? undefined, + }, + enabled: props.idOrganizationSelected !== null, + }) + + const options = (yearsData.data ?? []) + .filter((y) => y.idOrganization === props.idOrganizationSelected) + .map((y) => ({ + key: y.id, + label: y.label, + })) + + const selectedLabel = options.find((option) => option.key === props.value)?.label + + if (options.length === 0) { + if (!props.idOrganizationSelected) return null + return ( + + + + ) + } + + return ( + + + + + + {options.map((option) => ( + + ))} + + {props.idOrganizationSelected !== null && ( +
setOpen(false)}> + + + +
+ )} +
+
+ ) +} diff --git a/packages/website/src/features/dashboard/admin/AdminPanelLayout.tsx b/packages/website/src/features/dashboard/admin/AdminPanelLayout.tsx new file mode 100644 index 00000000..846e2890 --- /dev/null +++ b/packages/website/src/features/dashboard/admin/AdminPanelLayout.tsx @@ -0,0 +1,5 @@ +import { Outlet } from "@tanstack/react-router" + +export function AdminPanelLayout() { + return +} diff --git a/packages/website/src/features/dashboard/admin/adminPanelLayout.tsx b/packages/website/src/features/dashboard/admin/adminPanelLayout.tsx deleted file mode 100644 index 0bbb3884..00000000 --- a/packages/website/src/features/dashboard/admin/adminPanelLayout.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { PageNavigation } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconTicket } from "@tabler/icons-react" -import { Outlet } from "@tanstack/react-router" - -export function AdminPanelLayout() { - return ( -
- , - to: "/dashboard/admin/tickets", - }, - ]} - /> -
- -
-
- ) -} diff --git a/packages/website/src/features/dashboard/admin/tickets/$idTicket/AdminTicketTabContent.tsx b/packages/website/src/features/dashboard/admin/tickets/$idTicket/AdminTicketTabContent.tsx new file mode 100644 index 00000000..1a5de001 --- /dev/null +++ b/packages/website/src/features/dashboard/admin/tickets/$idTicket/AdminTicketTabContent.tsx @@ -0,0 +1,59 @@ +import { adminReadOneTicketRouteDefinition } from "@arrhes/application-metadata/routes" +import { CircularLoader } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { useDataFromAPI } from "../../../../../utilities/useHTTPData.js" +import { StatusToggle } from "./StatusToggle.js" +import { TicketPage } from "./TicketPage.js" + +export function AdminTicketTabContent(props: { idTicket: string }) { + const ticket = useDataFromAPI({ + routeDefinition: adminReadOneTicketRouteDefinition, + body: { + idTicket: props.idTicket, + }, + }) + + return ( +
+ {ticket.isPending && } + + {ticket.isError && ( + + Erreur lors de la recuperation du ticket. + + )} + + {ticket.data && ( +
+ +
+ )} + + +
+ ) +} diff --git a/packages/website/src/features/dashboard/admin/tickets/$idTicket/createOneTicketMessage.tsx b/packages/website/src/features/dashboard/admin/tickets/$idTicket/CreateOneTicketMessage.tsx similarity index 100% rename from packages/website/src/features/dashboard/admin/tickets/$idTicket/createOneTicketMessage.tsx rename to packages/website/src/features/dashboard/admin/tickets/$idTicket/CreateOneTicketMessage.tsx diff --git a/packages/website/src/features/dashboard/admin/tickets/$idTicket/statusToggle.tsx b/packages/website/src/features/dashboard/admin/tickets/$idTicket/StatusToggle.tsx similarity index 100% rename from packages/website/src/features/dashboard/admin/tickets/$idTicket/statusToggle.tsx rename to packages/website/src/features/dashboard/admin/tickets/$idTicket/StatusToggle.tsx diff --git a/packages/website/src/features/dashboard/admin/tickets/$idTicket/TicketLayout.tsx b/packages/website/src/features/dashboard/admin/tickets/$idTicket/TicketLayout.tsx new file mode 100644 index 00000000..ef1611e2 --- /dev/null +++ b/packages/website/src/features/dashboard/admin/tickets/$idTicket/TicketLayout.tsx @@ -0,0 +1,74 @@ +import { adminReadOneTicketRouteDefinition } from "@arrhes/application-metadata/routes" +import { ButtonGhostContent, CircularLoader } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconArrowLeft } from "@tabler/icons-react" +import { Outlet, useParams } from "@tanstack/react-router" +import { LinkButton } from "../../../../../components/LinkButton.js" + +import { useDataFromAPI } from "../../../../../utilities/useHTTPData.js" +import { StatusToggle } from "./StatusToggle.js" + +export function TicketLayout() { + const params = useParams({ + strict: false, + }) as { + idTicket: string + } + + const ticket = useDataFromAPI({ + routeDefinition: adminReadOneTicketRouteDefinition, + body: { + idTicket: params.idTicket, + }, + }) + + return ( +
+ {ticket.isPending && } + + {ticket.isError && ( + + Erreur lors de la recuperation du ticket. + + )} + + {ticket.data && ( +
+ + } + text="Retour aux tickets" + /> + + +
+ )} + + +
+ ) +} diff --git a/packages/website/src/features/dashboard/admin/tickets/$idTicket/ticketMessageList.tsx b/packages/website/src/features/dashboard/admin/tickets/$idTicket/TicketMessageList.tsx similarity index 100% rename from packages/website/src/features/dashboard/admin/tickets/$idTicket/ticketMessageList.tsx rename to packages/website/src/features/dashboard/admin/tickets/$idTicket/TicketMessageList.tsx diff --git a/packages/website/src/features/dashboard/admin/tickets/$idTicket/TicketPage.tsx b/packages/website/src/features/dashboard/admin/tickets/$idTicket/TicketPage.tsx new file mode 100644 index 00000000..fe8043f0 --- /dev/null +++ b/packages/website/src/features/dashboard/admin/tickets/$idTicket/TicketPage.tsx @@ -0,0 +1,267 @@ +import { + adminReadAllTicketMessagesRouteDefinition, + adminReadOneTicketRouteDefinition, + readUserSessionRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { Badge, CircularLoader, formatDate, Separator } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconCheck } from "@tabler/icons-react" +import { useParams } from "@tanstack/react-router" +import { useDataFromAPI } from "../../../../../utilities/useHTTPData.js" +import { CreateOneTicketMessage } from "./CreateOneTicketMessage.js" +import { TicketMessageList } from "./TicketMessageList.js" + +const statusLabels: Record = { + open: "Ouvert", + closed: "Ferme", +} + +const categoryLabels: Record = { + bug: "Erreur", + enhancement: "Amelioration", + feature: "Fonctionnalite", + other: "Autre", +} + +export function TicketPage(props: { idTicket?: string } = {}) { + const params = useParams({ + strict: false, + }) + const idTicket = props.idTicket ?? params.idTicket ?? "" + + const ticket = useDataFromAPI({ + routeDefinition: adminReadOneTicketRouteDefinition, + body: { + idTicket: idTicket, + }, + }) + + const messages = useDataFromAPI({ + routeDefinition: adminReadAllTicketMessagesRouteDefinition, + body: { + idTicket: idTicket, + }, + }) + + const userSession = useDataFromAPI({ + routeDefinition: readUserSessionRouteDefinition, + body: {}, + }) + + return ( +
+ {ticket.isPending && } + + {ticket.isError && ( + + Erreur lors de la recuperation du ticket. + + )} + + {ticket.data && ( +
+
+
+

+ Ticket +

+ + {ticket.data.id} + +
+ {statusLabels[ticket.data.status] ?? ticket.data.status} +
+ +
+ + Categorie :{" "} + + + {categoryLabels[ticket.data.category] ?? ticket.data.category} + +
+ + {`Utilisateur : ${ticket.data.idUser}`} + + + {`Cree le ${formatDate(ticket.data.createdAt, { + includeTime: true, + })}`} + + + {ticket.data.lastUpdatedAt ? ( + + {`Mis a jour le ${formatDate(ticket.data.lastUpdatedAt, { + includeTime: true, + })}`} + + ) : null} + + + + {messages.data && + (() => { + const sorted = [ + ...messages.data, + ].sort((a, b) => b.createdAt.localeCompare(a.createdAt)) + const lastMessage = sorted[0] + + return ( +
+ {lastMessage ? ( + + {lastMessage.idAdminUser !== null || + lastMessage.idUser === userSession.data?.user.id + ? "Dernier message : Admin (en attente de reponse utilisateur)" + : "Dernier message : Utilisateur (en attente de votre reponse)"} + + ) : ( + + Aucun message + + )} +
+ ) + })()} +
+ )} + + {ticket.data && + (ticket.data.status === "open" ? ( + + ) : ( +
+ + + Ce ticket est ferme. Rouvrez-le pour repondre. + +
+ ))} + + +
+ ) +} diff --git a/packages/website/src/features/dashboard/admin/tickets/$idTicket/ticketLayout.tsx b/packages/website/src/features/dashboard/admin/tickets/$idTicket/ticketLayout.tsx deleted file mode 100644 index f0be3365..00000000 --- a/packages/website/src/features/dashboard/admin/tickets/$idTicket/ticketLayout.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { adminReadOneTicketRouteDefinition } from "@arrhes/application-metadata/routes" -import { ButtonGhostContent, CircularLoader } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconArrowLeft } from "@tabler/icons-react" -import { Outlet, useParams } from "@tanstack/react-router" -import { LinkButton } from "../../../../../components/linkButton.js" -import { $idTicketLayoutRoute } from "../../../../../routes/root/dashboard/admin/tickets/$idTicket/$idTicketLayoutRoute.tsx" -import { useDataFromAPI } from "../../../../../utilities/useHTTPData.js" -import { StatusToggle } from "./statusToggle.js" - -export function TicketLayout() { - const params = useParams({ - from: $idTicketLayoutRoute.id, - }) - - const ticket = useDataFromAPI({ - routeDefinition: adminReadOneTicketRouteDefinition, - body: { - idTicket: params.idTicket, - }, - }) - - return ( -
- {ticket.isPending && } - - {ticket.isError && ( - - Erreur lors de la recuperation du ticket. - - )} - - {ticket.data && ( -
- - } - text="Retour aux tickets" - /> - - -
- )} - - -
- ) -} diff --git a/packages/website/src/features/dashboard/admin/tickets/$idTicket/ticketPage.tsx b/packages/website/src/features/dashboard/admin/tickets/$idTicket/ticketPage.tsx deleted file mode 100644 index b0bb9a68..00000000 --- a/packages/website/src/features/dashboard/admin/tickets/$idTicket/ticketPage.tsx +++ /dev/null @@ -1,267 +0,0 @@ -import { - adminReadAllTicketMessagesRouteDefinition, - adminReadOneTicketRouteDefinition, - readUserSessionRouteDefinition, -} from "@arrhes/application-metadata/routes" -import { Badge, CircularLoader, formatDate, Separator } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconCheck } from "@tabler/icons-react" -import { useParams } from "@tanstack/react-router" -import { $idTicketLayoutRoute } from "../../../../../routes/root/dashboard/admin/tickets/$idTicket/$idTicketLayoutRoute.tsx" -import { useDataFromAPI } from "../../../../../utilities/useHTTPData.js" -import { CreateOneTicketMessage } from "./createOneTicketMessage.js" -import { TicketMessageList } from "./ticketMessageList.js" - -const statusLabels: Record = { - open: "Ouvert", - closed: "Ferme", -} - -const categoryLabels: Record = { - bug: "Erreur", - enhancement: "Amelioration", - feature: "Fonctionnalite", - other: "Autre", -} - -export function TicketPage() { - const params = useParams({ - from: $idTicketLayoutRoute.id, - }) - - const ticket = useDataFromAPI({ - routeDefinition: adminReadOneTicketRouteDefinition, - body: { - idTicket: params.idTicket, - }, - }) - - const messages = useDataFromAPI({ - routeDefinition: adminReadAllTicketMessagesRouteDefinition, - body: { - idTicket: params.idTicket, - }, - }) - - const userSession = useDataFromAPI({ - routeDefinition: readUserSessionRouteDefinition, - body: {}, - }) - - return ( -
- {ticket.isPending && } - - {ticket.isError && ( - - Erreur lors de la recuperation du ticket. - - )} - - {ticket.data && ( -
-
-
-

- Ticket -

- - {ticket.data.id} - -
- {statusLabels[ticket.data.status] ?? ticket.data.status} -
- -
- - Categorie :{" "} - - - {categoryLabels[ticket.data.category] ?? ticket.data.category} - -
- - {`Utilisateur : ${ticket.data.idUser}`} - - - {`Cree le ${formatDate(ticket.data.createdAt, { - includeTime: true, - })}`} - - - {ticket.data.lastUpdatedAt ? ( - - {`Mis a jour le ${formatDate(ticket.data.lastUpdatedAt, { - includeTime: true, - })}`} - - ) : null} - - - - {messages.data && - (() => { - const sorted = [ - ...messages.data, - ].sort((a, b) => b.createdAt.localeCompare(a.createdAt)) - const lastMessage = sorted[0] - - return ( -
- {lastMessage ? ( - - {lastMessage.idAdminUser !== null || - lastMessage.idUser === userSession.data?.user.id - ? "Dernier message : Admin (en attente de reponse utilisateur)" - : "Dernier message : Utilisateur (en attente de votre reponse)"} - - ) : ( - - Aucun message - - )} -
- ) - })()} -
- )} - - {ticket.data && - (ticket.data.status === "open" ? ( - - ) : ( -
- - - Ce ticket est ferme. Rouvrez-le pour repondre. - -
- ))} - - -
- ) -} diff --git a/packages/website/src/features/dashboard/admin/tickets/TicketsPage.tsx b/packages/website/src/features/dashboard/admin/tickets/TicketsPage.tsx new file mode 100644 index 00000000..b4cfa702 --- /dev/null +++ b/packages/website/src/features/dashboard/admin/tickets/TicketsPage.tsx @@ -0,0 +1,187 @@ +import { adminReadAllTicketsRouteDefinition } from "@arrhes/application-metadata/routes" +import { Badge, Button, CircularLoader, formatDate } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconExternalLink } from "@tabler/icons-react" +import type * as v from "valibot" +import { Page } from "../../../../components/layouts/page/page.js" +import { TabLink } from "../../../../components/layouts/tabBar/TabLink.js" +import { useDataFromAPI } from "../../../../utilities/useHTTPData.js" + +const statusLabels: Record = { + open: "Ouvert", + closed: "Ferme", +} + +const categoryLabels: Record = { + bug: "Bug", + enhancement: "Amelioration", + feature: "Fonctionnalite", + other: "Autre", +} + +type Ticket = v.InferOutput<(typeof adminReadAllTicketsRouteDefinition)["schemas"]["return"]>[number] + +function TicketRow(props: { ticket: Ticket }) { + const { ticket } = props + return ( + + + + ) +} + +export function TicketsPage() { + const tickets = useDataFromAPI({ + routeDefinition: adminReadAllTicketsRouteDefinition, + body: {}, + }) + + return ( + + + Tickets + + + {tickets.isPending && } + + {tickets.isError && ( +
+ + Erreur lors de la recuperation des tickets. + +
+ )} + + {tickets.data && tickets.data.length === 0 && ( + + Aucun ticket pour le moment. + + )} + + {tickets.data && tickets.data.length > 0 && ( +
+ {[ + ...tickets.data, + ] + .sort((a, b) => b.createdAt.localeCompare(a.createdAt)) + .map((ticket) => ( + + ))} +
+ )} +
+
+ ) +} diff --git a/packages/website/src/features/dashboard/admin/tickets/ticketsPage.tsx b/packages/website/src/features/dashboard/admin/tickets/ticketsPage.tsx deleted file mode 100644 index 5c56fc4f..00000000 --- a/packages/website/src/features/dashboard/admin/tickets/ticketsPage.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import { adminReadAllTicketsRouteDefinition } from "@arrhes/application-metadata/routes" -import { Badge, CircularLoader, formatDate } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconExternalLink } from "@tabler/icons-react" -import type * as v from "valibot" -import { LinkButton } from "../../../../components/linkButton.js" -import { useDataFromAPI } from "../../../../utilities/useHTTPData.js" - -const statusLabels: Record = { - open: "Ouvert", - closed: "Ferme", -} - -const categoryLabels: Record = { - bug: "Bug", - enhancement: "Amelioration", - feature: "Fonctionnalite", - other: "Autre", -} - -type Ticket = v.InferOutput<(typeof adminReadAllTicketsRouteDefinition)["schemas"]["return"]>[number] - -function TicketRow(props: { ticket: Ticket }) { - const { ticket } = props - return ( - -
-
-
- - {ticket.id} - - {statusLabels[ticket.status] ?? ticket.status} - {categoryLabels[ticket.category] ?? ticket.category} -
- - {`Cree le ${formatDate(ticket.createdAt, { - includeTime: true, - })}`} - {ticket.lastUpdatedAt - ? ` - Mis a jour le ${formatDate(ticket.lastUpdatedAt, { - includeTime: true, - })}` - : ""} - -
- -
-
- ) -} - -export function TicketsPage() { - const tickets = useDataFromAPI({ - routeDefinition: adminReadAllTicketsRouteDefinition, - body: {}, - }) - - return ( -
-

- Tickets -

- - {tickets.isPending && } - - {tickets.isError && ( -
- - Erreur lors de la recuperation des tickets. - -
- )} - - {tickets.data && tickets.data.length === 0 && ( - - Aucun ticket pour le moment. - - )} - - {tickets.data && tickets.data.length > 0 && ( -
- {[ - ...tickets.data, - ] - .sort((a, b) => b.createdAt.localeCompare(a.createdAt)) - .map((ticket) => ( - - ))} -
- )} -
- ) -} diff --git a/packages/website/src/features/dashboard/dashboardLayout.tsx b/packages/website/src/features/dashboard/dashboardLayout.tsx deleted file mode 100644 index 57e7275e..00000000 --- a/packages/website/src/features/dashboard/dashboardLayout.tsx +++ /dev/null @@ -1,237 +0,0 @@ -import { readUserSessionRouteDefinition, signOutRouteDefinition } from "@arrhes/application-metadata/routes" -import { Button, ButtonGhostContent, ButtonOutlineContent, Logo, Separator, toast } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { - IconBook2, - IconBuildings, - IconLifebuoy, - IconLogout, - IconSettings, - IconShield, - IconUser, -} from "@tabler/icons-react" -import { Outlet, useRouterState } from "@tanstack/react-router" -import { LinkButton } from "../../components/linkButton.js" -import { Popover } from "../../components/overlays/popover/popover.js" -import { applicationRouter } from "../../routes/applicationRouter.js" -import { deleteCookies } from "../../utilities/cookies/deleteCookies.js" -import { getResponseBodyFromAPI } from "../../utilities/getResponseBodyFromAPI.js" -import { useDataFromAPI } from "../../utilities/useHTTPData.js" -import { Breadcrumbs } from "../breadcrumbs.js" - -export function DashboardLayout() { - const pathname = useRouterState({ - select: (state) => state.location.pathname, - }) - const isAdminPath = pathname.startsWith("/dashboard/admin") - - const userSession = useDataFromAPI({ - routeDefinition: readUserSessionRouteDefinition, - body: {}, - }) - - return ( -
- {/* Header */} -
-
-
- - } - text={isAdminPath ? "Admin" : undefined} - /> - - -
- -
-
- - {/* Main content */} -
-
- -
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/dashboardLayout/DashboardLayout.tsx b/packages/website/src/features/dashboard/dashboardLayout/DashboardLayout.tsx new file mode 100644 index 00000000..4d46022c --- /dev/null +++ b/packages/website/src/features/dashboard/dashboardLayout/DashboardLayout.tsx @@ -0,0 +1,13 @@ +import { DashboardContextProvider } from "../../../contexts/dashboard/DashboardContextProvider.js" +import { TabsProvider } from "../../../contexts/tabs/tabsProvider.tsx" +import { DashboardShell } from "./DashboardShell.tsx" + +export function DashboardLayout() { + return ( + + + + + + ) +} diff --git a/packages/website/src/features/dashboard/dashboardLayout/DashboardShell.tsx b/packages/website/src/features/dashboard/dashboardLayout/DashboardShell.tsx new file mode 100644 index 00000000..78a023e3 --- /dev/null +++ b/packages/website/src/features/dashboard/dashboardLayout/DashboardShell.tsx @@ -0,0 +1,658 @@ +import { readUserSessionRouteDefinition, signOutRouteDefinition } from "@arrhes/application-metadata/routes" +import { Button, ButtonGhostContent, ButtonOutlineContent, Logo, Separator, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { + IconBook2, + IconLifebuoy, + IconLogout, + IconSearch, + IconSettings, + IconShield, + IconTicket, + IconUser, +} from "@tabler/icons-react" +import { Outlet } from "@tanstack/react-router" +import { useCallback, useEffect, useRef, useState } from "react" +import { CommandPalette } from "../../../components/layouts/commandPalette/CommandPalette.js" +import { TabBar } from "../../../components/layouts/tabBar/TabBar.js" +import { Popover } from "../../../components/overlays/popover/popover.js" +import { useDashboardContext } from "../../../contexts/dashboard/dashboardContext.js" +import { TabContentArea } from "../../../contexts/tabs/TabContentArea.js" +import { type ComponentTab, currentEntry } from "../../../contexts/tabs/tabsContext.js" +import { useOuterRouter } from "../../../contexts/tabs/useOuterRouter.js" +import { useTabs } from "../../../contexts/tabs/useTabs.js" +import { deleteCookies } from "../../../utilities/cookies/deleteCookies.js" +import { getResponseBodyFromAPI } from "../../../utilities/getResponseBodyFromAPI.js" +import { useDataFromAPI } from "../../../utilities/useHTTPData.js" +import { OrganizationContextSelect } from "../OrganizationContextSelect.js" +import { YearContextSelect } from "../YearContextSelect.js" + +// ─── Inner shell — rendered inside TabsProvider ────────────────────────────── + +export function DashboardShell() { + const { tabs, activeTabId, activateTab, openTab, reorderTabs, closeTab } = useTabs() + const applicationRouter = useOuterRouter() + const { selectedOrgId, selectedYearId, setOrg, setYear } = useDashboardContext() + const [splitPosition, setSplitPosition] = useState(50) + const containerRef = useRef(null) + const isDragging = useRef(false) + const dragStartX = useRef(0) + const dragStartPosition = useRef(50) + + const handleMouseMove = useCallback((e: MouseEvent) => { + if (!isDragging.current || !containerRef.current) return + const rect = containerRef.current.getBoundingClientRect() + const deltaX = e.clientX - dragStartX.current + const deltaPct = (deltaX / rect.width) * 100 + const newPct = Math.min(80, Math.max(20, dragStartPosition.current + deltaPct)) + setSplitPosition(newPct) + }, []) + + const handleMouseUp = useCallback(() => { + isDragging.current = false + window.removeEventListener("mousemove", handleMouseMove) + window.removeEventListener("mouseup", handleMouseUp) + document.body.style.cursor = "" + document.body.style.userSelect = "" + }, [ + handleMouseMove, + ]) + + const handleDragStart = useCallback( + (e: React.MouseEvent) => { + isDragging.current = true + dragStartX.current = e.clientX + dragStartPosition.current = splitPosition + document.body.style.cursor = "col-resize" + document.body.style.userSelect = "none" + window.addEventListener("mousemove", handleMouseMove) + window.addEventListener("mouseup", handleMouseUp) + }, + [ + splitPosition, + handleMouseMove, + handleMouseUp, + ], + ) + + useEffect( + () => () => { + window.removeEventListener("mousemove", handleMouseMove) + window.removeEventListener("mouseup", handleMouseUp) + }, + [ + handleMouseMove, + handleMouseUp, + ], + ) + + const [rightPanel, setRightPanel] = useState<{ + tabIds: string[] + activeTabId: string + } | null>(null) + + // Listen for split-tab events dispatched from the tab bar context menu. + const handleSplitTab = useCallback( + (e: Event) => { + const tabId = ( + e as CustomEvent<{ + tabId: string + }> + ).detail.tabId + setRightPanel((prev) => { + if (!prev) + return { + tabIds: [ + tabId, + ], + activeTabId: tabId, + } + if (prev.tabIds.includes(tabId)) + return { + ...prev, + activeTabId: tabId, + } + return { + tabIds: [ + ...prev.tabIds, + tabId, + ], + activeTabId: tabId, + } + }) + // If the tab was active in the left panel, switch to another left-panel tab. + if (tabId === activeTabId) { + const rightIds = new Set(rightPanel?.tabIds ?? []) + rightIds.add(tabId) + const remaining = tabs.filter((t) => !rightIds.has(t.id)) + if (remaining.length > 0) activateTab(remaining[remaining.length - 1].id) + } + }, + [ + activeTabId, + tabs, + rightPanel, + activateTab, + ], + ) + + useEffect(() => { + window.addEventListener("arrhes:split-tab", handleSplitTab) + return () => window.removeEventListener("arrhes:split-tab", handleSplitTab) + }, [ + handleSplitTab, + ]) + + // Remove closed tabs from right panel. + useEffect(() => { + if (!rightPanel) return + const existingIds = new Set(tabs.map((t) => t.id)) + const filtered = rightPanel.tabIds.filter((id) => existingIds.has(id)) + if (filtered.length === rightPanel.tabIds.length) return + if (filtered.length === 0) { + setRightPanel(null) + } else { + const newActive = filtered.includes(rightPanel.activeTabId) + ? rightPanel.activeTabId + : filtered[filtered.length - 1] + setRightPanel({ + tabIds: filtered, + activeTabId: newActive, + }) + } + }, [ + tabs, + rightPanel, + ]) + + // Auto-close split view when the left panel would have no tabs. + useEffect(() => { + if (!rightPanel) return + const rightSet = new Set(rightPanel.tabIds) + if (tabs.filter((t) => !rightSet.has(t.id)).length === 0) { + setRightPanel(null) + } + }, [ + tabs, + rightPanel, + ]) + + // Update browser title when active tab changes. + useEffect(() => { + const activeTab = tabs.find((t) => t.id === activeTabId) + if (activeTab) { + const title = + activeTab.type === "component" ? currentEntry(activeTab as ComponentTab).title : activeTab.title + document.title = `${title} — Arrhes` + // Update meta description. + const meta = document.querySelector("meta[name='description']") + if (meta && activeTab.type === "component") { + const entry = currentEntry(activeTab as ComponentTab) + if (entry.description) meta.content = entry.description + } + } else { + document.title = "Arrhes" + } + }, [ + tabs, + activeTabId, + ]) + + function handleOrgChange(id: string | null) { + setOrg(id) + } + + function handleYearChange(id: string | null) { + setYear(id) + } + + const userSession = useDataFromAPI({ + routeDefinition: readUserSessionRouteDefinition, + body: {}, + }) + + return ( +
+ {/* Header */} +
+ {/* Breadcrumb: Logo / Org / Year */} +
+ + + / + + + {selectedOrgId !== null && ( + <> + + / + + + + )} +
+ + {/* Search */} + + + {/* Right: nav actions */} + +
+ + {/* Tab bar + content area — panels side by side when split */} +
+ {/* Main / left panel — has its own tab bar */} +
+ setRightPanel(null) : undefined} + onDropFromRight={(tabId, insertBeforeTabId) => { + setRightPanel((prev) => { + if (!prev) return null + const next = prev.tabIds.filter((id) => id !== tabId) + return next.length === 0 + ? null + : { + tabIds: next, + activeTabId: next.includes(prev.activeTabId) + ? prev.activeTabId + : next[next.length - 1], + } + }) + reorderTabs(tabId, insertBeforeTabId) + activateTab(tabId) + }} + /> + + !rightPanel?.tabIds.includes(t.id))} + /> +
+ + {/* Split / right panel */} + {rightPanel && ( + <> + {/* Drag handle */} +
+ {/* Right panel */} +
+ setRightPanel(null)} + panel={{ + tabIds: rightPanel.tabIds, + activeTabId: rightPanel.activeTabId, + onActivate: (tabId) => + setRightPanel( + (prev) => + prev && { + ...prev, + activeTabId: tabId, + }, + ), + onRemove: (tabId) => { + setRightPanel((prev) => { + if (!prev) return null + const next = prev.tabIds.filter((id) => id !== tabId) + if (next.length === 0) { + closeTab(tabId) + return null + } + const newActive = + prev.activeTabId === tabId ? next[next.length - 1] : prev.activeTabId + return { + tabIds: next, + activeTabId: newActive, + } + }) + }, + onReorder: (tabId, insertBeforeTabId) => + setRightPanel((prev) => { + if (!prev) return null + const without = prev.tabIds.filter((id) => id !== tabId) + if (insertBeforeTabId === null) + return { + ...prev, + tabIds: [ + ...without, + tabId, + ], + } + const idx = without.indexOf(insertBeforeTabId) + const tabIds = + idx === -1 + ? [ + ...without, + tabId, + ] + : [ + ...without.slice(0, idx), + tabId, + ...without.slice(idx), + ] + return { + ...prev, + tabIds, + } + }), + onDropFromLeft: (tabId, insertBeforeTabId) => + setRightPanel((prev) => { + const existing = prev?.tabIds.filter((id) => id !== tabId) ?? [] + if (insertBeforeTabId === null) + return { + tabIds: [ + ...existing, + tabId, + ], + activeTabId: tabId, + } + const idx = existing.indexOf(insertBeforeTabId) + const tabIds = + idx === -1 + ? [ + ...existing, + tabId, + ] + : [ + ...existing.slice(0, idx), + tabId, + ...existing.slice(idx), + ] + return { + tabIds, + activeTabId: tabId, + } + }), + }} + /> + rightPanel.tabIds.includes(t.id))} + /> +
+ + )} +
+ + {/* Command palette / search */} + +
+ ) +} diff --git a/packages/website/src/features/dashboard/dashboardNotFoundPage.tsx b/packages/website/src/features/dashboard/dashboardNotFoundPage.tsx deleted file mode 100644 index 2d52a83e..00000000 --- a/packages/website/src/features/dashboard/dashboardNotFoundPage.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { ButtonOutlineContent } from "@arrhes/ui/components/buttons/buttonOutlineContent.js" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconHome } from "@tabler/icons-react" -import { LinkButton } from "../../components/linkButton.js" - -export function DashboardNotFoundPage() { - return ( -
-
- - 404 - -
-

- Page introuvable -

-

- La page que vous recherchez n'existe pas ou a été déplacée. -

-
-
- - } - /> - -
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/organizations/AddNewOrganization.tsx b/packages/website/src/features/dashboard/organizations/AddNewOrganization.tsx new file mode 100644 index 00000000..622e9f5f --- /dev/null +++ b/packages/website/src/features/dashboard/organizations/AddNewOrganization.tsx @@ -0,0 +1,194 @@ +import { + addNewOrganizationRouteDefinition, + getAllMyOrganizationsRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { Button, InputText, InputToggle, toast } from "@arrhes/ui" +import { css, cx } from "@arrhes/ui/utilities/cn.js" +import { IconPlus } from "@tabler/icons-react" +import type { JSX } from "react" +import { Fragment } from "react/jsx-runtime" +import { FormControl } from "../../../components/forms/FormControl.tsx" +import { FormError } from "../../../components/forms/FormError.tsx" +import { FormField } from "../../../components/forms/FormField.tsx" +import { FormItem } from "../../../components/forms/FormItem.tsx" +import { FormLabel } from "../../../components/forms/FormLabel.tsx" +import { FormRoot } from "../../../components/forms/FormRoot.tsx" +import { useTabs } from "../../../contexts/tabs/useTabs.tsx" +import { getResponseBodyFromAPI } from "../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../utilities/invalidateData.ts" + +export function AddNewOrganization(props: { children: JSX.Element; className?: string }) { + const { openPanelTab, closeTab } = useTabs() + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/organizations/OrganizationListTableRow.tsx b/packages/website/src/features/dashboard/organizations/OrganizationListTableRow.tsx new file mode 100644 index 00000000..4d730450 --- /dev/null +++ b/packages/website/src/features/dashboard/organizations/OrganizationListTableRow.tsx @@ -0,0 +1,107 @@ +import type { getAllMyOrganizationsRouteDefinition } from "@arrhes/application-metadata/routes" +import { Button, Chip, LinkContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import type * as v from "valibot" +import { ListTable } from "../../../components/layouts/listTable/listTable.tsx" +import { TabLink } from "../../../components/layouts/tabBar/TabLink.js" + +export function OrganizationListTableRow(props: { + organizationUser: v.InferOutput[number] +}) { + const organization = props.organizationUser.organization + + const scopeLabel = organization.scope === "company" ? "Entreprise" : "Association" + + return ( + +
+
+ + + +
+ {props.organizationUser.isAdmin && ( + + )} + {props.organizationUser.status === "invited" && ( + + )} +
+
+
+ + {organization.siren && ( + + SIREN: {organization.siren} + + )} +
+
+
+ ) +} diff --git a/packages/website/src/features/dashboard/organizations/OrganizationsListTable.tsx b/packages/website/src/features/dashboard/organizations/OrganizationsListTable.tsx new file mode 100644 index 00000000..65eca163 --- /dev/null +++ b/packages/website/src/features/dashboard/organizations/OrganizationsListTable.tsx @@ -0,0 +1,35 @@ +import { getAllMyOrganizationsRouteDefinition } from "@arrhes/application-metadata/routes" +import { IconBuilding } from "@tabler/icons-react" +import { DataWrapper } from "../../../components/layouts/DataWrapper.tsx" +import { EmptyState } from "../../../components/layouts/EmptyState.tsx" +import { ListTable } from "../../../components/layouts/listTable/listTable.tsx" +import { OrganizationListTableRow } from "./OrganizationListTableRow.tsx" + +export function OrganizationsListTable() { + return ( + + + {(organizationUsers) => { + if (organizationUsers.length === 0) { + return ( + } + title="Aucune organisation" + subtitle="Créez une organisation pour commencer" + /> + ) + } + return organizationUsers.map((organizationUser) => ( + + )) + }} + + + ) +} diff --git a/packages/website/src/features/dashboard/organizations/OrganizationsPage.tsx b/packages/website/src/features/dashboard/organizations/OrganizationsPage.tsx new file mode 100644 index 00000000..8124833c --- /dev/null +++ b/packages/website/src/features/dashboard/organizations/OrganizationsPage.tsx @@ -0,0 +1,36 @@ +import { ButtonOutlineContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconBuildingPlus } from "@tabler/icons-react" +import { Page } from "../../../components/layouts/page/page.tsx" +import { AddNewOrganization } from "./AddNewOrganization.tsx" +import { OrganizationsListTable } from "./OrganizationsListTable.tsx" + +export function OrganizationsPage() { + return ( + + {/* + + Mes organisations + + */} + +
+ + } + text="Créer une organisation" + /> + +
+ +
+
+ ) +} diff --git a/packages/website/src/features/dashboard/organizations/addNewOrganization.tsx b/packages/website/src/features/dashboard/organizations/addNewOrganization.tsx deleted file mode 100644 index 8ecfe228..00000000 --- a/packages/website/src/features/dashboard/organizations/addNewOrganization.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { - addNewOrganizationRouteDefinition, - getAllMyOrganizationsRouteDefinition, -} from "@arrhes/application-metadata/routes" -import { InputText, InputToggle, toast } from "@arrhes/ui" -import { IconPlus } from "@tabler/icons-react" -import { type JSX, useState } from "react" -import { Fragment } from "react/jsx-runtime" -import { FormControl } from "../../../components/forms/formControl.tsx" -import { FormError } from "../../../components/forms/formError.tsx" -import { FormField } from "../../../components/forms/formField.tsx" -import { FormItem } from "../../../components/forms/formItem.tsx" -import { FormLabel } from "../../../components/forms/formLabel.tsx" -import { FormRoot } from "../../../components/forms/formRoot.tsx" -import { Drawer } from "../../../components/overlays/drawer/drawer.tsx" -import { getResponseBodyFromAPI } from "../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../utilities/invalidateData.ts" - -export function AddNewOrganization(props: { children: JSX.Element }) { - const [open, setOpen] = useState(false) - - return ( - - {props.children} - - - - , - text: "Ajouter l'organisation", - }} - onSubmit={async (data) => { - const response = await getResponseBodyFromAPI({ - routeDefinition: addNewOrganizationRouteDefinition, - body: data, - }) - if (!response.ok) { - toast({ - title: "Impossible d'ajouter l'organisation", - variant: "error", - }) - return false - } - - toast({ - title: "Organisation ajoutée avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await invalidateData({ - routeDefinition: getAllMyOrganizationsRouteDefinition, - body: {}, - }) - - setOpen(false) - }} - > - {(form) => ( - - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - - )} - - - - - ) -} diff --git a/packages/website/src/features/dashboard/organizations/organizationListTableRow.tsx b/packages/website/src/features/dashboard/organizations/organizationListTableRow.tsx deleted file mode 100644 index 255101c8..00000000 --- a/packages/website/src/features/dashboard/organizations/organizationListTableRow.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import type { getAllMyOrganizationsRouteDefinition } from "@arrhes/application-metadata/routes" -import { Chip, LinkContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import type * as v from "valibot" -import { ListTable } from "../../../components/layouts/listTable/listTable.tsx" -import { LinkButton } from "../../../components/linkButton.tsx" - -export function OrganizationListTableRow(props: { - organizationUser: v.InferOutput[number] -}) { - const organization = props.organizationUser.organization - - const scopeLabel = organization.scope === "company" ? "Entreprise" : "Association" - - return ( - -
-
- - - {organization.name} - - -
- {props.organizationUser.isAdmin && ( - - )} - {props.organizationUser.status === "invited" && ( - - )} -
-
-
- - {organization.siren && ( - - SIREN: {organization.siren} - - )} -
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/organizations/organizationsListTable.tsx b/packages/website/src/features/dashboard/organizations/organizationsListTable.tsx deleted file mode 100644 index aed037fe..00000000 --- a/packages/website/src/features/dashboard/organizations/organizationsListTable.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { getAllMyOrganizationsRouteDefinition } from "@arrhes/application-metadata/routes" -import { IconBuilding } from "@tabler/icons-react" -import { DataWrapper } from "../../../components/layouts/dataWrapper.tsx" -import { EmptyState } from "../../../components/layouts/emptyState.tsx" -import { ListTable } from "../../../components/layouts/listTable/listTable.tsx" -import { OrganizationListTableRow } from "./organizationListTableRow.tsx" - -export function OrganizationsListTable() { - return ( - - - {(organizationUsers) => { - if (organizationUsers.length === 0) { - return ( - } - title="Aucune organisation" - subtitle="Créez une organisation pour commencer" - /> - ) - } - return organizationUsers.map((organizationUser) => ( - - )) - }} - - - ) -} diff --git a/packages/website/src/features/dashboard/organizations/organizationsPage.tsx b/packages/website/src/features/dashboard/organizations/organizationsPage.tsx deleted file mode 100644 index 78cf69ae..00000000 --- a/packages/website/src/features/dashboard/organizations/organizationsPage.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { ButtonOutlineContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconBuildingPlus } from "@tabler/icons-react" -import { Page } from "../../../components/layouts/page/page.tsx" -import { AddNewOrganization } from "./addNewOrganization.tsx" -import { OrganizationsListTable } from "./organizationsListTable.tsx" - -export function OrganizationsPage() { - return ( - - {/* - - Mes organisations - - */} - -
- - } - text="Créer une organisation" - /> - -
- -
-
- ) -} diff --git a/packages/website/src/features/dashboard/profile/DeleteUser.tsx b/packages/website/src/features/dashboard/profile/DeleteUser.tsx new file mode 100644 index 00000000..dcb13609 --- /dev/null +++ b/packages/website/src/features/dashboard/profile/DeleteUser.tsx @@ -0,0 +1,110 @@ +import { deleteUserRouteDefinition } from "@arrhes/application-metadata/routes" +import { Button, InputPassword, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconTrash } from "@tabler/icons-react" +import type { JSX } from "react" +import { FormControl } from "../../../components/forms/FormControl.tsx" +import { FormError } from "../../../components/forms/FormError.tsx" +import { FormField } from "../../../components/forms/FormField.tsx" +import { FormItem } from "../../../components/forms/FormItem.tsx" +import { FormLabel } from "../../../components/forms/FormLabel.tsx" +import { FormRoot } from "../../../components/forms/FormRoot.tsx" +import { useTabs } from "../../../contexts/tabs/useTabs.tsx" +import { applicationRouter } from "../../../routes/applicationRouter.tsx" +import { deleteCookies } from "../../../utilities/cookies/deleteCookies.ts" +import { getResponseBodyFromAPI } from "../../../utilities/getResponseBodyFromAPI.ts" + +export function DeleteUser(props: { children: JSX.Element }) { + const { openPanelTab } = useTabs() + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/profile/UpdateUserEmail.tsx b/packages/website/src/features/dashboard/profile/UpdateUserEmail.tsx new file mode 100644 index 00000000..02ecec2e --- /dev/null +++ b/packages/website/src/features/dashboard/profile/UpdateUserEmail.tsx @@ -0,0 +1,132 @@ +import { readUserSessionRouteDefinition, updateUserEmailRouteDefinition } from "@arrhes/application-metadata/routes" +import { Button, InputPassword, InputText, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconDeviceFloppy } from "@tabler/icons-react" +import type { JSX } from "react" +import { Fragment } from "react/jsx-runtime" +import { FormControl } from "../../../components/forms/FormControl.tsx" +import { FormError } from "../../../components/forms/FormError.tsx" +import { FormField } from "../../../components/forms/FormField.tsx" +import { FormItem } from "../../../components/forms/FormItem.tsx" +import { FormLabel } from "../../../components/forms/FormLabel.tsx" +import { FormRoot } from "../../../components/forms/FormRoot.tsx" +import { useTabs } from "../../../contexts/tabs/useTabs.tsx" +import { getResponseBodyFromAPI } from "../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../utilities/invalidateData.ts" + +export function UpdateUserEmail(props: { children: JSX.Element }) { + const { openPanelTab, closeTab } = useTabs() + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/profile/UpdateUserPassword.tsx b/packages/website/src/features/dashboard/profile/UpdateUserPassword.tsx new file mode 100644 index 00000000..fc4a38e6 --- /dev/null +++ b/packages/website/src/features/dashboard/profile/UpdateUserPassword.tsx @@ -0,0 +1,144 @@ +import { updateUserPasswordRouteDefinition } from "@arrhes/application-metadata/routes" +import { Button, InputPassword, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconDeviceFloppy } from "@tabler/icons-react" +import type { JSX } from "react" +import { Fragment } from "react/jsx-runtime" +import { FormControl } from "../../../components/forms/FormControl.tsx" +import { FormError } from "../../../components/forms/FormError.tsx" +import { FormField } from "../../../components/forms/FormField.tsx" +import { FormItem } from "../../../components/forms/FormItem.tsx" +import { FormLabel } from "../../../components/forms/FormLabel.tsx" +import { FormRoot } from "../../../components/forms/FormRoot.tsx" +import { useTabs } from "../../../contexts/tabs/useTabs.tsx" +import { getResponseBodyFromAPI } from "../../../utilities/getResponseBodyFromAPI.ts" + +export function UpdateUserPassword(props: { children: JSX.Element }) { + const { openPanelTab, closeTab } = useTabs() + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/profile/UserProfilePage.tsx b/packages/website/src/features/dashboard/profile/UserProfilePage.tsx new file mode 100644 index 00000000..21d53235 --- /dev/null +++ b/packages/website/src/features/dashboard/profile/UserProfilePage.tsx @@ -0,0 +1,85 @@ +import { readUserSessionRouteDefinition } from "@arrhes/application-metadata/routes" +import { Button, ButtonOutlineContent } from "@arrhes/ui" +import { IconKey, IconMail, IconTrash } from "@tabler/icons-react" +import { DataWrapper } from "../../../components/layouts/DataWrapper.tsx" +import { Page } from "../../../components/layouts/page/page.js" +import { SettingsSection } from "../../../components/layouts/settingsSection/settingsSection.tsx" +import { DeleteUser } from "./DeleteUser.tsx" +import { UpdateUserEmail } from "./UpdateUserEmail.tsx" +import { UpdateUserPassword } from "./UpdateUserPassword.tsx" +import { ValidateUserEmail } from "./ValidateUserEmail.tsx" + +export function UserProfilePage() { + return ( + + + Mon compte + Modifiez votre adresse email ou votre mot de passe. + + + + {(userSession) => ( + <> + + + + + + + + {userSession.user.emailToValidate && ( + + )} + + + + + + + + + + + + + + + + )} + + + + ) +} diff --git a/packages/website/src/features/dashboard/profile/validateUserEmail.tsx b/packages/website/src/features/dashboard/profile/ValidateUserEmail.tsx similarity index 100% rename from packages/website/src/features/dashboard/profile/validateUserEmail.tsx rename to packages/website/src/features/dashboard/profile/ValidateUserEmail.tsx diff --git a/packages/website/src/features/dashboard/profile/deleteUser.tsx b/packages/website/src/features/dashboard/profile/deleteUser.tsx deleted file mode 100644 index aea456c6..00000000 --- a/packages/website/src/features/dashboard/profile/deleteUser.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { deleteUserRouteDefinition } from "@arrhes/application-metadata/routes" -import { InputPassword, toast } from "@arrhes/ui" -import { IconTrash } from "@tabler/icons-react" -import { type JSX, useState } from "react" -import { FormControl } from "../../../components/forms/formControl.tsx" -import { FormError } from "../../../components/forms/formError.tsx" -import { FormField } from "../../../components/forms/formField.tsx" -import { FormItem } from "../../../components/forms/formItem.tsx" -import { FormLabel } from "../../../components/forms/formLabel.tsx" -import { FormRoot } from "../../../components/forms/formRoot.tsx" -import { Drawer } from "../../../components/overlays/drawer/drawer.tsx" -import { applicationRouter } from "../../../routes/applicationRouter.tsx" -import { deleteCookies } from "../../../utilities/cookies/deleteCookies.ts" -import { getResponseBodyFromAPI } from "../../../utilities/getResponseBodyFromAPI.ts" - -export function DeleteUser(props: { children: JSX.Element }) { - const [open, setOpen] = useState(false) - - return ( - - {props.children} - - - - , - text: "Supprimer mon compte", - color: "danger", - }} - onSubmit={async (data) => { - const response = await getResponseBodyFromAPI({ - routeDefinition: deleteUserRouteDefinition, - body: data, - }) - if (response.ok === false) { - toast({ - title: response.error?.cause ?? "Impossible de supprimer le compte", - variant: "error", - }) - return false - } - toast({ - title: "Compte supprimé", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={() => { - deleteCookies() - applicationRouter.navigate({ - to: "/connexion", - reloadDocument: true, - }) - }} - > - {(form) => ( - ( - - - - - - - - )} - /> - )} - - - - - ) -} diff --git a/packages/website/src/features/dashboard/profile/updateUserEmail.tsx b/packages/website/src/features/dashboard/profile/updateUserEmail.tsx deleted file mode 100644 index 91c0f739..00000000 --- a/packages/website/src/features/dashboard/profile/updateUserEmail.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { readUserSessionRouteDefinition, updateUserEmailRouteDefinition } from "@arrhes/application-metadata/routes" -import { InputPassword, InputText, toast } from "@arrhes/ui" -import { IconDeviceFloppy } from "@tabler/icons-react" -import { type JSX, useState } from "react" -import { Fragment } from "react/jsx-runtime" -import { FormControl } from "../../../components/forms/formControl.tsx" -import { FormError } from "../../../components/forms/formError.tsx" -import { FormField } from "../../../components/forms/formField.tsx" -import { FormItem } from "../../../components/forms/formItem.tsx" -import { FormLabel } from "../../../components/forms/formLabel.tsx" -import { FormRoot } from "../../../components/forms/formRoot.tsx" -import { Drawer } from "../../../components/overlays/drawer/drawer.tsx" -import { getResponseBodyFromAPI } from "../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../utilities/invalidateData.ts" - -export function UpdateUserEmail(props: { children: JSX.Element }) { - const [open, setOpen] = useState(false) - - return ( - - {props.children} - - - - , - text: "Mettre à jour l'email", - }} - onSubmit={async (data) => { - const response = await getResponseBodyFromAPI({ - routeDefinition: updateUserEmailRouteDefinition, - body: data, - }) - if (response.ok === false) { - toast({ - title: response.error?.cause ?? "Impossible de mettre à jour l'email", - variant: "error", - }) - return false - } - toast({ - title: "Un email de vérification a été envoyé à la nouvelle adresse", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={() => { - invalidateData({ - routeDefinition: readUserSessionRouteDefinition, - body: {}, - }) - setOpen(false) - }} - resetOnSubmit - > - {(form) => ( - - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - - )} - - - - - ) -} diff --git a/packages/website/src/features/dashboard/profile/updateUserPassword.tsx b/packages/website/src/features/dashboard/profile/updateUserPassword.tsx deleted file mode 100644 index 740277cb..00000000 --- a/packages/website/src/features/dashboard/profile/updateUserPassword.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { updateUserPasswordRouteDefinition } from "@arrhes/application-metadata/routes" -import { InputPassword, toast } from "@arrhes/ui" -import { IconDeviceFloppy } from "@tabler/icons-react" -import { type JSX, useState } from "react" -import { Fragment } from "react/jsx-runtime" -import { FormControl } from "../../../components/forms/formControl.tsx" -import { FormError } from "../../../components/forms/formError.tsx" -import { FormField } from "../../../components/forms/formField.tsx" -import { FormItem } from "../../../components/forms/formItem.tsx" -import { FormLabel } from "../../../components/forms/formLabel.tsx" -import { FormRoot } from "../../../components/forms/formRoot.tsx" -import { Drawer } from "../../../components/overlays/drawer/drawer.tsx" -import { getResponseBodyFromAPI } from "../../../utilities/getResponseBodyFromAPI.ts" - -export function UpdateUserPassword(props: { children: JSX.Element }) { - const [open, setOpen] = useState(false) - - return ( - - {props.children} - - - - , - text: "Mettre à jour le mot de passe", - }} - onSubmit={async (data) => { - const response = await getResponseBodyFromAPI({ - routeDefinition: updateUserPasswordRouteDefinition, - body: data, - }) - if (response.ok === false) { - toast({ - title: response.error?.cause ?? "Impossible de mettre à jour le mot de passe", - variant: "error", - }) - return false - } - toast({ - title: "Mot de passe mis à jour avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={() => setOpen(false)} - resetOnSubmit - > - {(form) => ( - - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - - )} - - - - - ) -} diff --git a/packages/website/src/features/dashboard/profile/userProfilePage.tsx b/packages/website/src/features/dashboard/profile/userProfilePage.tsx deleted file mode 100644 index 1febdd47..00000000 --- a/packages/website/src/features/dashboard/profile/userProfilePage.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { readUserSessionRouteDefinition } from "@arrhes/application-metadata/routes" -import { Button, ButtonOutlineContent } from "@arrhes/ui" -import { IconKey, IconMail, IconTrash } from "@tabler/icons-react" -import { DataWrapper } from "../../../components/layouts/dataWrapper.tsx" -import { Page } from "../../../components/layouts/page/page.js" -import { SettingsSection } from "../../../components/layouts/settingsSection/settingsSection.tsx" -import { DeleteUser } from "./deleteUser.tsx" -import { UpdateUserEmail } from "./updateUserEmail.tsx" -import { UpdateUserPassword } from "./updateUserPassword.tsx" -import { ValidateUserEmail } from "./validateUserEmail.tsx" - -export function UserProfilePage() { - return ( - - - Mon compte - Modifiez votre adresse email ou votre mot de passe. - - - - {(userSession) => ( - <> - - - - - - - - {userSession.user.emailToValidate && ( - - )} - - - - - - - - - - - - - - - - )} - - - - ) -} diff --git a/packages/website/src/features/dashboard/settings/settingsPage.tsx b/packages/website/src/features/dashboard/settings/SettingsPage.tsx similarity index 100% rename from packages/website/src/features/dashboard/settings/settingsPage.tsx rename to packages/website/src/features/dashboard/settings/SettingsPage.tsx diff --git a/packages/website/src/features/dashboard/support/$idTicket/CreateOneTicketMessage.tsx b/packages/website/src/features/dashboard/support/$idTicket/CreateOneTicketMessage.tsx new file mode 100644 index 00000000..358cd7be --- /dev/null +++ b/packages/website/src/features/dashboard/support/$idTicket/CreateOneTicketMessage.tsx @@ -0,0 +1,91 @@ +import { + createOneTicketMessageRouteDefinition, + readAllTicketMessagesRouteDefinition, +} from "@arrhes/application-metadata/routes" +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { InputTextArea, toast } from "@arrhes/ui" +import { IconSend } from "@tabler/icons-react" +import { Fragment } from "react/jsx-runtime" +import type * as v from "valibot" +import { FormControl } from "../../../../components/forms/FormControl.tsx" +import { FormError } from "../../../../components/forms/FormError.tsx" +import { FormField } from "../../../../components/forms/FormField.tsx" +import { FormItem } from "../../../../components/forms/FormItem.tsx" +import { FormLabel } from "../../../../components/forms/FormLabel.tsx" +import { FormRoot } from "../../../../components/forms/FormRoot.tsx" +import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../../utilities/invalidateData.ts" + +export function CreateOneTicketMessage(props: { idTicket: v.InferOutput["id"] }) { + return ( + , + text: "Envoyer", + }} + maxWidth="100%" + alignSubmitButton="end" + submitOnPressEnterKey={false} + resetOnSubmit={true} + onSubmit={async (data) => { + const response = await getResponseBodyFromAPI({ + routeDefinition: createOneTicketMessageRouteDefinition, + body: data, + }) + if (response.ok === false) { + toast({ + title: "Impossible d'envoyer le message", + variant: "error", + }) + return false + } + + toast({ + title: "Message envoyé", + variant: "success", + }) + return true + }} + onCancel={undefined} + onSuccess={async () => { + await invalidateData({ + routeDefinition: readAllTicketMessagesRouteDefinition, + body: { + idTicket: props.idTicket, + }, + }) + }} + > + {(form) => ( + + ( + + + + + + + + )} + /> + + )} + + ) +} diff --git a/packages/website/src/features/dashboard/support/$idTicket/statusToggle.tsx b/packages/website/src/features/dashboard/support/$idTicket/StatusToggle.tsx similarity index 100% rename from packages/website/src/features/dashboard/support/$idTicket/statusToggle.tsx rename to packages/website/src/features/dashboard/support/$idTicket/StatusToggle.tsx diff --git a/packages/website/src/features/dashboard/support/$idTicket/TicketLayout.tsx b/packages/website/src/features/dashboard/support/$idTicket/TicketLayout.tsx new file mode 100644 index 00000000..c74116ae --- /dev/null +++ b/packages/website/src/features/dashboard/support/$idTicket/TicketLayout.tsx @@ -0,0 +1,69 @@ +import { readOneTicketRouteDefinition } from "@arrhes/application-metadata/routes" +import { ButtonOutlineContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconChevronLeft } from "@tabler/icons-react" +import { Outlet, useParams } from "@tanstack/react-router" +import { LinkButton } from "../../../../components/LinkButton.tsx" +import { DataWrapper } from "../../../../components/layouts/DataWrapper.tsx" +import { Page } from "../../../../components/layouts/page/page.tsx" +import { Section } from "../../../../components/layouts/section/section.tsx" + +import { StatusToggle } from "./StatusToggle.tsx" + +export function TicketLayout() { + const params = useParams({ + strict: false, + }) as { + idTicket: string + } + + return ( + + + + + + {(ticket) => { + return ( +
+ + } + text="Retour" + /> + + +
+ ) + }} +
+
+
+ +
+
+ ) +} diff --git a/packages/website/src/features/dashboard/support/$idTicket/ticketMessageList.tsx b/packages/website/src/features/dashboard/support/$idTicket/TicketMessageList.tsx similarity index 100% rename from packages/website/src/features/dashboard/support/$idTicket/ticketMessageList.tsx rename to packages/website/src/features/dashboard/support/$idTicket/TicketMessageList.tsx diff --git a/packages/website/src/features/dashboard/support/$idTicket/TicketPage.tsx b/packages/website/src/features/dashboard/support/$idTicket/TicketPage.tsx new file mode 100644 index 00000000..7348d32d --- /dev/null +++ b/packages/website/src/features/dashboard/support/$idTicket/TicketPage.tsx @@ -0,0 +1,253 @@ +import { readAllTicketMessagesRouteDefinition, readOneTicketRouteDefinition } from "@arrhes/application-metadata/routes" +import { Chip, formatDate, Separator } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { useParams } from "@tanstack/react-router" +import { Banner } from "../../../../components/layouts/Banner.tsx" +import { DataWrapper } from "../../../../components/layouts/DataWrapper.tsx" +import { Section } from "../../../../components/layouts/section/section.tsx" +import { CreateOneTicketMessage } from "./CreateOneTicketMessage.tsx" +import { TicketMessageList } from "./TicketMessageList.tsx" + +const categoryLabels: Record = { + bug: "Erreur", + enhancement: "Amélioration", + feature: "Fonctionnalité", + other: "Autre", +} + +const statusLabels: Record< + string, + { + text: string + color: "success" | "neutral" + } +> = { + open: { + text: "Ouvert", + color: "success", + }, + closed: { + text: "Fermé", + color: "neutral", + }, +} + +export function TicketPage(props: { idTicket?: string } = {}) { + const params = useParams({ + strict: false, + }) + const idTicket = props.idTicket ?? params.idTicket ?? "" + + return ( + + + {(ticket) => { + const status = statusLabels[ticket.status] ?? { + text: ticket.status, + color: "neutral", + } + const categoryLabel = categoryLabels[ticket.category] ?? ticket.category + + return ( +
+
+
+

+ Ticket +

+ + {ticket.id} + +
+ +
+ +
+ + Catégorie :{" "} + + + {categoryLabel} + +
+ + {/* Dates */} + + {`Créé le ${formatDate(ticket.createdAt, { + includeTime: true, + })}`} + + + {ticket.lastUpdatedAt ? ( + + {`Mis à jour le ${formatDate(ticket.lastUpdatedAt, { + includeTime: true, + })}`} + + ) : null} + + + + {/* Last message info + close/reopen */} + + {(messages) => { + const sorted = [ + ...messages, + ].sort((a, b) => b.createdAt.localeCompare(a.createdAt)) + const lastMessage = sorted[0] + + return ( +
+ {lastMessage ? ( + + {lastMessage.idAdminUser !== null + ? "Dernier message : Support (en attente de votre réponse)" + : "Dernier message : Vous (en attente d'une réponse)"} + + ) : ( + + Aucun message + + )} +
+ ) + }} +
+
+ ) + }} +
+ + {/* Reply form — only when open */} + + {(ticket) => + ticket.status === "open" ? ( + + ) : ( + + Ce ticket est fermé. Ouvrez-le de nouveau pour envoyer un message. + + ) + } + + + {/* Messages */} + + {(messages) => } + +
+ ) +} diff --git a/packages/website/src/features/dashboard/support/$idTicket/TicketTabContent.tsx b/packages/website/src/features/dashboard/support/$idTicket/TicketTabContent.tsx new file mode 100644 index 00000000..17f35820 --- /dev/null +++ b/packages/website/src/features/dashboard/support/$idTicket/TicketTabContent.tsx @@ -0,0 +1,48 @@ +import { readOneTicketRouteDefinition } from "@arrhes/application-metadata/routes" +import { css } from "@arrhes/ui/utilities/cn.js" +import { DataWrapper } from "../../../../components/layouts/DataWrapper.tsx" +import { Page } from "../../../../components/layouts/page/page.tsx" +import { Section } from "../../../../components/layouts/section/section.tsx" +import { StatusToggle } from "./StatusToggle.tsx" +import { TicketPage } from "./TicketPage.tsx" + +export function TicketTabContent(props: { idTicket: string }) { + return ( + + + + + + {(ticket) => ( +
+ +
+ )} +
+
+
+ +
+
+ ) +} diff --git a/packages/website/src/features/dashboard/support/$idTicket/createOneTicketMessage.tsx b/packages/website/src/features/dashboard/support/$idTicket/createOneTicketMessage.tsx deleted file mode 100644 index 1c986e15..00000000 --- a/packages/website/src/features/dashboard/support/$idTicket/createOneTicketMessage.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { - createOneTicketMessageRouteDefinition, - readAllTicketMessagesRouteDefinition, -} from "@arrhes/application-metadata/routes" -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { InputTextArea, toast } from "@arrhes/ui" -import { IconSend } from "@tabler/icons-react" -import { Fragment } from "react/jsx-runtime" -import type * as v from "valibot" -import { FormControl } from "../../../../components/forms/formControl.tsx" -import { FormError } from "../../../../components/forms/formError.tsx" -import { FormField } from "../../../../components/forms/formField.tsx" -import { FormItem } from "../../../../components/forms/formItem.tsx" -import { FormLabel } from "../../../../components/forms/formLabel.tsx" -import { FormRoot } from "../../../../components/forms/formRoot.tsx" -import { getResponseBodyFromAPI } from "../../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../../utilities/invalidateData.ts" - -export function CreateOneTicketMessage(props: { idTicket: v.InferOutput["id"] }) { - return ( - , - text: "Envoyer", - }} - maxWidth="100%" - alignSubmitButton="end" - submitOnPressEnterKey={false} - resetOnSubmit={true} - onSubmit={async (data) => { - const response = await getResponseBodyFromAPI({ - routeDefinition: createOneTicketMessageRouteDefinition, - body: data, - }) - if (response.ok === false) { - toast({ - title: "Impossible d'envoyer le message", - variant: "error", - }) - return false - } - - toast({ - title: "Message envoyé", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await invalidateData({ - routeDefinition: readAllTicketMessagesRouteDefinition, - body: { - idTicket: props.idTicket, - }, - }) - }} - > - {(form) => ( - - ( - - - - - - - - )} - /> - - )} - - ) -} diff --git a/packages/website/src/features/dashboard/support/$idTicket/ticketLayout.tsx b/packages/website/src/features/dashboard/support/$idTicket/ticketLayout.tsx deleted file mode 100644 index 9f4ca7ce..00000000 --- a/packages/website/src/features/dashboard/support/$idTicket/ticketLayout.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { readOneTicketRouteDefinition } from "@arrhes/application-metadata/routes" -import { ButtonOutlineContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconChevronLeft } from "@tabler/icons-react" -import { Outlet, useParams } from "@tanstack/react-router" -import { DataWrapper } from "../../../../components/layouts/dataWrapper.tsx" -import { Page } from "../../../../components/layouts/page/page.tsx" -import { Section } from "../../../../components/layouts/section/section.tsx" -import { LinkButton } from "../../../../components/linkButton.tsx" -import { ticketLayoutRoute } from "../../../../routes/root/dashboard/support/ticketLayoutRoute.tsx" -import { StatusToggle } from "./statusToggle.tsx" - -export function TicketLayout() { - const params = useParams({ - from: ticketLayoutRoute.id, - }) - - return ( - - - - - - {(ticket) => { - return ( -
- - } - text="Retour" - /> - - -
- ) - }} -
-
-
- -
-
- ) -} diff --git a/packages/website/src/features/dashboard/support/$idTicket/ticketPage.tsx b/packages/website/src/features/dashboard/support/$idTicket/ticketPage.tsx deleted file mode 100644 index 25cbd54e..00000000 --- a/packages/website/src/features/dashboard/support/$idTicket/ticketPage.tsx +++ /dev/null @@ -1,253 +0,0 @@ -import { readAllTicketMessagesRouteDefinition, readOneTicketRouteDefinition } from "@arrhes/application-metadata/routes" -import { Chip, formatDate, Separator } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { useParams } from "@tanstack/react-router" -import { Banner } from "../../../../components/layouts/banner.tsx" -import { DataWrapper } from "../../../../components/layouts/dataWrapper.tsx" -import { Section } from "../../../../components/layouts/section/section.tsx" -import { ticketLayoutRoute } from "../../../../routes/root/dashboard/support/ticketLayoutRoute.tsx" -import { CreateOneTicketMessage } from "./createOneTicketMessage.tsx" -import { TicketMessageList } from "./ticketMessageList.tsx" - -const categoryLabels: Record = { - bug: "Erreur", - enhancement: "Amélioration", - feature: "Fonctionnalité", - other: "Autre", -} - -const statusLabels: Record< - string, - { - text: string - color: "success" | "neutral" - } -> = { - open: { - text: "Ouvert", - color: "success", - }, - closed: { - text: "Fermé", - color: "neutral", - }, -} - -export function TicketPage() { - const params = useParams({ - from: ticketLayoutRoute.id, - }) - - return ( - - - {(ticket) => { - const status = statusLabels[ticket.status] ?? { - text: ticket.status, - color: "neutral", - } - const categoryLabel = categoryLabels[ticket.category] ?? ticket.category - - return ( -
-
-
-

- Ticket -

- - {ticket.id} - -
- -
- -
- - Catégorie :{" "} - - - {categoryLabel} - -
- - {/* Dates */} - - {`Créé le ${formatDate(ticket.createdAt, { - includeTime: true, - })}`} - - - {ticket.lastUpdatedAt ? ( - - {`Mis à jour le ${formatDate(ticket.lastUpdatedAt, { - includeTime: true, - })}`} - - ) : null} - - - - {/* Last message info + close/reopen */} - - {(messages) => { - const sorted = [ - ...messages, - ].sort((a, b) => b.createdAt.localeCompare(a.createdAt)) - const lastMessage = sorted[0] - - return ( -
- {lastMessage ? ( - - {lastMessage.idAdminUser !== null - ? "Dernier message : Support (en attente de votre réponse)" - : "Dernier message : Vous (en attente d'une réponse)"} - - ) : ( - - Aucun message - - )} -
- ) - }} -
-
- ) - }} -
- - {/* Reply form — only when open */} - - {(ticket) => - ticket.status === "open" ? ( - - ) : ( - - Ce ticket est fermé. Ouvrez-le de nouveau pour envoyer un message. - - ) - } - - - {/* Messages */} - - {(messages) => } - -
- ) -} diff --git a/packages/website/src/features/dashboard/support/CreateOneTicket.tsx b/packages/website/src/features/dashboard/support/CreateOneTicket.tsx new file mode 100644 index 00000000..80f4e48d --- /dev/null +++ b/packages/website/src/features/dashboard/support/CreateOneTicket.tsx @@ -0,0 +1,165 @@ +import { createOneTicketRouteDefinition, readAllTicketsRouteDefinition } from "@arrhes/application-metadata/routes" +import { Button, InputTextArea, InputToggle, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconPlus } from "@tabler/icons-react" +import { type JSX, useRef } from "react" +import { Fragment } from "react/jsx-runtime" +import { FormControl } from "../../../components/forms/FormControl.tsx" +import { FormError } from "../../../components/forms/FormError.tsx" +import { FormField } from "../../../components/forms/FormField.tsx" +import { FormItem } from "../../../components/forms/FormItem.tsx" +import { FormLabel } from "../../../components/forms/FormLabel.tsx" +import { FormRoot } from "../../../components/forms/FormRoot.tsx" +import { useTabs } from "../../../contexts/tabs/useTabs.tsx" +import { applicationRouter } from "../../../routes/applicationRouter.tsx" +import { getResponseBodyFromAPI } from "../../../utilities/getResponseBodyFromAPI.ts" +import { invalidateData } from "../../../utilities/invalidateData.ts" + +export function CreateOneTicket(props: { children: JSX.Element }) { + const { openPanelTab, closeTab } = useTabs() + const createdTicketIdRef = useRef(undefined) + + return ( + + ) +} diff --git a/packages/website/src/features/dashboard/support/SupportPage.tsx b/packages/website/src/features/dashboard/support/SupportPage.tsx new file mode 100644 index 00000000..1d9e2807 --- /dev/null +++ b/packages/website/src/features/dashboard/support/SupportPage.tsx @@ -0,0 +1,113 @@ +import { readOrganizationBillingRouteDefinition } from "@arrhes/application-metadata/routes" +import { ButtonOutlineContent, ButtonPlainContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconBrandGithub, IconBrandLinkedin, IconMail, IconPlus } from "@tabler/icons-react" +import { Banner } from "../../../components/layouts/Banner.tsx" +import { Box } from "../../../components/layouts/Box.tsx" +import { Page } from "../../../components/layouts/page/page.tsx" +import { Section } from "../../../components/layouts/section/section.tsx" +import { getCookie } from "../../../utilities/cookies/getCookie.ts" +import { useDataFromAPI } from "../../../utilities/useHTTPData.ts" +import { cookiePrefix } from "../../../utilities/variables.ts" +import { CreateOneTicket } from "./CreateOneTicket.tsx" +import { TicketsListTable } from "./TicketsListTable.tsx" + +function SupportBanner() { + const hasOrganization = !!getCookie(`${cookiePrefix}_id_organization`) + + const subscription = useDataFromAPI({ + routeDefinition: readOrganizationBillingRouteDefinition, + body: {}, + enabled: hasOrganization, + }) + + if (!hasOrganization || subscription.isPending || subscription.isError) { + return null + } + + if ((subscription.data?.licenceAmount ?? 0) > 0) { + return ( + + Vos tickets sont traités en priorité grâce à votre licence active. + + ) + } + + return ( + + Activez une licence pour bénéficier d'un traitement prioritaire de vos tickets. + + ) +} + +export function SupportPage() { + return ( + + + + + +
+ + } + text="Créer un ticket" + /> + +
+ +
+
+ + Vous pouvez aussi nous contacter directement via + + +
+
+ ) +} diff --git a/packages/website/src/features/dashboard/support/TicketListTableRow.tsx b/packages/website/src/features/dashboard/support/TicketListTableRow.tsx new file mode 100644 index 00000000..c5cfc0b2 --- /dev/null +++ b/packages/website/src/features/dashboard/support/TicketListTableRow.tsx @@ -0,0 +1,128 @@ +import type { returnedSchemas } from "@arrhes/application-metadata/schemas" +import { Button, Chip, formatDate, LinkContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import type * as v from "valibot" +import { ListTable } from "../../../components/layouts/listTable/listTable.tsx" +import { TabLink } from "../../../components/layouts/tabBar/TabLink.js" + +const categoryLabels: Record = { + bug: "Erreur", + enhancement: "Amélioration", + feature: "Fonctionnalité", + other: "Autre", +} + +const statusLabels: Record< + string, + { + text: string + color: "information" | "neutral" + } +> = { + open: { + text: "Ouvert", + color: "information", + }, + closed: { + text: "Fermé", + color: "neutral", + }, +} + +export function TicketListTableRow(props: { ticket: v.InferOutput }) { + const createdAt = formatDate(props.ticket.createdAt) + const status = statusLabels[props.ticket.status] ?? { + text: props.ticket.status, + color: "neutral" as const, + } + const categoryLabel = categoryLabels[props.ticket.category] ?? props.ticket.category + + return ( + +
+
+
+ + + +
+
+ + +
+
+
+ {`Créé le ${createdAt}`} +
+
+
+ ) +} diff --git a/packages/website/src/features/dashboard/support/TicketsListTable.tsx b/packages/website/src/features/dashboard/support/TicketsListTable.tsx new file mode 100644 index 00000000..097d908e --- /dev/null +++ b/packages/website/src/features/dashboard/support/TicketsListTable.tsx @@ -0,0 +1,37 @@ +import { readAllTicketsRouteDefinition } from "@arrhes/application-metadata/routes" +import { IconTicket } from "@tabler/icons-react" +import { DataWrapper } from "../../../components/layouts/DataWrapper.tsx" +import { EmptyState } from "../../../components/layouts/EmptyState.tsx" +import { ListTable } from "../../../components/layouts/listTable/listTable.tsx" +import { TicketListTableRow } from "./TicketListTableRow.tsx" + +export function TicketsListTable() { + return ( + + + {(tickets) => { + const sortedTickets = tickets.sort((a, b) => b.createdAt.localeCompare(a.createdAt)) + + if (sortedTickets.length === 0) { + return ( + } + title="Aucun ticket" + subtitle="Créez un ticket pour contacter le support" + /> + ) + } + return sortedTickets.map((ticket) => ( + + )) + }} + + + ) +} diff --git a/packages/website/src/features/dashboard/support/createOneTicket.tsx b/packages/website/src/features/dashboard/support/createOneTicket.tsx deleted file mode 100644 index d3ba8778..00000000 --- a/packages/website/src/features/dashboard/support/createOneTicket.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { createOneTicketRouteDefinition, readAllTicketsRouteDefinition } from "@arrhes/application-metadata/routes" -import { InputTextArea, InputToggle, toast } from "@arrhes/ui" -import { IconPlus } from "@tabler/icons-react" -import { type JSX, useRef, useState } from "react" -import { Fragment } from "react/jsx-runtime" -import { FormControl } from "../../../components/forms/formControl.tsx" -import { FormError } from "../../../components/forms/formError.tsx" -import { FormField } from "../../../components/forms/formField.tsx" -import { FormItem } from "../../../components/forms/formItem.tsx" -import { FormLabel } from "../../../components/forms/formLabel.tsx" -import { FormRoot } from "../../../components/forms/formRoot.tsx" -import { Drawer } from "../../../components/overlays/drawer/drawer.tsx" -import { applicationRouter } from "../../../routes/applicationRouter.tsx" -import { getResponseBodyFromAPI } from "../../../utilities/getResponseBodyFromAPI.ts" -import { invalidateData } from "../../../utilities/invalidateData.ts" - -export function CreateOneTicket(props: { children: JSX.Element }) { - const [open, setOpen] = useState(false) - const createdTicketIdRef = useRef(undefined) - - return ( - - {props.children} - - - - , - text: "Créer le ticket", - }} - onSubmit={async (data) => { - const response = await getResponseBodyFromAPI({ - routeDefinition: createOneTicketRouteDefinition, - body: data, - }) - if (response.ok === false) { - toast({ - title: "Impossible de créer le ticket", - variant: "error", - }) - return false - } - - createdTicketIdRef.current = response.data.id - toast({ - title: "Ticket créé avec succès", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={async () => { - await invalidateData({ - routeDefinition: readAllTicketsRouteDefinition, - body: {}, - }) - - setOpen(false) - - if (createdTicketIdRef.current !== undefined) { - applicationRouter.navigate({ - to: "/dashboard/support/tickets/$idTicket", - params: { - idTicket: createdTicketIdRef.current, - }, - }) - } - }} - > - {(form) => ( - - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - - )} - - - - - ) -} diff --git a/packages/website/src/features/dashboard/support/supportPage.tsx b/packages/website/src/features/dashboard/support/supportPage.tsx deleted file mode 100644 index 54b129aa..00000000 --- a/packages/website/src/features/dashboard/support/supportPage.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { readOrganizationBillingRouteDefinition } from "@arrhes/application-metadata/routes" -import { ButtonOutlineContent, ButtonPlainContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconBrandGithub, IconBrandLinkedin, IconMail, IconPlus } from "@tabler/icons-react" -import { Banner } from "../../../components/layouts/banner.tsx" -import { Box } from "../../../components/layouts/box.tsx" -import { Page } from "../../../components/layouts/page/page.tsx" -import { Section } from "../../../components/layouts/section/section.tsx" -import { getCookie } from "../../../utilities/cookies/getCookie.ts" -import { useDataFromAPI } from "../../../utilities/useHTTPData.ts" -import { cookiePrefix } from "../../../utilities/variables.ts" -import { CreateOneTicket } from "./createOneTicket.tsx" -import { TicketsListTable } from "./ticketsListTable.tsx" - -function SupportBanner() { - const hasOrganization = !!getCookie(`${cookiePrefix}_id_organization`) - - const subscription = useDataFromAPI({ - routeDefinition: readOrganizationBillingRouteDefinition, - body: {}, - enabled: hasOrganization, - }) - - if (!hasOrganization || subscription.isPending || subscription.isError) { - return null - } - - if ((subscription.data?.licenceAmount ?? 0) > 0) { - return ( - - Vos tickets sont traités en priorité grâce à votre licence active. - - ) - } - - return ( - - Activez une licence pour bénéficier d'un traitement prioritaire de vos tickets. - - ) -} - -export function SupportPage() { - return ( - - - - - -
- - } - text="Créer un ticket" - /> - -
- -
-
- - Vous pouvez aussi nous contacter directement via - - -
-
- ) -} diff --git a/packages/website/src/features/dashboard/support/ticketListTableRow.tsx b/packages/website/src/features/dashboard/support/ticketListTableRow.tsx deleted file mode 100644 index ed213eec..00000000 --- a/packages/website/src/features/dashboard/support/ticketListTableRow.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import type { returnedSchemas } from "@arrhes/application-metadata/schemas" -import { Chip, formatDate, LinkContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import type * as v from "valibot" -import { ListTable } from "../../../components/layouts/listTable/listTable.tsx" -import { LinkButton } from "../../../components/linkButton.tsx" - -const categoryLabels: Record = { - bug: "Erreur", - enhancement: "Amélioration", - feature: "Fonctionnalité", - other: "Autre", -} - -const statusLabels: Record< - string, - { - text: string - color: "information" | "neutral" - } -> = { - open: { - text: "Ouvert", - color: "information", - }, - closed: { - text: "Fermé", - color: "neutral", - }, -} - -export function TicketListTableRow(props: { ticket: v.InferOutput }) { - const createdAt = formatDate(props.ticket.createdAt) - const status = statusLabels[props.ticket.status] ?? { - text: props.ticket.status, - color: "neutral" as const, - } - const categoryLabel = categoryLabels[props.ticket.category] ?? props.ticket.category - - return ( - -
-
-
- - - {`Ticket - ${categoryLabel}`} - - -
-
- - -
-
-
- {`Créé le ${createdAt}`} -
-
-
- ) -} diff --git a/packages/website/src/features/dashboard/support/ticketsListTable.tsx b/packages/website/src/features/dashboard/support/ticketsListTable.tsx deleted file mode 100644 index be3e63a4..00000000 --- a/packages/website/src/features/dashboard/support/ticketsListTable.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { readAllTicketsRouteDefinition } from "@arrhes/application-metadata/routes" -import { IconTicket } from "@tabler/icons-react" -import { DataWrapper } from "../../../components/layouts/dataWrapper.tsx" -import { EmptyState } from "../../../components/layouts/emptyState.tsx" -import { ListTable } from "../../../components/layouts/listTable/listTable.tsx" -import { TicketListTableRow } from "./ticketListTableRow.tsx" - -export function TicketsListTable() { - return ( - - - {(tickets) => { - const sortedTickets = tickets.sort((a, b) => b.createdAt.localeCompare(a.createdAt)) - - if (sortedTickets.length === 0) { - return ( - } - title="Aucun ticket" - subtitle="Créez un ticket pour contacter le support" - /> - ) - } - return sortedTickets.map((ticket) => ( - - )) - }} - - - ) -} diff --git a/packages/website/src/features/docs/DocsLayout.tsx b/packages/website/src/features/docs/DocsLayout.tsx new file mode 100644 index 00000000..9824ad38 --- /dev/null +++ b/packages/website/src/features/docs/DocsLayout.tsx @@ -0,0 +1,304 @@ +import { Button, ButtonGhostContent, ButtonPlainContent, Logo } from "@arrhes/ui" +import { css, cx } from "@arrhes/ui/utilities/cn.js" +import { IconBook2, IconBrandGithub, IconMenu } from "@tabler/icons-react" +import { Outlet, useRouterState } from "@tanstack/react-router" +import { useState } from "react" +import { LinkButton } from "../../components/LinkButton.js" +import { DocsSearch } from "./DocsSearch.js" +import { docSections } from "./docSections.js" +import { SectionTab } from "./SectionTab.js" +import { SidebarNavigation } from "./SidebarNavigation.js" + +type DocSectionId = keyof typeof docSections + +function getCurrentSection(pathname: string): DocSectionId { + if (pathname.startsWith("/documentation/comptabilité")) return "comptabilite" + if (pathname.startsWith("/documentation/dashboard")) return "dashboard" + if (pathname.startsWith("/documentation/api")) return "api" + if (pathname.startsWith("/documentation/cli")) return "cli" + return "general" +} + +export function DocsLayout() { + const pathname = useRouterState({ + select: (s) => s.location.pathname, + }) + const [isMenuOpen, setIsMenuOpen] = useState(false) + + const currentSectionId = getCurrentSection(pathname) + const currentSection = docSections[currentSectionId] + + return ( +
+ {/* Header */} +
+ {/* Top bar */} +
+
+
+ + } + text="Arrhes" + /> + + + + / + + + + } + text="Documentation" + /> + +
+ + + + +
+
+ + {/* Section tabs */} +
+
+ {Object.values(docSections).map((section) => ( + + ))} +
+
+
+ +
+
+ {/* Sidebar */} + + + {/* Main content */} +
+
+ + {isMenuOpen === false ? null : ( + + )} +
+
+ +
+
+
+
+
+ ) +} diff --git a/packages/website/src/features/docs/docsSearch.tsx b/packages/website/src/features/docs/DocsSearch.tsx similarity index 100% rename from packages/website/src/features/docs/docsSearch.tsx rename to packages/website/src/features/docs/DocsSearch.tsx diff --git a/packages/website/src/features/docs/SectionTab.tsx b/packages/website/src/features/docs/SectionTab.tsx new file mode 100644 index 00000000..cc933eb8 --- /dev/null +++ b/packages/website/src/features/docs/SectionTab.tsx @@ -0,0 +1,37 @@ +import { ButtonGhostContent } from "@arrhes/ui" +import type { Icon, IconProps } from "@tabler/icons-react" +import type { ReactElement } from "react" +import { LinkButton } from "../../components/LinkButton.tsx" + +export interface DocSection { + id: string + label: string + path: string + icon: ReactElement> + navigation: Record< + string, + { + title?: string + icon?: ReactElement> + items: { + path: string + hash?: string + label: string + chipText?: string + chipColor?: "neutral" | "information" | "error" | "warning" | "success" + }[] + } + > +} + +export function SectionTab(props: { section: DocSection; isActive: boolean }) { + return ( + + + + ) +} diff --git a/packages/website/src/features/docs/SidebarNavigation.tsx b/packages/website/src/features/docs/SidebarNavigation.tsx new file mode 100644 index 00000000..2f6e1729 --- /dev/null +++ b/packages/website/src/features/docs/SidebarNavigation.tsx @@ -0,0 +1,125 @@ +import { ButtonGhostContent, Chip, type ChipColors } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import type { Icon, IconProps } from "@tabler/icons-react" +import { cloneElement, type ReactElement } from "react" +import { LinkButton } from "../../components/LinkButton.tsx" + +export interface NavigationSection { + title?: string + icon?: ReactElement> + items: { + path: string + hash?: string + label: string + chipText?: string + chipColor?: ChipColors + }[] +} + +export function SidebarNavigation(props: { + navigation: Record + pathname: string + onClick?: () => void +}) { + return ( + + ) +} diff --git a/packages/website/src/features/docs/accounting/introduction/AccountsAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/introduction/AccountsAccountingDocPage.tsx new file mode 100644 index 00000000..afdd50f7 --- /dev/null +++ b/packages/website/src/features/docs/accounting/introduction/AccountsAccountingDocPage.tsx @@ -0,0 +1,437 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import { DocDefinition } from "../../../../components/document/DocDefinition.js" +import { DocExample } from "../../../../components/document/DocExample.js" +import { DocHeader } from "../../../../components/document/DocHeader.js" +import { DocLink } from "../../../../components/document/DocLink.js" +import { DocList } from "../../../../components/document/DocList.js" +import { DocParagraph } from "../../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../../components/document/DocRoot.js" +import { DocSection } from "../../../../components/document/DocSection.js" +import { DocSourceRef } from "../../../../components/document/DocSourceRef.js" +import { DocSources } from "../../../../components/document/DocSources.js" +import { DocTable } from "../../../../components/document/DocTable.js" +import { DocTip } from "../../../../components/document/DocTip.js" +import { accountClasses } from "../resources/accounts/accountsData.js" + +export function AccountsAccountingDocPage() { + return ( + + + + + + Un{" "} + + compte + {" "} + est une catégorie qui regroupe des opérations de même nature. Chaque compte possède un numéro et un + intitulé qui permettent de l'identifier. Par exemple, le compte 512 - Banque regroupe toutes les + opérations transitant par votre compte bancaire. + + + En France, les comptes sont organisés selon le{" "} + + Plan Comptable Général (PCG) + + , qui définit une structure commune à toutes les organisations. Le PCG est aujourd'hui défini par le + règlement n°2014-03 de l'Autorité des Normes Comptables (ANC). + + + + + Le premier Plan Comptable Général a été adopté en France en 1943, puis maintenu après la Seconde + Guerre mondiale pour servir d'outil de planification économique lors de la reconstruction. Il a été + révisé en 1947, 1957, puis profondément refondu en 1982. + L'ANC, créée par ordonnance en 2009, est aujourd'hui l'organisme chargé de le + faire évoluer. + + + + + + + Pour bien comprendre le fonctionnement des comptes, il est utile de distinguer deux grandes familles + : les{" "} + + + comptes d'opérations + + {" "} + et les{" "} + + + comptes d'agents + + + . + + + + Ces comptes enregistrent les opérations économiques du point de vue de l'entreprise : achats, + ventes, charges, produits. Ils décrivent ce que fait l'entreprise. + + + Ces comptes enregistrent les relations avec les tiers du point de vue de ces tiers : clients, + fournisseurs, banque, caisse, État. Ils décrivent qui doit quoi à qui. + + + +

Le compte Clients est tenu du point de vue des clients :

+
    +
  • + Quand un client vous doit de l'argent - le compte est débité (sa dette + augmente) +
  • +
  • + Quand il vous paye - le compte est crédité (sa dette diminue) +
  • +
+

+ Le compte Fournisseurs est tenu du point de vue des fournisseurs : +

+
    +
  • + Quand vous leur devez de l'argent - le compte est crédité (leur créance + augmente) +
  • +
  • + Quand vous les payez - le compte est débité (leur créance diminue) +
  • +
+
+ + + Cette distinction explique pourquoi les comptes d'{" "} + + actif + {" "} + (Banque, Caisse, Clients) augmentent au{" "} + + débit + + , tandis que les comptes de{" "} + + passif + {" "} + (Fournisseurs, Capital) augmentent au{" "} + + crédit + {" "} + : on adopte toujours le point de vue de l'agent concerné. + +
+ + + + Les comptes sont regroupés en 8 classes, numérotées de 1 à 8. + Le premier chiffre du numéro de compte indique sa classe. + + + [ + String(c.number), + c.label, + c.type, + ])} + /> + + + Les classes 1 à 5 concernent le{" "} + + + bilan + + {" "} + (ce que vous possédez et ce que vous devez). Les classes 6 et 7 concernent le{" "} + + + compte de résultat + + {" "} + (ce que vous dépensez et ce que vous gagnez). + + + + La classe 9, autrefois réservée à la comptabilité analytique, a été supprimée du PCG. La + comptabilité analytique est désormais tenue librement par les entreprises en dehors du plan de + comptes officiel. + + + + + + Selon le type de compte, les mouvements au débit et au crédit ont des significations différentes : + + + + + +

Quand vous recevez de l'argent sur votre compte bancaire :

+

+ - Le compte 512 (Banque) est un compte d'actif +

+

+ - Une augmentation se traduit par un débit +

+

+ Quand vous payez une facture depuis ce compte : +

+

+ - Une diminution se traduit par un crédit +

+
+
+ + + + L'État est traité comme un agent particulier avec plusieurs comptes dans la classe 4. Pour la TVA, + on distingue deux comptes principaux : + + + + TVA payée sur les achats. L'État vous doit cette somme (ou vous pouvez la déduire de la TVA + collectée). Le compte est débité quand la TVA déductible augmente. + + + TVA facturée sur les ventes. Vous devez cette somme à l'État. Le compte est crédité quand la TVA + collectée augmente. + + + +

À la fin de la période :

+
    +
  • Si TVA collectée > TVA déductible - vous devez la différence à l'État
  • +
  • Si TVA collectée < TVA déductible - l'État vous doit la différence (crédit de TVA)
  • +
+
+
+ + + + Le numéro d'un compte n'est pas choisi au hasard. Le PCG suit des règles de numérotation précises + + qui facilitent la lecture et la classification : + + + + + +

+ Le compte 60611 se décompose ainsi : +

+
    +
  • + 6 = Classe 6 (Charges) +
  • +
  • + 60 = Achats +
  • +
  • + 606 = Achats non stockés de matières et fournitures +
  • +
  • + 6061 = Fournitures non stockables +
  • +
  • + 60611 = Eau, énergie +
  • +
+
+
+ + + + Le PCG distingue deux catégories de comptes selon leur caractère obligatoire : + + + + Ces comptes constituent le socle obligatoire du plan comptable. Ils doivent être utilisés par toutes + les entités, quelle que soit leur taille. Ils couvrent les opérations courantes et permettent + d'établir les documents de synthèse essentiels. + + + Ces comptes offrent un niveau de détail supplémentaire. Leur utilisation est optionnelle et dépend + des besoins de l'entité et de la complexité de son activité. Ils permettent un suivi plus fin de + certaines opérations. + + + + + + Dans Arrhes, vous pouvez{" "} + configurer votre plan comptable selon + les besoins de votre{" "} + + organisation + + . Le logiciel propose un plan comptable par défaut adapté aux entreprises et aux associations + françaises, que vous pouvez personnaliser. + + + + +
+ ) +} diff --git a/packages/website/src/features/docs/accounting/introduction/ClassesAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/introduction/ClassesAccountingDocPage.tsx new file mode 100644 index 00000000..1663218e --- /dev/null +++ b/packages/website/src/features/docs/accounting/introduction/ClassesAccountingDocPage.tsx @@ -0,0 +1,202 @@ +import { DocHeader } from "../../../../components/document/DocHeader.js" +import { DocParagraph } from "../../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../../components/document/DocRoot.js" +import { DocSection } from "../../../../components/document/DocSection.js" +import { DocSourceRef } from "../../../../components/document/DocSourceRef.js" +import { DocSources } from "../../../../components/document/DocSources.js" +import { DocTip } from "../../../../components/document/DocTip.js" +import { accountClasses } from "../resources/accounts/accountsData.js" + +// ── Class descriptions & tips ────────────────────────────────────────────── + +interface ClassInfo { + description: string + tips: Array<{ + variant: "tip" | "info" + children: string + }> +} + +const classInfos: Record = { + 1: { + description: + "Les comptes de la classe 1 enregistrent les ressources durables de l'entreprise : le capital social, les réserves, le résultat de l'exercice, les subventions d'investissement, les provisions pour risques et charges, ainsi que les emprunts et dettes à long terme. Ces comptes figurent au passif du bilan et reflètent les moyens de financement permanents de l'entreprise.", + tips: [ + { + variant: "info", + children: + "Les comptes de la classe 1 constituent le « haut du bilan » côté passif. Ils représentent l'origine des financements à long terme, qu'ils soient propres (capital, réserves) ou empruntés (emprunts, dettes financières).", + }, + { + variant: "tip", + children: + "Le compte 108 - Compte de l'exploitant est spécifique aux entreprises individuelles. Il enregistre les apports et retraits personnels de l'exploitant, jouant un rôle similaire au capital dans les sociétés.", + }, + ], + }, + 2: { + description: + "Les comptes de la classe 2 enregistrent les actifs destinés à rester durablement dans l'entreprise : immobilisations incorporelles (brevets, logiciels, fonds de commerce), immobilisations corporelles (terrains, constructions, matériel), immobilisations financières (participations, prêts), ainsi que leurs amortissements et dépréciations. Ces comptes figurent à l'actif du bilan.", + tips: [ + { + variant: "info", + children: + "Les amortissements (comptes 28) et les dépréciations (comptes 29) viennent en déduction de la valeur brute des immobilisations pour donner leur valeur nette comptable au bilan.", + }, + { + variant: "tip", + children: + "Un bien est comptabilisé en immobilisation s'il est destiné à servir de façon durable l'activité de l'entreprise. Les biens de faible valeur (généralement inférieurs à 500 € HT) peuvent être comptabilisés directement en charges.", + }, + ], + }, + 3: { + description: + "Les comptes de la classe 3 enregistrent les biens et services achetés ou produits par l'entreprise, destinés à être vendus ou consommés : matières premières, marchandises, produits en cours de fabrication, produits finis et en-cours de production de services. Ces comptes figurent à l'actif du bilan et sont évalués en fin d'exercice lors de l'inventaire.", + tips: [ + { + variant: "info", + children: + "La variation de stock en fin d'exercice donne lieu à des écritures de régularisation. Un stock final supérieur au stock initial vient diminuer les charges (ou augmenter les produits pour les produits finis), et inversement.", + }, + { + variant: "tip", + children: + "Les comptes 39 enregistrent les dépréciations de stocks lorsque la valeur actuelle d'un stock est inférieure à son coût d'entrée. Cette dépréciation est réversible, contrairement à un amortissement.", + }, + ], + }, + 4: { + description: + "Les comptes de la classe 4 enregistrent les créances et les dettes liées aux relations avec les tiers : fournisseurs, clients, personnel, organismes sociaux, État (impôts et taxes, TVA), associés, débiteurs et créditeurs divers. Selon le solde, ces comptes figurent à l'actif (créances) ou au passif (dettes) du bilan.", + tips: [ + { + variant: "info", + children: + "Les comptes de TVA (4456 et 4457) jouent un rôle central dans la gestion fiscale. Le compte 4456 enregistre la TVA payée sur les achats (déductible), tandis que le compte 4457 enregistre la TVA facturée sur les ventes (collectée).", + }, + { + variant: "tip", + children: + "Les comptes de la classe 4 sont dits « bilatéraux » : ils peuvent présenter un solde débiteur (créance) ou créditeur (dette). En fin d'exercice, il faut veiller à ne pas compenser les soldes et à présenter séparément les créances et les dettes au bilan.", + }, + ], + }, + 5: { + description: + "Les comptes de la classe 5 enregistrent les opérations liées à la trésorerie de l'entreprise : comptes bancaires, caisse, valeurs mobilières de placement, instruments financiers à court terme et virements internes. Ces comptes figurent à l'actif du bilan et permettent de suivre la liquidité immédiate de l'entreprise.", + tips: [ + { + variant: "info", + children: + "Le compte 512 - Banque est l'un des comptes les plus utilisés en comptabilité. Il doit être rapproché régulièrement avec les relevés bancaires pour vérifier la concordance entre la comptabilité et la banque.", + }, + { + variant: "tip", + children: + "Le compte 580 - Virements internes est un compte de passage utilisé pour les transferts entre comptes financiers (par exemple, un retrait de la banque vers la caisse). Il doit toujours avoir un solde nul en fin de période.", + }, + ], + }, + 6: { + description: + "Les comptes de la classe 6 enregistrent les charges de l'exercice : achats de marchandises et de matières premières, services extérieurs, impôts et taxes, charges de personnel, charges financières (intérêts), dotations aux amortissements et provisions, ainsi que les charges exceptionnelles. Ces comptes figurent dans le compte de résultat et viennent diminuer le résultat de l'exercice.", + tips: [ + { + variant: "info", + children: + "La structure de la classe 6 suit la logique du compte de résultat : les comptes 60 à 65 concernent les charges d'exploitation, les comptes 66 les charges financières, et les comptes 67 les charges exceptionnelles. Les comptes 68 enregistrent les dotations aux amortissements, dépréciations et provisions.", + }, + { + variant: "tip", + children: + "Les charges sont enregistrées au débit des comptes de la classe 6. Pour annuler ou réduire une charge, on passe l'écriture au crédit du même compte (par exemple, lors d'un avoir reçu d'un fournisseur).", + }, + ], + }, + 7: { + description: + "Les comptes de la classe 7 enregistrent les produits de l'exercice : ventes de marchandises, production vendue et stockée, subventions d'exploitation, produits financiers (intérêts, dividendes), reprises sur amortissements et provisions, ainsi que les produits exceptionnels. Ces comptes figurent dans le compte de résultat et viennent augmenter le résultat de l'exercice.", + tips: [ + { + variant: "info", + children: + "La structure de la classe 7 est symétrique à celle de la classe 6 : les comptes 70 à 75 concernent les produits d'exploitation, les comptes 76 les produits financiers, et les comptes 77 les produits exceptionnels. Les comptes 78 enregistrent les reprises sur amortissements, dépréciations et provisions.", + }, + { + variant: "tip", + children: + "Le compte 701 - Ventes de produits finis et le compte 707 - Ventes de marchandises sont à distinguer : le premier concerne les biens fabriqués par l'entreprise, le second les biens achetés et revendus en l'état.", + }, + ], + }, + 8: { + description: + "Les comptes de la classe 8 enregistrent les opérations particulières qui ne figurent ni au bilan ni au compte de résultat : engagements hors bilan (cautions, avals, garanties) et contributions volontaires en nature dans les associations (bénévolat, dons en nature, mises à disposition gratuites). Ces comptes sont présentés dans l'annexe des comptes annuels.", + tips: [ + { + variant: "info", + children: + "Les comptes 80 à 809 retracent les engagements hors bilan (donnés, reçus et réciproques). Ils fonctionnent en partie double grâce au compte 809 - Contrepartie des engagements. Ces informations sont obligatoirement mentionnées dans l'annexe.", + }, + { + variant: "tip", + children: + "Les comptes 86 et 87 sont spécifiques aux associations. Ils permettent de valoriser les contributions volontaires en nature (bénévolat, dons en nature) au pied du compte de résultat, offrant une image plus fidèle des ressources réellement mobilisées par l'association.", + }, + ], + }, +} + +// ── Main page ────────────────────────────────────────────────────────────── + +export function ClassesAccountingDocPage() { + return ( + + + + + Le Plan Comptable Général organise les comptes en 8 classes. + Les classes 1 à 5 concernent les comptes de bilan, les classes 6 et 7 les comptes + de résultat, et la classe 8 les comptes spéciaux. + + + {accountClasses.map((cls) => { + const info = classInfos[cls.number] + if (!info) return null + return ( + + {info.description} + {info.tips.map((tip, i) => ( + + {tip.children} + + ))} + + ) + })} + + + + ) +} diff --git a/packages/website/src/features/docs/accounting/introduction/DoubleEntryAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/introduction/DoubleEntryAccountingDocPage.tsx new file mode 100644 index 00000000..8cd084b5 --- /dev/null +++ b/packages/website/src/features/docs/accounting/introduction/DoubleEntryAccountingDocPage.tsx @@ -0,0 +1,346 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import { DocDefinition } from "../../../../components/document/DocDefinition.tsx" +import { DocExample } from "../../../../components/document/DocExample.tsx" +import { DocHeader } from "../../../../components/document/DocHeader.tsx" +import { DocLink } from "../../../../components/document/DocLink.tsx" +import { DocParagraph } from "../../../../components/document/DocParagraph.tsx" +import { DocRoot } from "../../../../components/document/DocRoot.tsx" +import { DocSection } from "../../../../components/document/DocSection.tsx" +import { DocSourceRef } from "../../../../components/document/DocSourceRef.tsx" +import { DocSources } from "../../../../components/document/DocSources.tsx" +import { DocTable } from "../../../../components/document/DocTable.tsx" +import { DocTip } from "../../../../components/document/DocTip.tsx" + +export function DoubleEntryAccountingDocPage() { + return ( + + + + + + La vie d'une organisation est faite d'opérations économiques : achats, ventes, encaissements, + paiements, etc. Une opération est un événement qui modifie la situation financière de + l'organisation. Cette opération peut autant traduire un flux réel d'argent (ex : paiement d'une + facture) qu'un flux virtuel à venir, comme la reconnaissance d'une créance ou d'une dette (ex : + émission d'une facture). + + + Le principe de la partie double est le fondement de toute comptabilité moderne, codifié par Luca + Pacioli en 1494. + Il repose sur une idée simple : chaque opération de l'organisation est un + flux qui a une origine et une destination . Origine et destination + sont par définition d'un même montant : le flux est équilibré. La comptabilité garantit ainsi que + chaque mouvement de ressource (argent, matériaux, service, etc.) est correctement enregistré et + équilibré. + + + + Concrètement, chaque opération de l'organisation est notée dans ce qu'on appelle un{" "} + + journal + + , sous la forme d'une{" "} + + écriture + + . Une écriture comporte au moins deux lignes. Chaque ligne fait correspondre ce qu'on appelle un{" "} + + compte + + , à un montant, soit au{" "} + + débit + + , soit au{" "} + + crédit + + . Peu importe le nombre de lignes, la somme des montants débités doit toujours être égale à la somme + des montants crédités. C'est ce qui garantit l'équilibre permanent de la comptabilité. + + + + +

+ Voici deux écritures extraites du journal, correspondant à deux opérations de l'organisation : +

+

+ Achat en espèces de fournitures de bureau pour la somme de 100,00€. +

+ +

+ Vente d'une prestation de service pour 1500,00€ : le client paie 500,00€ par virement, le reste + est dû à 30 jours. +

+ +

+ Pour chaque écriture, on a bien le total des débits qui est égal au total des crédits. +

+
+ + + Grâce à ce principe, toute erreur d'enregistrement crée un déséquilibre entre débits et crédits, ce + qui la rend immédiatement détectable. C'est aussi un outil de prévention de la fraude : la + falsification d'une écriture sans altérer l'équilibre global est extrêmement difficile. Enfin, la + partie double permet d'enregistrer des opérations futures (dettes, créances) et pas seulement des + mouvements d'argent effectifs. + +
+ + + + Une écriture comptable est l'enregistrement d'une opération économique dans les comptes de + l'organisation. Elle se compose d'une date, d'un libellé décrivant l'opération, et d'au moins deux + lignes : chaque ligne associe un compte à un montant, inscrit au débit ou au crédit. +
+
+ Le total des débits d'une écriture est toujours égal au total de ses crédits : c'est le principe de + la partie double appliqué à chaque opération. Une écriture doit être justifiée par une pièce + justificative (facture, relevé bancaire, ticket de caisse, etc.). +
+ + Un compte est un registre qui suit l'évolution d'un élément précis du patrimoine ou de l'activité de + l'organisation. Son montant est modifié à chaque fois qu'il est impliqué dans une opération + comptable (on parle d'écriture), soit au débit, soit au crédit. On dit que le compte est mouvementé + (par l'écriture). +
+ Chaque compte a un numéro et un intitulé qui précisent ce qu'il suit. +
+
+ Il existe quatre grands types de comptes : les comptes d'actif (ce que l'organisation possède), de + passif (ce qu'elle doit), de charges (ce qu'elle dépense) et de produits (ce qu'elle gagne). +
+
+ Par exemple, le compte « Banque » (numéro 512) suit l'argent disponible sur le compte bancaire. Le + compte « Fournisseurs » (numéro 401) suit les dettes envers les fournisseurs. Le compte « Ventes » + (numéro 706) suit le montant des ventes réalisées. +
+ + Débiter un compte, c'est enregistrer que ce compte est la destination d'un flux. Les ressources vont + vers ce compte. +
+
+ Actif (ex : 512 - Banque) - un client paie par virement : l'argent arrive sur le compte bancaire, on + débite le compte 512. +
+ Charges (ex : 601 - Achats stockés) - l'organisation reçoit des matières premières : la charge + augmente, on débite le compte 601. +
+ Passif (ex : 164 - Emprunt) - l'organisation rembourse une partie de son emprunt : le remboursement + est dirigé vers la dette pour la réduire, on débite le compte 164. +
+ Produits (ex : 706 - Ventes) - un client retourne un article : l'annulation est dirigée vers le + compte de ventes pour le réduire, on débite le compte 706. +
+ + Créditer un compte, c'est enregistrer que ce compte est l'origine d'un flux. Les ressources partent + de ce compte. +
    +
  • + + Passif (ex : Emprunt) + {" "} + - l'organisation contracte un emprunt : la dette est la source des fonds reçus, on crédite + Emprunt. +
  • +
  • + + Produits (ex : Ventes) + {" "} + - l'organisation réalise une vente : le revenu est la source de la valeur créée, on crédite + Ventes. +
  • +
  • + + Actif (ex : Caisse) + {" "} + - l'organisation paie en espèces : l'argent part de la caisse, on crédite Caisse. +
  • +
  • + + Charges (ex : Fournitures) + {" "} + - le fournisseur accorde un avoir : la charge est annulée, on crédite Fournitures. +
  • +
+
+ + Le solde d'un compte résume en un seul chiffre tout ce qui y est entré et sorti. C'est la différence + entre le total de ses débits et le total de ses crédits. Si les débits sont supérieurs, le solde est + débiteur. Dans le cas contraire, il est créditeur. + +
+ + + + Ces concepts sont directement appliqués dans Arrhes. Lorsque vous{" "} + saisissez une écriture, le logiciel vous + demande de spécifier les comptes à débiter et à créditer, et vérifie automatiquement que l'équilibre + est respecté. + + + + +
+ ) +} diff --git a/packages/website/src/features/docs/accounting/introduction/EntriesAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/introduction/EntriesAccountingDocPage.tsx new file mode 100644 index 00000000..d6d4cacc --- /dev/null +++ b/packages/website/src/features/docs/accounting/introduction/EntriesAccountingDocPage.tsx @@ -0,0 +1,560 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import { DocDefinition } from "../../../../components/document/DocDefinition.tsx" +import { DocExample } from "../../../../components/document/DocExample.tsx" +import { DocHeader } from "../../../../components/document/DocHeader.tsx" +import { DocLink } from "../../../../components/document/DocLink.tsx" +import { DocList } from "../../../../components/document/DocList.tsx" +import { DocParagraph } from "../../../../components/document/DocParagraph.tsx" +import { DocRoot } from "../../../../components/document/DocRoot.tsx" +import { DocSection } from "../../../../components/document/DocSection.tsx" +import { DocSourceRef } from "../../../../components/document/DocSourceRef.tsx" +import { DocSources } from "../../../../components/document/DocSources.tsx" +import { DocTable } from "../../../../components/document/DocTable.tsx" +import { DocTip } from "../../../../components/document/DocTip.tsx" + +export function EntriesAccountingDocPage() { + return ( + + + + + + Une{" "} + + écriture comptable + {" "} + est la traduction d'une opération économique dans le langage de la comptabilité. C'est l'acte + concret d'enregistrement : à chaque fois que votre organisation effectue une opération (achat, + vente, encaissement, paiement…), celle-ci doit être consignée sous forme d'écriture. + + + + Comme vu dans la page sur la{" "} + partie double, chaque écriture + respecte le principe de la{" "} + + partie double + {" "} + : elle est composée d'au moins deux lignes, chacune associée à un{" "} + + compte comptable + + . Une ligne enregistre un montant au débit, l'autre au crédit. Le total des débits égale toujours le + total des crédits. + + + Les écritures sont le lien entre les opérations réelles et les comptes. Sans écriture, un compte ne + bouge pas. Les comptes ne sont que le reflet cumulé de toutes les écritures qui les ont affectés. + + + + Chaque écriture doit être appuyée par un document qui prouve la réalité de l'opération (facture, + relevé bancaire, ticket de caisse…). C'est le fondement du contrôle comptable. + + + Les écritures doivent être passées dans l'ordre chronologique. On ne revient jamais en arrière : les + erreurs sont corrigées par des écritures de sens contraire, jamais effacées. + + + + + Une écriture comptable complète contient les éléments suivants : + + + + Seuls les comptes à 3 chiffres ou plus peuvent être utilisés dans les écritures comptables. Les + comptes à 1 ou 2 chiffres (par exemple{" "} + + 1 + + ,{" "} + + 10 + + ,{" "} + + 60 + + ) sont des comptes de regroupement servant uniquement à la classification dans le plan comptable. + + + + +

+ Achat de fournitures de bureau - 120 euros TTC payé par chèque +

+ +

+ Total débit = Total crédit = 120,00 euros +

+
+
+ + + + Une distinction importante existe entre les opérations à crédit (paiement différé) + et les opérations au comptant (paiement immédiat). + + + +

+ 1. Réception de la facture fournisseur (journal HA) +

+ +

+ 2. Règlement de la facture (journal BQ) +

+ +

+ La première écriture crée la dette, la seconde l'éteint. +

+
+ + +

+ Achat payé immédiatement par carte bancaire +

+ +

+ Pas de passage par le compte fournisseur car le paiement est immédiat. +

+
+
+ + + +

+ 1. Émission de la facture client (journal VE) +

+ +

+ 2. Encaissement du client (journal BQ) +

+ +
+ + +

+ Un adhérent paye sa cotisation annuelle de 50 euros en espèces. +

+ +

+ La caisse augmente (débit), les produits augmentent (crédit). +

+
+
+ + + + Si votre{" "} + + organisation + {" "} + est assujettie à la TVA, chaque opération doit distinguer le montant hors taxes (HT) et la TVA. + Le compte État joue un rôle central. + + + + + +

+ À la fin du mois, vous avez : +

+
    +
  • TVA collectée (4457) : 500 euros (créditeur)
  • +
  • TVA déductible (4456) : 300 euros (débiteur)
  • +
+

+ Écriture de liquidation de TVA : +

+ +

+ Vous devez 200 euros à l'État (différence entre collectée et déductible). +

+
+
+ + + + La{" "} + + + balance + + {" "} + permet de vérifier que toutes les écritures sont équilibrées. Elle liste tous les comptes avec : + + + + Si le total des débits n'égale pas le total des crédits, c'est qu'une erreur s'est glissée quelque + part. La balance doit toujours être équilibrée. + + + + + + Dans Arrhes, la saisie des écritures est + simplifiée. Le logiciel vérifie automatiquement l'équilibre débit/crédit et vous guide dans le choix + des comptes. Vous pouvez également créer des modèles d'écritures pour les opérations répétitives. + + + + +
+ ) +} diff --git a/packages/website/src/features/docs/accounting/introduction/IntroductionAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/introduction/IntroductionAccountingDocPage.tsx new file mode 100644 index 00000000..3bddf421 --- /dev/null +++ b/packages/website/src/features/docs/accounting/introduction/IntroductionAccountingDocPage.tsx @@ -0,0 +1,146 @@ +import { DocHeader } from "../../../../components/document/DocHeader.tsx" +import { DocLink } from "../../../../components/document/DocLink.tsx" +import { DocList } from "../../../../components/document/DocList.tsx" +import { DocParagraph } from "../../../../components/document/DocParagraph.tsx" +import { DocRoot } from "../../../../components/document/DocRoot.tsx" +import { DocSection } from "../../../../components/document/DocSection.tsx" +import { DocSourceRef } from "../../../../components/document/DocSourceRef.tsx" +import { DocSources } from "../../../../components/document/DocSources.tsx" +import { DocTip } from "../../../../components/document/DocTip.tsx" + +export function IntroductionAccountingDocPage() { + return ( + + + + + + La comptabilité est un système d'organisation de l'information financière. Elle permet + d'enregistrer, classer et analyser toutes les opérations économiques d'une{" "} + + organisation + {" "} + (entreprise, association, etc.). + + + En termes simples, la comptabilité répond à deux questions essentielles : + + + + + + La comptabilité est l'une des plus anciennes pratiques de gestion. Les premières traces remontent à + l'Antiquité : les Sumériens enregistraient déjà les transactions commerciales sur des tablettes + d'argile, et le Code de Hammurabi (vers 1750 av. J.-C.) imposait aux commerçants de tenir des + registres. + + + + Le tournant majeur intervient à la fin du XVe siècle. En 1494, le mathématicien italien{" "} + Luca Pacioli publie la Summa de arithmetica + , dans laquelle il codifie le système de la{" "} + + partie double + + , déjà utilisé par les banques italiennes depuis la fin du XIIIe siècle. Ce système, où + chaque opération est enregistrée simultanément au débit et au crédit, reste le fondement de toute + comptabilité moderne. + + + En France, c'est l'ordonnance de Colbert de 1673 qui impose pour la première fois la tenue de livres + comptables aux commerçants. + Puis, en 1947, la France adopte son premier{" "} + + Plan Comptable Général (PCG) + + , unifiant les pratiques comptables à l'échelle nationale. Ce plan a été révisé plusieurs fois + (1957, 1982) et est aujourd'hui défini par le règlement n°2014-03 de l'Autorité des Normes + Comptables (ANC). + + + + + + + La tenue d'une comptabilité est une obligation légale pour la plupart des + structures en France. Le Code de commerce (articles L.123-12 à L.123-28) impose à toute personne + physique ou morale ayant la qualité de commerçant d'enregistrer chronologiquement les mouvements + affectant le patrimoine de l'entreprise, de contrôler par inventaire l'existence et la valeur de ses + éléments, et d'établir des comptes annuels à la clôture de chaque exercice. + + + + + Les très petites entreprises (TPE) relevant du régime micro peuvent bénéficier d'une comptabilité + simplifiée, dite comptabilité de caisse : elles n'enregistrent que les + encaissements et les décaissements, sans comptabiliser les créances et les dettes. + + + + Au-delà de l'obligation légale, la comptabilité offre plusieurs avantages concrets : + + + + + + + ) +} diff --git a/packages/website/src/features/docs/accounting/introduction/RootAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/introduction/RootAccountingDocPage.tsx new file mode 100644 index 00000000..f59ee1ac --- /dev/null +++ b/packages/website/src/features/docs/accounting/introduction/RootAccountingDocPage.tsx @@ -0,0 +1,49 @@ +import { DocHeader } from "../../../../components/document/DocHeader.tsx" +import { DocLink } from "../../../../components/document/DocLink.tsx" +import { DocList } from "../../../../components/document/DocList.tsx" +import { DocParagraph } from "../../../../components/document/DocParagraph.tsx" +import { DocRoot } from "../../../../components/document/DocRoot.tsx" +import { DocSection } from "../../../../components/document/DocSection.tsx" + +export function RootAccountingDocPage() { + return ( + + + + + + Ce cours est conçu pour ceux qui souhaitent comprendre les fondamentaux de la comptabilité + française. Il peut également servir de rappel aux professionnels. Chaque page contient des + définitions claires et des exemples concrets pour faciliter l'apprentissage. + + + + Aucun prérequis n'est nécessaire : les notions sont introduites progressivement, des concepts les + plus simples jusqu'aux documents de synthèse. + + + + + + + + + + Arrhes est un logiciel de comptabilité conçu pour les entreprises et les associations françaises. Ce + cours reprend des concepts directement utilisés dans le logiciel. En le suivant, vous comprendrez + comment utiliser Arrhes efficacement. + + + + ) +} diff --git a/packages/website/src/features/docs/accounting/introduction/accountsAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/introduction/accountsAccountingDocPage.tsx deleted file mode 100644 index 4ad7682d..00000000 --- a/packages/website/src/features/docs/accounting/introduction/accountsAccountingDocPage.tsx +++ /dev/null @@ -1,444 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" -import { DocDefinition } from "../../../../components/document/docDefinition.js" -import { DocExample } from "../../../../components/document/docExample.js" -import { DocHeader } from "../../../../components/document/docHeader.js" -import { DocLink } from "../../../../components/document/docLink.js" -import { DocList } from "../../../../components/document/docList.js" -import { DocNextPage } from "../../../../components/document/docNextPage.js" -import { DocParagraph } from "../../../../components/document/docParagraph.js" -import { DocRoot } from "../../../../components/document/docRoot.js" -import { DocSection } from "../../../../components/document/docSection.js" -import { DocSourceRef } from "../../../../components/document/docSourceRef.js" -import { DocSources } from "../../../../components/document/docSources.js" -import { DocTable } from "../../../../components/document/docTable.js" -import { DocTip } from "../../../../components/document/docTip.js" -import { accountClasses } from "../resources/accounts/accountsData.js" - -export function AccountsAccountingDocPage() { - return ( - - - - - - Un{" "} - - compte - {" "} - est une catégorie qui regroupe des opérations de même nature. Chaque compte possède un numéro et un - intitulé qui permettent de l'identifier. Par exemple, le compte 512 - Banque regroupe toutes les - opérations transitant par votre compte bancaire. - - - En France, les comptes sont organisés selon le{" "} - - Plan Comptable Général (PCG) - - , qui définit une structure commune à toutes les organisations. Le PCG est aujourd'hui défini par le - règlement n°2014-03 de l'Autorité des Normes Comptables (ANC). - - - - - Le premier Plan Comptable Général a été adopté en France en 1943, puis maintenu après la Seconde - Guerre mondiale pour servir d'outil de planification économique lors de la reconstruction. Il a été - révisé en 1947, 1957, puis profondément refondu en 1982. - L'ANC, créée par ordonnance en 2009, est aujourd'hui l'organisme chargé de le - faire évoluer. - - - - - - - Pour bien comprendre le fonctionnement des comptes, il est utile de distinguer deux grandes familles - : les{" "} - - - comptes d'opérations - - {" "} - et les{" "} - - - comptes d'agents - - - . - - - - Ces comptes enregistrent les opérations économiques du point de vue de l'entreprise : achats, - ventes, charges, produits. Ils décrivent ce que fait l'entreprise. - - - Ces comptes enregistrent les relations avec les tiers du point de vue de ces tiers : clients, - fournisseurs, banque, caisse, État. Ils décrivent qui doit quoi à qui. - - - -

Le compte Clients est tenu du point de vue des clients :

-
    -
  • - Quand un client vous doit de l'argent - le compte est débité (sa dette - augmente) -
  • -
  • - Quand il vous paye - le compte est crédité (sa dette diminue) -
  • -
-

- Le compte Fournisseurs est tenu du point de vue des fournisseurs : -

-
    -
  • - Quand vous leur devez de l'argent - le compte est crédité (leur créance - augmente) -
  • -
  • - Quand vous les payez - le compte est débité (leur créance diminue) -
  • -
-
- - - Cette distinction explique pourquoi les comptes d'{" "} - - actif - {" "} - (Banque, Caisse, Clients) augmentent au{" "} - - débit - - , tandis que les comptes de{" "} - - passif - {" "} - (Fournisseurs, Capital) augmentent au{" "} - - crédit - {" "} - : on adopte toujours le point de vue de l'agent concerné. - -
- - - - Les comptes sont regroupés en 8 classes, numérotées de 1 à 8. - Le premier chiffre du numéro de compte indique sa classe. - - - [ - String(c.number), - c.label, - c.type, - ])} - /> - - - Les classes 1 à 5 concernent le{" "} - - - bilan - - {" "} - (ce que vous possédez et ce que vous devez). Les classes 6 et 7 concernent le{" "} - - - compte de résultat - - {" "} - (ce que vous dépensez et ce que vous gagnez). - - - - La classe 9, autrefois réservée à la comptabilité analytique, a été supprimée du PCG. La - comptabilité analytique est désormais tenue librement par les entreprises en dehors du plan de - comptes officiel. - - - - - - Selon le type de compte, les mouvements au débit et au crédit ont des significations différentes : - - - - - -

Quand vous recevez de l'argent sur votre compte bancaire :

-

- - Le compte 512 (Banque) est un compte d'actif -

-

- - Une augmentation se traduit par un débit -

-

- Quand vous payez une facture depuis ce compte : -

-

- - Une diminution se traduit par un crédit -

-
-
- - - - L'État est traité comme un agent particulier avec plusieurs comptes dans la classe 4. Pour la TVA, - on distingue deux comptes principaux : - - - - TVA payée sur les achats. L'État vous doit cette somme (ou vous pouvez la déduire de la TVA - collectée). Le compte est débité quand la TVA déductible augmente. - - - TVA facturée sur les ventes. Vous devez cette somme à l'État. Le compte est crédité quand la TVA - collectée augmente. - - - -

À la fin de la période :

-
    -
  • Si TVA collectée > TVA déductible - vous devez la différence à l'État
  • -
  • Si TVA collectée < TVA déductible - l'État vous doit la différence (crédit de TVA)
  • -
-
-
- - - - Le numéro d'un compte n'est pas choisi au hasard. Le PCG suit des règles de numérotation précises - - qui facilitent la lecture et la classification : - - - - - -

- Le compte 60611 se décompose ainsi : -

-
    -
  • - 6 = Classe 6 (Charges) -
  • -
  • - 60 = Achats -
  • -
  • - 606 = Achats non stockés de matières et fournitures -
  • -
  • - 6061 = Fournitures non stockables -
  • -
  • - 60611 = Eau, énergie -
  • -
-
-
- - - - Le PCG distingue deux catégories de comptes selon leur caractère obligatoire : - - - - Ces comptes constituent le socle obligatoire du plan comptable. Ils doivent être utilisés par toutes - les entités, quelle que soit leur taille. Ils couvrent les opérations courantes et permettent - d'établir les documents de synthèse essentiels. - - - Ces comptes offrent un niveau de détail supplémentaire. Leur utilisation est optionnelle et dépend - des besoins de l'entité et de la complexité de son activité. Ils permettent un suivi plus fin de - certaines opérations. - - - - - - Dans Arrhes, vous pouvez{" "} - configurer votre plan comptable selon - les besoins de votre{" "} - - organisation - - . Le logiciel propose un plan comptable par défaut adapté aux entreprises et aux associations - françaises, que vous pouvez personnaliser. - - - - - - -
- ) -} diff --git a/packages/website/src/features/docs/accounting/introduction/classesAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/introduction/classesAccountingDocPage.tsx deleted file mode 100644 index b09341e5..00000000 --- a/packages/website/src/features/docs/accounting/introduction/classesAccountingDocPage.tsx +++ /dev/null @@ -1,208 +0,0 @@ -import { DocHeader } from "../../../../components/document/docHeader.js" -import { DocNextPage } from "../../../../components/document/docNextPage.js" -import { DocParagraph } from "../../../../components/document/docParagraph.js" -import { DocRoot } from "../../../../components/document/docRoot.js" -import { DocSection } from "../../../../components/document/docSection.js" -import { DocSourceRef } from "../../../../components/document/docSourceRef.js" -import { DocSources } from "../../../../components/document/docSources.js" -import { DocTip } from "../../../../components/document/docTip.js" -import { accountClasses } from "../resources/accounts/accountsData.js" - -// ── Class descriptions & tips ────────────────────────────────────────────── - -interface ClassInfo { - description: string - tips: Array<{ - variant: "tip" | "info" - children: string - }> -} - -const classInfos: Record = { - 1: { - description: - "Les comptes de la classe 1 enregistrent les ressources durables de l'entreprise : le capital social, les réserves, le résultat de l'exercice, les subventions d'investissement, les provisions pour risques et charges, ainsi que les emprunts et dettes à long terme. Ces comptes figurent au passif du bilan et reflètent les moyens de financement permanents de l'entreprise.", - tips: [ - { - variant: "info", - children: - "Les comptes de la classe 1 constituent le « haut du bilan » côté passif. Ils représentent l'origine des financements à long terme, qu'ils soient propres (capital, réserves) ou empruntés (emprunts, dettes financières).", - }, - { - variant: "tip", - children: - "Le compte 108 - Compte de l'exploitant est spécifique aux entreprises individuelles. Il enregistre les apports et retraits personnels de l'exploitant, jouant un rôle similaire au capital dans les sociétés.", - }, - ], - }, - 2: { - description: - "Les comptes de la classe 2 enregistrent les actifs destinés à rester durablement dans l'entreprise : immobilisations incorporelles (brevets, logiciels, fonds de commerce), immobilisations corporelles (terrains, constructions, matériel), immobilisations financières (participations, prêts), ainsi que leurs amortissements et dépréciations. Ces comptes figurent à l'actif du bilan.", - tips: [ - { - variant: "info", - children: - "Les amortissements (comptes 28) et les dépréciations (comptes 29) viennent en déduction de la valeur brute des immobilisations pour donner leur valeur nette comptable au bilan.", - }, - { - variant: "tip", - children: - "Un bien est comptabilisé en immobilisation s'il est destiné à servir de façon durable l'activité de l'entreprise. Les biens de faible valeur (généralement inférieurs à 500 € HT) peuvent être comptabilisés directement en charges.", - }, - ], - }, - 3: { - description: - "Les comptes de la classe 3 enregistrent les biens et services achetés ou produits par l'entreprise, destinés à être vendus ou consommés : matières premières, marchandises, produits en cours de fabrication, produits finis et en-cours de production de services. Ces comptes figurent à l'actif du bilan et sont évalués en fin d'exercice lors de l'inventaire.", - tips: [ - { - variant: "info", - children: - "La variation de stock en fin d'exercice donne lieu à des écritures de régularisation. Un stock final supérieur au stock initial vient diminuer les charges (ou augmenter les produits pour les produits finis), et inversement.", - }, - { - variant: "tip", - children: - "Les comptes 39 enregistrent les dépréciations de stocks lorsque la valeur actuelle d'un stock est inférieure à son coût d'entrée. Cette dépréciation est réversible, contrairement à un amortissement.", - }, - ], - }, - 4: { - description: - "Les comptes de la classe 4 enregistrent les créances et les dettes liées aux relations avec les tiers : fournisseurs, clients, personnel, organismes sociaux, État (impôts et taxes, TVA), associés, débiteurs et créditeurs divers. Selon le solde, ces comptes figurent à l'actif (créances) ou au passif (dettes) du bilan.", - tips: [ - { - variant: "info", - children: - "Les comptes de TVA (4456 et 4457) jouent un rôle central dans la gestion fiscale. Le compte 4456 enregistre la TVA payée sur les achats (déductible), tandis que le compte 4457 enregistre la TVA facturée sur les ventes (collectée).", - }, - { - variant: "tip", - children: - "Les comptes de la classe 4 sont dits « bilatéraux » : ils peuvent présenter un solde débiteur (créance) ou créditeur (dette). En fin d'exercice, il faut veiller à ne pas compenser les soldes et à présenter séparément les créances et les dettes au bilan.", - }, - ], - }, - 5: { - description: - "Les comptes de la classe 5 enregistrent les opérations liées à la trésorerie de l'entreprise : comptes bancaires, caisse, valeurs mobilières de placement, instruments financiers à court terme et virements internes. Ces comptes figurent à l'actif du bilan et permettent de suivre la liquidité immédiate de l'entreprise.", - tips: [ - { - variant: "info", - children: - "Le compte 512 - Banque est l'un des comptes les plus utilisés en comptabilité. Il doit être rapproché régulièrement avec les relevés bancaires pour vérifier la concordance entre la comptabilité et la banque.", - }, - { - variant: "tip", - children: - "Le compte 580 - Virements internes est un compte de passage utilisé pour les transferts entre comptes financiers (par exemple, un retrait de la banque vers la caisse). Il doit toujours avoir un solde nul en fin de période.", - }, - ], - }, - 6: { - description: - "Les comptes de la classe 6 enregistrent les charges de l'exercice : achats de marchandises et de matières premières, services extérieurs, impôts et taxes, charges de personnel, charges financières (intérêts), dotations aux amortissements et provisions, ainsi que les charges exceptionnelles. Ces comptes figurent dans le compte de résultat et viennent diminuer le résultat de l'exercice.", - tips: [ - { - variant: "info", - children: - "La structure de la classe 6 suit la logique du compte de résultat : les comptes 60 à 65 concernent les charges d'exploitation, les comptes 66 les charges financières, et les comptes 67 les charges exceptionnelles. Les comptes 68 enregistrent les dotations aux amortissements, dépréciations et provisions.", - }, - { - variant: "tip", - children: - "Les charges sont enregistrées au débit des comptes de la classe 6. Pour annuler ou réduire une charge, on passe l'écriture au crédit du même compte (par exemple, lors d'un avoir reçu d'un fournisseur).", - }, - ], - }, - 7: { - description: - "Les comptes de la classe 7 enregistrent les produits de l'exercice : ventes de marchandises, production vendue et stockée, subventions d'exploitation, produits financiers (intérêts, dividendes), reprises sur amortissements et provisions, ainsi que les produits exceptionnels. Ces comptes figurent dans le compte de résultat et viennent augmenter le résultat de l'exercice.", - tips: [ - { - variant: "info", - children: - "La structure de la classe 7 est symétrique à celle de la classe 6 : les comptes 70 à 75 concernent les produits d'exploitation, les comptes 76 les produits financiers, et les comptes 77 les produits exceptionnels. Les comptes 78 enregistrent les reprises sur amortissements, dépréciations et provisions.", - }, - { - variant: "tip", - children: - "Le compte 701 - Ventes de produits finis et le compte 707 - Ventes de marchandises sont à distinguer : le premier concerne les biens fabriqués par l'entreprise, le second les biens achetés et revendus en l'état.", - }, - ], - }, - 8: { - description: - "Les comptes de la classe 8 enregistrent les opérations particulières qui ne figurent ni au bilan ni au compte de résultat : engagements hors bilan (cautions, avals, garanties) et contributions volontaires en nature dans les associations (bénévolat, dons en nature, mises à disposition gratuites). Ces comptes sont présentés dans l'annexe des comptes annuels.", - tips: [ - { - variant: "info", - children: - "Les comptes 80 à 809 retracent les engagements hors bilan (donnés, reçus et réciproques). Ils fonctionnent en partie double grâce au compte 809 - Contrepartie des engagements. Ces informations sont obligatoirement mentionnées dans l'annexe.", - }, - { - variant: "tip", - children: - "Les comptes 86 et 87 sont spécifiques aux associations. Ils permettent de valoriser les contributions volontaires en nature (bénévolat, dons en nature) au pied du compte de résultat, offrant une image plus fidèle des ressources réellement mobilisées par l'association.", - }, - ], - }, -} - -// ── Main page ────────────────────────────────────────────────────────────── - -export function ClassesAccountingDocPage() { - return ( - - - - - Le Plan Comptable Général organise les comptes en 8 classes. - Les classes 1 à 5 concernent les comptes de bilan, les classes 6 et 7 les comptes - de résultat, et la classe 8 les comptes spéciaux. - - - {accountClasses.map((cls) => { - const info = classInfos[cls.number] - if (!info) return null - return ( - - {info.description} - {info.tips.map((tip, i) => ( - - {tip.children} - - ))} - - ) - })} - - - - - - ) -} diff --git a/packages/website/src/features/docs/accounting/introduction/doubleEntryAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/introduction/doubleEntryAccountingDocPage.tsx deleted file mode 100644 index daa15f8f..00000000 --- a/packages/website/src/features/docs/accounting/introduction/doubleEntryAccountingDocPage.tsx +++ /dev/null @@ -1,353 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" -import { DocDefinition } from "../../../../components/document/docDefinition.tsx" -import { DocExample } from "../../../../components/document/docExample.tsx" -import { DocHeader } from "../../../../components/document/docHeader.tsx" -import { DocLink } from "../../../../components/document/docLink.tsx" -import { DocNextPage } from "../../../../components/document/docNextPage.tsx" -import { DocParagraph } from "../../../../components/document/docParagraph.tsx" -import { DocRoot } from "../../../../components/document/docRoot.tsx" -import { DocSection } from "../../../../components/document/docSection.tsx" -import { DocSourceRef } from "../../../../components/document/docSourceRef.tsx" -import { DocSources } from "../../../../components/document/docSources.tsx" -import { DocTable } from "../../../../components/document/docTable.tsx" -import { DocTip } from "../../../../components/document/docTip.tsx" - -export function DoubleEntryAccountingDocPage() { - return ( - - - - - - La vie d'une organisation est faite d'opérations économiques : achats, ventes, encaissements, - paiements, etc. Une opération est un événement qui modifie la situation financière de - l'organisation. Cette opération peut autant traduire un flux réel d'argent (ex : paiement d'une - facture) qu'un flux virtuel à venir, comme la reconnaissance d'une créance ou d'une dette (ex : - émission d'une facture). - - - Le principe de la partie double est le fondement de toute comptabilité moderne, codifié par Luca - Pacioli en 1494. - Il repose sur une idée simple : chaque opération de l'organisation est un - flux qui a une origine et une destination . Origine et destination - sont par définition d'un même montant : le flux est équilibré. La comptabilité garantit ainsi que - chaque mouvement de ressource (argent, matériaux, service, etc.) est correctement enregistré et - équilibré. - - - - Concrètement, chaque opération de l'organisation est notée dans ce qu'on appelle un{" "} - - journal - - , sous la forme d'une{" "} - - écriture - - . Une écriture comporte au moins deux lignes. Chaque ligne fait correspondre ce qu'on appelle un{" "} - - compte - - , à un montant, soit au{" "} - - débit - - , soit au{" "} - - crédit - - . Peu importe le nombre de lignes, la somme des montants débités doit toujours être égale à la somme - des montants crédités. C'est ce qui garantit l'équilibre permanent de la comptabilité. - - - - -

- Voici deux écritures extraites du journal, correspondant à deux opérations de l'organisation : -

-

- Achat en espèces de fournitures de bureau pour la somme de 100,00€. -

- -

- Vente d'une prestation de service pour 1500,00€ : le client paie 500,00€ par virement, le reste - est dû à 30 jours. -

- -

- Pour chaque écriture, on a bien le total des débits qui est égal au total des crédits. -

-
- - - Grâce à ce principe, toute erreur d'enregistrement crée un déséquilibre entre débits et crédits, ce - qui la rend immédiatement détectable. C'est aussi un outil de prévention de la fraude : la - falsification d'une écriture sans altérer l'équilibre global est extrêmement difficile. Enfin, la - partie double permet d'enregistrer des opérations futures (dettes, créances) et pas seulement des - mouvements d'argent effectifs. - -
- - - - Une écriture comptable est l'enregistrement d'une opération économique dans les comptes de - l'organisation. Elle se compose d'une date, d'un libellé décrivant l'opération, et d'au moins deux - lignes : chaque ligne associe un compte à un montant, inscrit au débit ou au crédit. -
-
- Le total des débits d'une écriture est toujours égal au total de ses crédits : c'est le principe de - la partie double appliqué à chaque opération. Une écriture doit être justifiée par une pièce - justificative (facture, relevé bancaire, ticket de caisse, etc.). -
- - Un compte est un registre qui suit l'évolution d'un élément précis du patrimoine ou de l'activité de - l'organisation. Son montant est modifié à chaque fois qu'il est impliqué dans une opération - comptable (on parle d'écriture), soit au débit, soit au crédit. On dit que le compte est mouvementé - (par l'écriture). -
- Chaque compte a un numéro et un intitulé qui précisent ce qu'il suit. -
-
- Il existe quatre grands types de comptes : les comptes d'actif (ce que l'organisation possède), de - passif (ce qu'elle doit), de charges (ce qu'elle dépense) et de produits (ce qu'elle gagne). -
-
- Par exemple, le compte « Banque » (numéro 512) suit l'argent disponible sur le compte bancaire. Le - compte « Fournisseurs » (numéro 401) suit les dettes envers les fournisseurs. Le compte « Ventes » - (numéro 706) suit le montant des ventes réalisées. -
- - Débiter un compte, c'est enregistrer que ce compte est la destination d'un flux. Les ressources vont - vers ce compte. -
-
- Actif (ex : 512 - Banque) - un client paie par virement : l'argent arrive sur le compte bancaire, on - débite le compte 512. -
- Charges (ex : 601 - Achats stockés) - l'organisation reçoit des matières premières : la charge - augmente, on débite le compte 601. -
- Passif (ex : 164 - Emprunt) - l'organisation rembourse une partie de son emprunt : le remboursement - est dirigé vers la dette pour la réduire, on débite le compte 164. -
- Produits (ex : 706 - Ventes) - un client retourne un article : l'annulation est dirigée vers le - compte de ventes pour le réduire, on débite le compte 706. -
- - Créditer un compte, c'est enregistrer que ce compte est l'origine d'un flux. Les ressources partent - de ce compte. -
    -
  • - - Passif (ex : Emprunt) - {" "} - - l'organisation contracte un emprunt : la dette est la source des fonds reçus, on crédite - Emprunt. -
  • -
  • - - Produits (ex : Ventes) - {" "} - - l'organisation réalise une vente : le revenu est la source de la valeur créée, on crédite - Ventes. -
  • -
  • - - Actif (ex : Caisse) - {" "} - - l'organisation paie en espèces : l'argent part de la caisse, on crédite Caisse. -
  • -
  • - - Charges (ex : Fournitures) - {" "} - - le fournisseur accorde un avoir : la charge est annulée, on crédite Fournitures. -
  • -
-
- - Le solde d'un compte résume en un seul chiffre tout ce qui y est entré et sorti. C'est la différence - entre le total de ses débits et le total de ses crédits. Si les débits sont supérieurs, le solde est - débiteur. Dans le cas contraire, il est créditeur. - -
- - - - Ces concepts sont directement appliqués dans Arrhes. Lorsque vous{" "} - saisissez une écriture, le logiciel vous - demande de spécifier les comptes à débiter et à créditer, et vérifie automatiquement que l'équilibre - est respecté. - - - - - - -
- ) -} diff --git a/packages/website/src/features/docs/accounting/introduction/entriesAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/introduction/entriesAccountingDocPage.tsx deleted file mode 100644 index 26d4cf39..00000000 --- a/packages/website/src/features/docs/accounting/introduction/entriesAccountingDocPage.tsx +++ /dev/null @@ -1,567 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" -import { DocDefinition } from "../../../../components/document/docDefinition.tsx" -import { DocExample } from "../../../../components/document/docExample.tsx" -import { DocHeader } from "../../../../components/document/docHeader.tsx" -import { DocLink } from "../../../../components/document/docLink.tsx" -import { DocList } from "../../../../components/document/docList.tsx" -import { DocNextPage } from "../../../../components/document/docNextPage.tsx" -import { DocParagraph } from "../../../../components/document/docParagraph.tsx" -import { DocRoot } from "../../../../components/document/docRoot.tsx" -import { DocSection } from "../../../../components/document/docSection.tsx" -import { DocSourceRef } from "../../../../components/document/docSourceRef.tsx" -import { DocSources } from "../../../../components/document/docSources.tsx" -import { DocTable } from "../../../../components/document/docTable.tsx" -import { DocTip } from "../../../../components/document/docTip.tsx" - -export function EntriesAccountingDocPage() { - return ( - - - - - - Une{" "} - - écriture comptable - {" "} - est la traduction d'une opération économique dans le langage de la comptabilité. C'est l'acte - concret d'enregistrement : à chaque fois que votre organisation effectue une opération (achat, - vente, encaissement, paiement…), celle-ci doit être consignée sous forme d'écriture. - - - - Comme vu dans la page sur la{" "} - partie double, chaque écriture - respecte le principe de la{" "} - - partie double - {" "} - : elle est composée d'au moins deux lignes, chacune associée à un{" "} - - compte comptable - - . Une ligne enregistre un montant au débit, l'autre au crédit. Le total des débits égale toujours le - total des crédits. - - - Les écritures sont le lien entre les opérations réelles et les comptes. Sans écriture, un compte ne - bouge pas. Les comptes ne sont que le reflet cumulé de toutes les écritures qui les ont affectés. - - - - Chaque écriture doit être appuyée par un document qui prouve la réalité de l'opération (facture, - relevé bancaire, ticket de caisse…). C'est le fondement du contrôle comptable. - - - Les écritures doivent être passées dans l'ordre chronologique. On ne revient jamais en arrière : les - erreurs sont corrigées par des écritures de sens contraire, jamais effacées. - - - - - Une écriture comptable complète contient les éléments suivants : - - - - Seuls les comptes à 3 chiffres ou plus peuvent être utilisés dans les écritures comptables. Les - comptes à 1 ou 2 chiffres (par exemple{" "} - - 1 - - ,{" "} - - 10 - - ,{" "} - - 60 - - ) sont des comptes de regroupement servant uniquement à la classification dans le plan comptable. - - - - -

- Achat de fournitures de bureau - 120 euros TTC payé par chèque -

- -

- Total débit = Total crédit = 120,00 euros -

-
-
- - - - Une distinction importante existe entre les opérations à crédit (paiement différé) - et les opérations au comptant (paiement immédiat). - - - -

- 1. Réception de la facture fournisseur (journal HA) -

- -

- 2. Règlement de la facture (journal BQ) -

- -

- La première écriture crée la dette, la seconde l'éteint. -

-
- - -

- Achat payé immédiatement par carte bancaire -

- -

- Pas de passage par le compte fournisseur car le paiement est immédiat. -

-
-
- - - -

- 1. Émission de la facture client (journal VE) -

- -

- 2. Encaissement du client (journal BQ) -

- -
- - -

- Un adhérent paye sa cotisation annuelle de 50 euros en espèces. -

- -

- La caisse augmente (débit), les produits augmentent (crédit). -

-
-
- - - - Si votre{" "} - - organisation - {" "} - est assujettie à la TVA, chaque opération doit distinguer le montant hors taxes (HT) et la TVA. - Le compte État joue un rôle central. - - - - - -

- À la fin du mois, vous avez : -

-
    -
  • TVA collectée (4457) : 500 euros (créditeur)
  • -
  • TVA déductible (4456) : 300 euros (débiteur)
  • -
-

- Écriture de liquidation de TVA : -

- -

- Vous devez 200 euros à l'État (différence entre collectée et déductible). -

-
-
- - - - La{" "} - - - balance - - {" "} - permet de vérifier que toutes les écritures sont équilibrées. Elle liste tous les comptes avec : - - - - Si le total des débits n'égale pas le total des crédits, c'est qu'une erreur s'est glissée quelque - part. La balance doit toujours être équilibrée. - - - - - - Dans Arrhes, la saisie des écritures est - simplifiée. Le logiciel vérifie automatiquement l'équilibre débit/crédit et vous guide dans le choix - des comptes. Vous pouvez également créer des modèles d'écritures pour les opérations répétitives. - - - - - - -
- ) -} diff --git a/packages/website/src/features/docs/accounting/introduction/introductionAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/introduction/introductionAccountingDocPage.tsx deleted file mode 100644 index 0b1d4401..00000000 --- a/packages/website/src/features/docs/accounting/introduction/introductionAccountingDocPage.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import { DocHeader } from "../../../../components/document/docHeader.tsx" -import { DocLink } from "../../../../components/document/docLink.tsx" -import { DocList } from "../../../../components/document/docList.tsx" -import { DocNextPage } from "../../../../components/document/docNextPage.tsx" -import { DocParagraph } from "../../../../components/document/docParagraph.tsx" -import { DocRoot } from "../../../../components/document/docRoot.tsx" -import { DocSection } from "../../../../components/document/docSection.tsx" -import { DocSourceRef } from "../../../../components/document/docSourceRef.tsx" -import { DocSources } from "../../../../components/document/docSources.tsx" -import { DocTip } from "../../../../components/document/docTip.tsx" - -export function IntroductionAccountingDocPage() { - return ( - - - - - - La comptabilité est un système d'organisation de l'information financière. Elle permet - d'enregistrer, classer et analyser toutes les opérations économiques d'une{" "} - - organisation - {" "} - (entreprise, association, etc.). - - - En termes simples, la comptabilité répond à deux questions essentielles : - - - - - - La comptabilité est l'une des plus anciennes pratiques de gestion. Les premières traces remontent à - l'Antiquité : les Sumériens enregistraient déjà les transactions commerciales sur des tablettes - d'argile, et le Code de Hammurabi (vers 1750 av. J.-C.) imposait aux commerçants de tenir des - registres. - - - - Le tournant majeur intervient à la fin du XVe siècle. En 1494, le mathématicien italien{" "} - Luca Pacioli publie la Summa de arithmetica - , dans laquelle il codifie le système de la{" "} - - partie double - - , déjà utilisé par les banques italiennes depuis la fin du XIIIe siècle. Ce système, où - chaque opération est enregistrée simultanément au débit et au crédit, reste le fondement de toute - comptabilité moderne. - - - En France, c'est l'ordonnance de Colbert de 1673 qui impose pour la première fois la tenue de livres - comptables aux commerçants. - Puis, en 1947, la France adopte son premier{" "} - - Plan Comptable Général (PCG) - - , unifiant les pratiques comptables à l'échelle nationale. Ce plan a été révisé plusieurs fois - (1957, 1982) et est aujourd'hui défini par le règlement n°2014-03 de l'Autorité des Normes - Comptables (ANC). - - - - - - - La tenue d'une comptabilité est une obligation légale pour la plupart des - structures en France. Le Code de commerce (articles L.123-12 à L.123-28) impose à toute personne - physique ou morale ayant la qualité de commerçant d'enregistrer chronologiquement les mouvements - affectant le patrimoine de l'entreprise, de contrôler par inventaire l'existence et la valeur de ses - éléments, et d'établir des comptes annuels à la clôture de chaque exercice. - - - - - Les très petites entreprises (TPE) relevant du régime micro peuvent bénéficier d'une comptabilité - simplifiée, dite comptabilité de caisse : elles n'enregistrent que les - encaissements et les décaissements, sans comptabiliser les créances et les dettes. - - - - Au-delà de l'obligation légale, la comptabilité offre plusieurs avantages concrets : - - - - - - - - - ) -} diff --git a/packages/website/src/features/docs/accounting/introduction/rootAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/introduction/rootAccountingDocPage.tsx deleted file mode 100644 index ce5c3591..00000000 --- a/packages/website/src/features/docs/accounting/introduction/rootAccountingDocPage.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { DocHeader } from "../../../../components/document/docHeader.tsx" -import { DocLink } from "../../../../components/document/docLink.tsx" -import { DocList } from "../../../../components/document/docList.tsx" -import { DocNextPage } from "../../../../components/document/docNextPage.tsx" -import { DocParagraph } from "../../../../components/document/docParagraph.tsx" -import { DocRoot } from "../../../../components/document/docRoot.tsx" -import { DocSection } from "../../../../components/document/docSection.tsx" - -export function RootAccountingDocPage() { - return ( - - - - - - Ce cours est conçu pour ceux qui souhaitent comprendre les fondamentaux de la comptabilité - française. Il peut également servir de rappel aux professionnels. Chaque page contient des - définitions claires et des exemples concrets pour faciliter l'apprentissage. - - - - Aucun prérequis n'est nécessaire : les notions sont introduites progressivement, des concepts les - plus simples jusqu'aux documents de synthèse. - - - - - - - - - - Arrhes est un logiciel de comptabilité conçu pour les entreprises et les associations françaises. Ce - cours reprend des concepts directement utilisés dans le logiciel. En le suivant, vous comprendrez - comment utiliser Arrhes efficacement. - - - - - - ) -} diff --git a/packages/website/src/features/docs/accounting/reports/BalanceAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/reports/BalanceAccountingDocPage.tsx new file mode 100644 index 00000000..6ae76069 --- /dev/null +++ b/packages/website/src/features/docs/accounting/reports/BalanceAccountingDocPage.tsx @@ -0,0 +1,253 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import { DocDefinition } from "../../../../components/document/DocDefinition.js" +import { DocExample } from "../../../../components/document/DocExample.js" +import { DocHeader } from "../../../../components/document/DocHeader.js" +import { DocLink } from "../../../../components/document/DocLink.js" +import { DocList } from "../../../../components/document/DocList.js" +import { DocParagraph } from "../../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../../components/document/DocRoot.js" +import { DocSection } from "../../../../components/document/DocSection.js" +import { DocSourceRef } from "../../../../components/document/DocSourceRef.js" +import { DocSources } from "../../../../components/document/DocSources.js" +import { DocTable } from "../../../../components/document/DocTable.js" +import { DocTip } from "../../../../components/document/DocTip.js" + +export function BalanceAccountingDocPage() { + return ( + + + + + + Tableau récapitulatif de tous les comptes utilisés pendant l'exercice, avec pour chacun le total des + débits, le total des crédits et le solde. C'est l'outil de contrôle fondamental de la comptabilité. + + + + La balance + est un résumé du{" "} + grand livre : elle reprend + chaque{" "} + + compte + {" "} + mais ne conserve que les totaux, sans le détail des mouvements. C'est le document idéal pour + vérifier rapidement que la comptabilité est cohérente. + + + + La balance se construit automatiquement à partir du{" "} + grand livre : pour chaque + compte, on additionne tous les débits, tous les crédits, et on en déduit le solde. Si le grand livre + est juste, la balance l'est aussi. + + + + + + La balance permet de vérifier plusieurs équilibres qui découlent du principe de la{" "} + + partie double + + : + + + + + Si la balance n'est pas équilibrée, cela signifie qu'une ou plusieurs écritures ne respectent pas la + partie double. C'est la première vérification à effectuer avant d'établir le{" "} + bilan et le{" "} + compte de résultat. + + + + + + Reprend tous les comptes utilisés pendant l'exercice. C'est le document de contrôle principal. + + + Détaille un compte collectif (clients ou fournisseurs) en montrant le solde de chaque tiers + individuellement. Par exemple, la balance auxiliaire clients liste le solde dû par chaque client. + + + Ventile les soldes par ancienneté (moins de 30 jours, 30 à 60 jours, etc.). Elle est + particulièrement utile pour suivre les retards de paiement des clients ou des fournisseurs. + + + + + + +

+ Total des débits (48 000) = Total des crédits (48 000). Total des soldes débiteurs (35 000) = + Total des soldes créditeurs (35 000). La balance est équilibrée. +

+
+
+ + + + La balance est le pont entre les écritures quotidiennes et les documents de synthèse : + + + + + + Une balance juste est la condition nécessaire pour produire un{" "} + bilan et un{" "} + compte de résultat{" "} + fiables. C'est pourquoi la vérification de la balance est une étape incontournable de la clôture + comptable. + + + + + + Arrhes calcule automatiquement la balance à partir de vos{" "} + écritures. Vous pouvez la consulter à + tout moment pour vérifier l'état de votre comptabilité. Consultez le guide sur les{" "} + rapports pour en savoir plus. + + + + +
+ ) +} diff --git a/packages/website/src/features/docs/accounting/reports/BalanceSheetAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/reports/BalanceSheetAccountingDocPage.tsx new file mode 100644 index 00000000..1449b0fa --- /dev/null +++ b/packages/website/src/features/docs/accounting/reports/BalanceSheetAccountingDocPage.tsx @@ -0,0 +1,462 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import { DocDefinition } from "../../../../components/document/DocDefinition.js" +import { DocExample } from "../../../../components/document/DocExample.js" +import { DocHeader } from "../../../../components/document/DocHeader.js" +import { DocLink } from "../../../../components/document/DocLink.js" +import { DocList } from "../../../../components/document/DocList.js" +import { DocParagraph } from "../../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../../components/document/DocRoot.js" +import { DocSection } from "../../../../components/document/DocSection.js" +import { DocSourceRef } from "../../../../components/document/DocSourceRef.js" +import { DocSources } from "../../../../components/document/DocSources.js" +import { DocTable } from "../../../../components/document/DocTable.js" +import { DocTip } from "../../../../components/document/DocTip.js" + +export function BalanceSheetAccountingDocPage() { + return ( + + + + + + Photographie du patrimoine de l'organisation à une date donnée. Il montre ce que l'organisation + possède (actif) et comment elle l'a financé (passif). + + + + Le bilan + est toujours établi à une date précise, généralement la date + de clôture de l'{" "} + + exercice comptable + + . Contrairement au{" "} + compte de résultat{" "} + qui couvre une période, le bilan est un instantané : il décrit la situation patrimoniale à un moment + donné. + + + + + + Le bilan est construit à partir des{" "} + + soldes des{" "} + + comptes d'agents + + {" "} + (classes 1 à 5 du{" "} + + plan comptable + + ) + : + + + + + + + Le bilan se présente sous forme d'un tableau à deux colonnes. À gauche, l'{" "} + + actif + {" "} + (ce que l'organisation possède). À droite, le{" "} + + passif + {" "} + (comment c'est financé). + + + + + + Biens destinés à rester durablement dans l'organisation : terrains, bâtiments, matériel, brevets. + Ils sont enregistrés dans les comptes de classe 2. + + + Éléments qui se renouvellent au cours du cycle d'exploitation : stocks de marchandises (classe 3), + créances clients (classe 4), trésorerie (classe 5). + + + Ressources appartenant aux propriétaires : capital social, réserves, résultat de l'exercice. Ils + représentent ce que l'organisation doit à ses propriétaires. + + + Sommes dues à des tiers : emprunts bancaires, dettes fournisseurs, dettes fiscales et sociales. + + + + + + L'équation fondamentale du bilan est :{" "} + + + Actif + {" "} + ={" "} + + Passif + {" "} + +{" "} + + Résultat + + + . + + + + Le résultat vient équilibrer le bilan : un{" "} + + bénéfice + {" "} + augmente le passif (les{" "} + + capitaux propres + + ), une{" "} + + perte + {" "} + le diminue. Le total de l'actif est donc toujours strictement égal au total du passif. + + + + Si l'actif et le passif ne sont pas égaux, c'est qu'il y a une erreur dans les écritures. La{" "} + balance permet de détecter ce + type de déséquilibre avant d'établir le bilan. + + + + + + En analyse financière, on distingue le haut de bilan et le{" "} + bas de bilan, une distinction essentielle pour comprendre la structure financière + de l'organisation : + + + + Éléments permanents : à l'actif, les immobilisations (biens durables) ; au passif, les capitaux + propres et les dettes à long terme (emprunts). Ces éléments constituent le socle stable du + financement. + + + Éléments circulants : à l'actif, les stocks, créances et trésorerie ; au passif, les dettes à court + terme (fournisseurs, dettes fiscales). Ces éléments se renouvellent au fil de l'activité. + + + + L'équilibre entre haut et bas de bilan est un indicateur clé de la santé financière. Idéalement, les + emplois permanents (immobilisations) doivent être financés par des ressources permanentes (capitaux + propres + dettes à long terme). + + + + + + Différence entre l'actif circulant (hors trésorerie) et les dettes à court terme. Il mesure le + besoin de financement lié au cycle d'exploitation de l'entreprise. + + + + Le BFR + apparaît lorsque l'entreprise doit financer ses stocks et ses créances + clients avant de recevoir les paiements correspondants. Un BFR positif signifie que l'entreprise a + besoin de trésorerie pour fonctionner ; un BFR négatif (rare) signifie que le cycle d'exploitation + génère de la trésorerie. + + + +

+ Stocks : 5 000 + Créances clients : 8 000 = Actif circulant d'exploitation : 13 000 +

+

+ Dettes fournisseurs : 4 000 + Dettes fiscales : 2 000 = Dettes d'exploitation : 6 000 +

+

+ BFR = 13 000 - 6 000 = 7 000 euros +

+

+ L'entreprise doit trouver 7 000 euros de financement pour couvrir son cycle d'exploitation. +

+
+
+ + + Il existe en pratique plusieurs présentations du bilan selon l'usage : + + + Le bilan au sens strict, établi selon les règles du PCG et déposé au greffe du tribunal de commerce. + C'est le document obligatoire qui fait partie des comptes annuels. + + + Présentation analytique qui reclasse les postes par fonction (exploitation, investissement, + financement). Il sert à l'analyse financière interne et au calcul du BFR. + + + Bilan établi pour les besoins de la déclaration fiscale (liasse fiscale). Il reprend les mêmes + données que le bilan comptable avec des retraitements spécifiques. + + + + + + Certaines obligations de l'organisation n'apparaissent pas directement dans le bilan mais peuvent + représenter des risques ou des avantages significatifs. On les appelle les{" "} + engagements hors bilan : + + + + + + Ces engagements doivent être mentionnés dans l'{" "} + annexe pour que les lecteurs + des comptes aient une image complète de la situation de l'organisation. + + + + En normes IFRS (normes internationales) + , le bilan s'appelle « état de la situation financière » et sa présentation + est différente : les postes sont classés en éléments courants et non courants plutôt qu'en actif + immobilisé et circulant. Les crédits-baux sont également intégrés au bilan (norme IFRS 16). + + + + + +

+ Une association présente le bilan suivant au 31 décembre : +

+
+
+

+ ACTIF +

+
    +
  • Matériel informatique : 2 000
  • +
  • Créances adhérents : 500
  • +
  • Banque : 4 500
  • +
  • + Total : 7 000 +
  • +
+
+
+

+ PASSIF +

+
    +
  • Fonds associatifs : 4 000
  • +
  • Résultat de l'exercice : 2 000
  • +
  • Dettes fournisseurs : 1 000
  • +
  • + Total : 7 000 +
  • +
+
+
+

+ L'association possède 7 000 euros de biens, financés par ses fonds propres (6 000 euros, dont le + résultat de l'année) et une dette fournisseur (1 000 euros). Actif = Passif. +

+
+
+ + + + Le bilan de clôture d'un exercice devient le bilan d'ouverture de l'exercice + suivant. Les soldes de tous les comptes de bilan (classes 1 à 5) sont reportés. Seuls les comptes de + charges et de produits (classes 6 et 7) sont remis à zéro, car ils alimentent le compte de résultat + d'une seule période. + + + + Cette continuité est un principe fondamental : le patrimoine de l'organisation se transmet d'un + exercice à l'autre sans interruption. + + + + +
+ ) +} diff --git a/packages/website/src/features/docs/accounting/reports/FecAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/reports/FecAccountingDocPage.tsx new file mode 100644 index 00000000..2445e759 --- /dev/null +++ b/packages/website/src/features/docs/accounting/reports/FecAccountingDocPage.tsx @@ -0,0 +1,263 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import { DocDefinition } from "../../../../components/document/DocDefinition.js" +import { DocExample } from "../../../../components/document/DocExample.js" +import { DocHeader } from "../../../../components/document/DocHeader.js" +import { DocLink } from "../../../../components/document/DocLink.js" +import { DocList } from "../../../../components/document/DocList.js" +import { DocParagraph } from "../../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../../components/document/DocRoot.js" +import { DocSection } from "../../../../components/document/DocSection.js" +import { DocSourceRef } from "../../../../components/document/DocSourceRef.js" +import { DocSources } from "../../../../components/document/DocSources.js" +import { DocTable } from "../../../../components/document/DocTable.js" +import { DocTip } from "../../../../components/document/DocTip.js" + +export function FecAccountingDocPage() { + return ( + + + + + + Fichier dématérialisé contenant l'ensemble des écritures comptables d'un exercice, dans un format + normé imposé par l'administration fiscale française. Il doit être remis en cas de contrôle fiscal + informatisé. + + + + Le FEC a été instauré par l'article L.47 A-I du Livre des Procédures Fiscales + , applicable depuis le 1er janvier 2014. Toute organisation tenant une + comptabilité informatisée doit être en mesure de produire ce fichier sur demande du vérificateur. + + + + Pour en savoir plus sur le FEC et ses spécificités, consultez{" "} + + fec.arrhes.com + + . + + + + + + Le FEC est un fichier texte tabulé (séparateur tabulation) encodé en UTF-8 avec BOM. La première + ligne contient les en-têtes normés. Chaque ligne suivante représente un mouvement comptable (une + ligne d' + écriture). + + + Le fichier comporte 18 colonnes obligatoires : + + + + + + + Les montants utilisent la virgule comme séparateur décimal (ex. : 1234,50). Les dates sont au format + AAAAMMJJ sans séparateur. Ces conventions sont imposées par le cahier des charges du FEC + . + + + + + + Le nom du fichier FEC suit une convention stricte : [SIREN]FEC[AAAAMMJJ].txt, où le + SIREN est le numéro d'identification de l'organisation et la date correspond à la date de clôture de + l'exercice. + + + +

+ Pour une organisation dont le SIREN est 123456789, avec un exercice clos au 31 décembre 2024 : +

+

+ 123456789FEC20241231.txt +

+
+ + + Si le numéro SIREN n'est pas renseigné dans les paramètres de l'organisation, le nom du fichier ne + sera pas conforme aux exigences de l'administration fiscale. + +
+ + + + L'obligation de présenter un FEC s'applique à toute organisation qui tient sa comptabilité de + manière informatisée, quelle que soit sa taille ou sa forme juridique + . Cela comprend : + + + + + + Seules les micro-entreprises (régime micro-BIC ou micro-BNC) sont dispensées, car elles ne tiennent + pas de comptabilité en partie double. + + + + + + Le défaut de présentation du FEC lors d'un contrôle fiscal peut entraîner des sanctions + significatives : + + + + + + Il est recommandé de vérifier régulièrement que votre FEC est conforme, sans attendre un contrôle + fiscal. Arrhes génère le FEC à partir de vos écritures validées. + + + + + + Arrhes permet d'exporter le FEC directement depuis la page des{" "} + écritures de l'exercice. L'export + respecte le format normé (tabulation, encodage UTF-8 avec BOM, colonnes obligatoires) et nomme + automatiquement le fichier à partir du SIREN de l'organisation. + + + + +
+ ) +} diff --git a/packages/website/src/features/docs/accounting/reports/IncomeStatementAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/reports/IncomeStatementAccountingDocPage.tsx new file mode 100644 index 00000000..1d03bae8 --- /dev/null +++ b/packages/website/src/features/docs/accounting/reports/IncomeStatementAccountingDocPage.tsx @@ -0,0 +1,431 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import { DocDefinition } from "../../../../components/document/DocDefinition.js" +import { DocExample } from "../../../../components/document/DocExample.js" +import { DocHeader } from "../../../../components/document/DocHeader.js" +import { DocLink } from "../../../../components/document/DocLink.js" +import { DocList } from "../../../../components/document/DocList.js" +import { DocParagraph } from "../../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../../components/document/DocRoot.js" +import { DocSection } from "../../../../components/document/DocSection.js" +import { DocSourceRef } from "../../../../components/document/DocSourceRef.js" +import { DocSources } from "../../../../components/document/DocSources.js" +import { DocTip } from "../../../../components/document/DocTip.js" + +export function IncomeStatementAccountingDocPage() { + return ( + + + + + + Document qui récapitule l'ensemble des produits et des charges d'un exercice. La différence entre + les deux donne le résultat : bénéfice ou perte. + + + + Alors que le bilan est une + photographie à un instant donné, le compte de résultat est un film : il couvre + toute la durée de l'{" "} + + exercice comptable + + . Il répond à la question : l'organisation a-t-elle gagné ou perdu de l'argent sur la période ? + + + + + + + Le compte de résultat est construit à partir des{" "} + + + comptes d'opérations + + {" "} + (classes 6 et 7 du{" "} + + plan comptable + + ) + . Les comptes de classe 6 enregistrent les{" "} + + charges + {" "} + (ce que l'on dépense), les comptes de classe 7 enregistrent les{" "} + + produits + {" "} + (ce que l'on gagne). + + + Charges : bénéfice (ou excédent pour une association)", + "Si Produits < Charges : perte (ou déficit)", + ]} + /> + + + + + Le compte de résultat distingue trois catégories d'opérations, ce qui permet d'analyser finement la + performance de l'organisation : + + + + Différence entre les produits et les charges liés à l'activité courante (ventes, achats, salaires, + loyer...). C'est l'indicateur principal de la performance opérationnelle. + + + Différence entre les produits financiers (intérêts reçus, gains de change) et les charges + financières (intérêts d'emprunts, pertes de change). + + + Différence entre les produits et charges qui ne relèvent ni de l'exploitation ni du financier : + cessions d'immobilisations, pénalités, subventions exceptionnelles. + + + + Le résultat net est la somme de ces trois résultats, après déduction de l'impôt sur + les bénéfices le cas échéant. On parle aussi de{" "} + Résultat Courant Avant Impôts (RCAI), qui correspond à la somme du résultat + d'exploitation et du résultat financier, avant prise en compte des éléments exceptionnels et de + l'impôt. Le RCAI reflète la performance récurrente de l'entreprise. + + + + La distinction exploitation / financier / exceptionnel est propre au droit comptable français. Les + normes IFRS + n'utilisent pas cette catégorisation et présentent le compte de résultat + différemment. + + + + + +
+
+

+ CHARGES +

+
    +
  • Achats de marchandises : 3 000
  • +
  • Loyer : 6 000
  • +
  • Salaires : 20 000
  • +
  • Charges sociales : 8 000
  • +
  • Amortissements : 1 000
  • +
  • Intérêts d'emprunt : 500
  • +
  • + Total : 38 500 +
  • +
+
+
+

+ PRODUITS +

+
    +
  • Ventes de marchandises : 25 000
  • +
  • Prestations de services : 12 000
  • +
  • Subventions : 4 000
  • +
  • Intérêts reçus : 100
  • +
  • + Total : 41 100 +
  • +
+
+
+

+ Résultat = 41 100 - 38 500 = 2 600 euros (bénéfice) +

+
+
+ + + + Pour analyser plus finement la performance d'une entreprise, on calcule des indicateurs + intermédiaires appelés Soldes Intermédiaires de Gestion (SIG) + . Chaque solde s'obtient en cascade à partir du précédent, permettant de + comprendre étape par étape comment se forme le résultat : + + + + Ventes de marchandises - Coût d'achat des marchandises vendues. Indicateur clé pour les activités de + négoce. + + + Marge commerciale + Production - Consommations intermédiaires. Mesure la richesse créée par + l'entreprise grâce à son activité. + + + Valeur ajoutée + Subventions d'exploitation - Impôts et taxes - Charges de personnel. Indicateur de + la rentabilité opérationnelle avant amortissements et provisions. + + + + L'EBE est un indicateur particulièrement surveillé car il reflète la capacité de l'entreprise à + générer de la richesse par son activité courante, indépendamment de sa politique de financement et + d'investissement. + + + + + + Une erreur fréquente consiste à assimiler le résultat du compte de résultat à de l'argent gagné ou + perdu. En réalité, le compte de résultat ne mesure pas des flux de trésorerie mais + des flux d'enrichissement ou d'appauvrissement du patrimoine. + + + +

+ Une entreprise facture 50 000 euros de prestations (produits) et engage 30 000 euros de charges. + Son résultat est un bénéfice de 20 000 euros. +

+

+ Mais si ses clients n'ont pas encore payé leurs factures, sa trésorerie peut être négative : + elle a un bénéfice comptable mais pas d'argent en banque. +

+

+ C'est pourquoi on complète souvent le compte de résultat par un tableau des flux de trésorerie, + qui mesure les mouvements réels d'argent. +

+
+ + + Un bénéfice ne signifie pas que l'entreprise a de l'argent disponible, et une perte ne signifie pas + qu'elle n'en a plus. Le résultat mesure la variation du patrimoine, pas celle du compte en banque. + +
+ + + + Les{" "} + + capitaux propres + {" "} + représentent ce que l'entreprise doit à ses propriétaires. Ils comprennent plusieurs éléments : + + + + Apports initiaux des associés ou fondateurs. Il reste généralement stable sauf augmentation ou + réduction de capital. + + + Bénéfices des années passées qui ont été conservés dans l'entreprise (non distribués aux associés). + + + Résultat de l'exercice précédent en attente d'affectation (mise en réserve ou distribution). + + + Bénéfice ou perte de l'année en cours, tel que calculé par le compte de résultat. + + + +

+ Une entreprise réalise un bénéfice de 10 000 euros. Les associés décident : +

+
    +
  • Distribution de dividendes : 4 000 euros (versés aux associés)
  • +
  • Mise en réserve : 6 000 euros (conservés dans l'entreprise)
  • +
+

+ Les réserves augmentent de 6 000 euros, renforçant les capitaux propres. +

+
+
+ + + + Le résultat du compte de résultat vient s'ajouter aux{" "} + + capitaux propres + {" "} + du bilan. C'est ce qui fait le lien entre les deux documents : + + + + On peut aussi exprimer le résultat comme la variation du patrimoine net : + +

+ Résultat = Variation des créances - Variation des dettes +

+

+ Si les créances (ce qu'on possède) ont augmenté de 5 000 euros et les dettes de 2 000 euros, le + résultat est de 3 000 euros : l'entreprise s'est enrichie de la différence. +

+
+ + + Retenez que le bilan répond à « combien vaut l'organisation ? » et le compte de résultat à « comment + s'est passée l'année ? ». Les deux documents se rejoignent par le résultat, qui apparaît dans les + capitaux propres du bilan. + +
+ + + + Les associations utilisent une présentation adaptée. On parle d'excédent plutôt que + de bénéfice et de déficit plutôt que de perte. Certaines lignes sont spécifiques : + contributions volontaires en nature, valorisation du bénévolat, emplois des contributions. + + + + Le principe reste identique : comparer ce que l'association a reçu (cotisations, subventions, dons, + produits d'activité) à ce qu'elle a dépensé pour déterminer le résultat de l'exercice. + + + + +
+ ) +} diff --git a/packages/website/src/features/docs/accounting/reports/JournalAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/reports/JournalAccountingDocPage.tsx new file mode 100644 index 00000000..71706485 --- /dev/null +++ b/packages/website/src/features/docs/accounting/reports/JournalAccountingDocPage.tsx @@ -0,0 +1,368 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import { DocDefinition } from "../../../../components/document/DocDefinition.js" +import { DocExample } from "../../../../components/document/DocExample.js" +import { DocHeader } from "../../../../components/document/DocHeader.js" +import { DocLink } from "../../../../components/document/DocLink.js" +import { DocList } from "../../../../components/document/DocList.js" +import { DocParagraph } from "../../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../../components/document/DocRoot.js" +import { DocSection } from "../../../../components/document/DocSection.js" +import { DocSourceRef } from "../../../../components/document/DocSourceRef.js" +import { DocSources } from "../../../../components/document/DocSources.js" +import { DocTable } from "../../../../components/document/DocTable.js" +import { DocTip } from "../../../../components/document/DocTip.js" + +export function JournalAccountingDocPage() { + return ( + + + + + + Registre dans lequel sont inscrites, par ordre chronologique, toutes les écritures comptables de + l'organisation. Chaque écriture y est enregistrée avec sa date, ses comptes mouvementés, ses + montants et un libellé explicatif. + + + + Le journal est le point d'entrée de la comptabilité + : c'est ici que chaque opération est enregistrée pour la première fois. Il + constitue la trace originale et chronologique de l'ensemble des{" "} + écritures passées au cours d'un{" "} + + exercice comptable + + . + + + + + + Chaque ligne du journal représente un mouvement comptable. Une{" "} + + écriture comptable + {" "} + comprend toujours au moins deux lignes (un{" "} + + débit + {" "} + et un{" "} + + crédit + + ), conformément au principe de la{" "} + + partie double + + . + + + + +

+ Chaque écriture est équilibrée : le total des débits est égal au total des crédits. +

+
+
+ + + + En pratique, pour des raisons d'organisation, les écritures ne sont pas toutes enregistrées dans un + seul journal. On utilise des journaux auxiliaires spécialisés, chacun dédié à un + type d'opération. Cela permet de répartir le travail et de vérifier plus facilement les opérations. + + + + Enregistre toutes les factures fournisseurs reçues. On y trouve les achats à crédit avant leur + règlement. + + + Enregistre toutes les factures clients émises. On y trouve les ventes à crédit avant leur + encaissement. + + + Enregistre tous les mouvements du compte bancaire : encaissements, décaissements, virements. + + + Enregistre tous les mouvements d'espèces : recettes et dépenses en liquide. + + + Enregistre les opérations qui ne rentrent pas dans les autres journaux : salaires, amortissements, + régularisations, écritures de clôture. + + + + L'ensemble de ces journaux auxiliaires forme le journal général, qui centralise + toutes les écritures de l'exercice. Cette organisation permet de répartir le travail de saisie et + facilite les contrôles. + + + + + + Lorsqu'une organisation utilise des journaux auxiliaires, elle doit tenir un{" "} + journal centralisateur (ou livre-journal). Ce document récapitule chaque mois les + totaux de chaque journal auxiliaire, créant ainsi un enregistrement unique et synthétique de toutes + les opérations. + + + + Le journal centralisateur est le document qui a valeur juridique : c'est lui qui est visé par les + obligations légales. Les journaux auxiliaires en sont les documents préparatoires. + + + + Lorsqu'une opération concerne plusieurs journaux auxiliaires (par exemple, un virement de la caisse + vers la banque), on utilise un compte de virements internes (classe 58) pour + assurer la liaison entre les deux journaux sans créer de double enregistrement. + + + + + + En début d'exercice, les soldes des comptes de{" "} + + bilan + {" "} + (classes 1 à 5) doivent être repris de l'exercice précédent. Cette reprise s'effectue par des + écritures d'ouverture enregistrées dans un journal particulier : le{" "} + journal des À-Nouveaux (AN). + + + + Journal spécial qui enregistre les écritures de report des soldes de l'exercice précédent vers le + nouvel exercice. Il porte les soldes de tous les comptes de bilan non soldés. + + + + Les comptes de{" "} + + charges + {" "} + et de{" "} + + produits + {" "} + (classes 6 et 7) ne sont pas reportés : ils sont remis à zéro car ils ne concernent qu'un seul + exercice. Le{" "} + + résultat + {" "} + de l'exercice précédent est affecté aux{" "} + + reports à nouveau + {" "} + ou aux{" "} + + réserves + + . + + + + + + Le journal est un document obligatoire. Plusieurs textes encadrent sa tenue : + + + + Le Code de commerce impose la tenue d'un livre-journal à toute personne ayant la qualité de + commerçant + + , + "Le Code Général des Impôts (CGI) reprend cette obligation pour les besoins fiscaux", + <> + Le Plan Comptable Général (PCG) définit les règles de forme du journal + + , + "L'article R.123-173 du Code de commerce autorise la tenue du journal sur support informatique, à condition de garantir l'authenticité et l'intégrité des écritures", + ]} + /> + + + Le journal doit être conservé pendant 10 ans (obligation comptable) et peut être + exigé en cas de contrôle fiscal, de litige commercial ou de procédure judiciaire. + + + + Le journal doit être tenu sans blanc ni rature. En comptabilité informatisée, les écritures validées + ne peuvent pas être supprimées : toute correction se fait par une écriture de contrepassation. Cette + règle d'irréversibilité garantit la fiabilité de la piste d'audit. + + + + + + Arrhes génère automatiquement le journal à partir de vos{" "} + écritures. Chaque saisie est datée, + numérotée et classée dans le journal approprié. Consultez le guide sur les{" "} + rapports pour apprendre à exporter et + consulter votre journal. + + + + +
+ ) +} diff --git a/packages/website/src/features/docs/accounting/reports/LedgerAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/reports/LedgerAccountingDocPage.tsx new file mode 100644 index 00000000..40b0cc31 --- /dev/null +++ b/packages/website/src/features/docs/accounting/reports/LedgerAccountingDocPage.tsx @@ -0,0 +1,284 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import { DocDefinition } from "../../../../components/document/DocDefinition.js" +import { DocExample } from "../../../../components/document/DocExample.js" +import { DocHeader } from "../../../../components/document/DocHeader.js" +import { DocLink } from "../../../../components/document/DocLink.js" +import { DocList } from "../../../../components/document/DocList.js" +import { DocParagraph } from "../../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../../components/document/DocRoot.js" +import { DocSection } from "../../../../components/document/DocSection.js" +import { DocSourceRef } from "../../../../components/document/DocSourceRef.js" +import { DocSources } from "../../../../components/document/DocSources.js" +import { DocTable } from "../../../../components/document/DocTable.js" +import { DocTip } from "../../../../components/document/DocTip.js" + +export function LedgerAccountingDocPage() { + return ( + + + + + + Document qui reprend l'ensemble des comptes de l'organisation avec le détail de tous leurs + mouvements. C'est la vue par compte de la comptabilité, là où le journal offre une vue + chronologique. + + + + Le grand livre + et le{" "} + journal contiennent exactement + les mêmes informations, mais présentées différemment. Le journal classe les écritures par date ; le + grand livre les classe par{" "} + + compte + + . + + + + Bien que le grand livre ne soit pas explicitement mentionné comme document obligatoire au même titre + que le journal, il est indispensable en pratique + : c'est lui qui permet de justifier le solde de chaque compte et donc de + construire la balance, le{" "} + bilan et le{" "} + compte de résultat. + Il fait partie des documents pouvant être exigés lors d'un contrôle fiscal. + + + + + + Pour chaque compte, le grand livre affiche l'ensemble des mouvements avec leur date, leur libellé, + les montants au{" "} + + débit + {" "} + et au{" "} + + crédit + + , ainsi que le solde progressif. + + + + +

+ Le solde progressif permet de suivre l'évolution du compte au fil des opérations. +

+
+ + + +

+ Le solde créditeur indique que l'organisation doit encore 620 euros à ses fournisseurs. +

+
+
+ + + + Le journal et le grand livre sont les deux faces d'une même réalité. Le journal répond à la question + « que s'est-il passé à telle date ? », tandis que le grand livre répond à « quelle est la situation + de tel compte ? ». + + + + + + + Le grand livre est indispensable pour : + + + + + + + Certains comptes collectifs regroupent de nombreux tiers : le compte 411 (Clients) ou le compte 401 + (Fournisseurs), par exemple. Pour suivre chaque tiers individuellement, on tient un{" "} + grand livre auxiliaire, qui détaille les mouvements par sous-compte (un par client + ou par fournisseur). + + + + Déclinaison du grand livre qui détaille un compte collectif (clients, fournisseurs) en sous-comptes + individuels. Il permet de connaître le solde dû par chaque tiers sans consulter les écritures une + par une. + + + + Le total du grand livre auxiliaire doit toujours correspondre au solde du compte collectif dans le + grand livre général. Cette vérification est un contrôle important lors de la clôture. + + + + + + Le grand livre du compte 512 (Banque) est l'outil central du rapprochement bancaire + . Cette opération consiste à comparer, ligne par ligne, les mouvements enregistrés dans la + comptabilité avec ceux figurant sur le relevé de banque. + + + + Des écarts sont normaux : un chèque émis peut être enregistré en comptabilité mais pas encore + encaissé par le bénéficiaire, ou inversement, un prélèvement peut apparaître sur le relevé bancaire + avant d'être saisi dans les comptes. Le rapprochement bancaire permet d'identifier et d'expliquer + ces décalages. + + + + Le rapprochement bancaire doit être effectué régulièrement (idéalement chaque mois). C'est un outil + de contrôle interne essentiel qui permet de détecter les erreurs de saisie, les opérations oubliées + ou les mouvements non autorisés. + + + + + + Arrhes génère automatiquement le grand livre à partir de vos écritures. Vous pouvez le consulter + compte par compte ou le télécharger dans sa totalité. Consultez le guide sur les{" "} + rapports pour en savoir plus. + + + + +
+ ) +} diff --git a/packages/website/src/features/docs/accounting/reports/NotesAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/reports/NotesAccountingDocPage.tsx new file mode 100644 index 00000000..f099fc7c --- /dev/null +++ b/packages/website/src/features/docs/accounting/reports/NotesAccountingDocPage.tsx @@ -0,0 +1,225 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import { DocDefinition } from "../../../../components/document/DocDefinition.js" +import { DocExample } from "../../../../components/document/DocExample.js" +import { DocHeader } from "../../../../components/document/DocHeader.js" +import { DocLink } from "../../../../components/document/DocLink.js" +import { DocList } from "../../../../components/document/DocList.js" +import { DocParagraph } from "../../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../../components/document/DocRoot.js" +import { DocSection } from "../../../../components/document/DocSection.js" +import { DocSourceRef } from "../../../../components/document/DocSourceRef.js" +import { DocSources } from "../../../../components/document/DocSources.js" +import { DocTip } from "../../../../components/document/DocTip.js" + +export function NotesAccountingDocPage() { + return ( + + + + + + Document obligatoire qui complète le bilan et le compte de résultat. L'annexe fournit les + informations nécessaires à la compréhension des comptes : méthodes comptables utilisées, détail de + certains postes, engagements hors bilan. + + + + Le bilan et le{" "} + compte de résultat{" "} + donnent des chiffres, mais pas toujours les clés pour les interpréter + . C'est le rôle de l'annexe : expliquer comment ces chiffres ont été + construits et quelles informations complémentaires sont nécessaires à leur lecture. + + + + L'annexe est obligatoire en France depuis la réforme du Plan Comptable Général de 1982 + (article 511 du PCG). D'inspiration anglo-saxonne (les{" "} + notes to the financial statements), elle répondait au besoin croissant de transparence + financière. Son objectif principal est de garantir l' + image fidèle de la situation de l'organisation : les comptes annuels doivent + refléter la réalité économique et pas seulement la stricte application des règles comptables. + + + + L'inscription d'une information dans l'annexe ne peut pas se substituer à son inscription au bilan + ou au compte de résultat. Si un élément doit figurer dans un document de synthèse, il doit y figurer + : l'annexe est un complément, pas un remplacement. + + + + + + L'annexe doit inclure toute information significative qui n'apparaît pas directement dans le bilan + ou le compte de résultat. Son contenu varie selon la taille et la nature de l' + + organisation + + , mais elle comprend généralement : + + + + Les conventions appliquées pour évaluer les éléments du bilan et du compte de résultat : méthodes + d'amortissement, règles de comptabilisation des stocks, traitement des opérations en devises + étrangères, etc. + + + Détail des immobilisations et de leurs amortissements, état des provisions, échéancier des créances + et des dettes, variation des capitaux propres. + + + Détail des produits et charges exceptionnels, ventilation du chiffre d'affaires, information sur les + impôts. + + + Obligations qui n'apparaissent pas au bilan mais qui représentent un risque ou un avantage potentiel + : cautions données, crédits-baux, engagements de retraite. + + + Parmi les informations fréquemment requises, on trouve notamment : + + + + + + + Toutes les organisations n'ont pas les mêmes obligations. Le droit français distingue plusieurs + niveaux : + + + + + +

+ En France, une entreprise peut bénéficier de l'annexe simplifiée si elle ne dépasse pas deux des + trois seuils suivants : +

+
    +
  • Chiffre d'affaires : 12 millions d'euros
  • +
  • Total du bilan : 6 millions d'euros
  • +
  • Nombre de salariés : 50
  • +
+

+ Ces seuils sont régulièrement révisés par le législateur. +

+
+ + + Même lorsqu'elle n'est pas obligatoire, l'annexe reste utile pour documenter les choix comptables de + l'organisation et faciliter la compréhension des comptes par les tiers (banquier, commissaire aux + comptes, financeurs). + +
+ + + + L'annexe joue un rôle central dans la transparence financière. Elle permet à toute personne qui lit + les comptes de comprendre : + + + + + + Sans l'annexe, le bilan et le compte de résultat ne forment qu'une image partielle de la situation + de l'organisation. C'est pourquoi ces trois documents constituent ensemble les{" "} + comptes annuels et sont indissociables. + + + + + + En normes IFRS, la norme IAS 1 impose des exigences similaires mais plus + structurées. L'annexe doit notamment inclure : + + + + + + Les entreprises cotées en bourse dans l'Union européenne sont tenues d'appliquer les normes IFRS + pour leurs comptes consolidés. L'annexe y est généralement beaucoup plus volumineuse que dans les + comptes établis selon le PCG français. + + + + + + Arrhes vous accompagne dans la préparation de vos comptes annuels. Consultez le guide sur les{" "} + rapports pour savoir comment exporter les + données nécessaires à la rédaction de votre annexe. + + + + +
+ ) +} diff --git a/packages/website/src/features/docs/accounting/reports/ReportsAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/reports/ReportsAccountingDocPage.tsx new file mode 100644 index 00000000..3f4b7ea4 --- /dev/null +++ b/packages/website/src/features/docs/accounting/reports/ReportsAccountingDocPage.tsx @@ -0,0 +1,196 @@ +import { DocDefinition } from "../../../../components/document/DocDefinition.js" +import { DocHeader } from "../../../../components/document/DocHeader.js" +import { DocLink } from "../../../../components/document/DocLink.js" +import { DocList } from "../../../../components/document/DocList.js" +import { DocParagraph } from "../../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../../components/document/DocRoot.js" +import { DocSection } from "../../../../components/document/DocSection.js" +import { DocSourceRef } from "../../../../components/document/DocSourceRef.js" +import { DocSources } from "../../../../components/document/DocSources.js" +import { DocTip } from "../../../../components/document/DocTip.js" + +export function ReportsAccountingDocPage() { + return ( + + + + + + À la fin de chaque{" "} + + exercice comptable + + , les écritures sont agrégées pour produire des documents de synthèse. Ces documents offrent une + vision globale de la situation financière et des performances de l' + + organisation + + . + + + Les deux documents principaux sont le{" "} + + + bilan + + {" "} + et le{" "} + + + compte de résultat + + + . Ils sont complémentaires et se lisent ensemble. + + + + + + En droit français, les comptes annuels désignent les trois documents obligatoires + que toute organisation doit produire à la clôture de chaque exercice + : + + + + + + Ces trois documents forment un tout indissociable. Le bilan et le compte de résultat donnent les + chiffres ; l' + annexe fournit les clés pour + les interpréter. Leur objectif commun est de donner une image fidèle + du patrimoine, de la situation financière et du résultat de l'organisation. + + + + Le principe d'image fidèle est le fil conducteur de la comptabilité française. Il signifie que les + comptes doivent refléter la réalité économique de l'organisation, quitte à s'écarter dans de rares + cas d'une règle comptable si son application stricte donnerait une image trompeuse. Toute dérogation + doit être expliquée dans l'annexe. + + + + Pour les sociétés, les comptes annuels doivent être présentés et approuvés par l'assemblée générale + des associés ou actionnaires dans les six mois suivant la clôture de l'exercice, puis déposés au + greffe du tribunal de commerce + . + + + + + + En complément des comptes annuels, la comptabilité produit des documents utilisés tout au long de + l'année pour contrôler et piloter l'activité : + + + + Registre chronologique de toutes les écritures passées. C'est le document de base de la + comptabilité. + + + Reprise des mêmes écritures que le journal, mais classées par compte. Il permet d'analyser chaque + compte individuellement. + + + Liste de tous les comptes avec leurs totaux et soldes. C'est l'outil de contrôle qui vérifie que la + comptabilité est équilibrée. + + + + + + Le{" "} + + bénéfice + {" "} + (ou la{" "} + + perte + + ) d'une entreprise peut être calculé de deux manières équivalentes, ce qui constitue un contrôle + fondamental de la comptabilité : + + + + Bénéfice = Patrimoine à la fin - Patrimoine au début. Si l'entreprise possède plus à la fin qu'au + début, elle s'est enrichie. + + + Bénéfice = Produits - Charges. La somme de ce qu'on a gagné moins la somme de ce qu'on a dépensé. + + + + Ces deux calculs doivent donner le même résultat. C'est la cohérence entre le{" "} + bilan (patrimoine) et le{" "} + compte de résultat{" "} + (opérations) qui garantit l'exactitude de la comptabilité. + + + + Cette double vérification est au cœur de la comptabilité en{" "} + + partie double + + . Si les deux méthodes ne donnent pas le même résultat, c'est qu'il y a une erreur dans les + écritures. + + + + + + ) +} diff --git a/packages/website/src/features/docs/accounting/reports/balanceAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/reports/balanceAccountingDocPage.tsx deleted file mode 100644 index 1cae6b37..00000000 --- a/packages/website/src/features/docs/accounting/reports/balanceAccountingDocPage.tsx +++ /dev/null @@ -1,259 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" -import { DocDefinition } from "../../../../components/document/docDefinition.js" -import { DocExample } from "../../../../components/document/docExample.js" -import { DocHeader } from "../../../../components/document/docHeader.js" -import { DocLink } from "../../../../components/document/docLink.js" -import { DocList } from "../../../../components/document/docList.js" -import { DocNextPage } from "../../../../components/document/docNextPage.js" -import { DocParagraph } from "../../../../components/document/docParagraph.js" -import { DocRoot } from "../../../../components/document/docRoot.js" -import { DocSection } from "../../../../components/document/docSection.js" -import { DocSourceRef } from "../../../../components/document/docSourceRef.js" -import { DocSources } from "../../../../components/document/docSources.js" -import { DocTable } from "../../../../components/document/docTable.js" -import { DocTip } from "../../../../components/document/docTip.js" - -export function BalanceAccountingDocPage() { - return ( - - - - - - Tableau récapitulatif de tous les comptes utilisés pendant l'exercice, avec pour chacun le total des - débits, le total des crédits et le solde. C'est l'outil de contrôle fondamental de la comptabilité. - - - - La balance - est un résumé du{" "} - grand livre : elle reprend - chaque{" "} - - compte - {" "} - mais ne conserve que les totaux, sans le détail des mouvements. C'est le document idéal pour - vérifier rapidement que la comptabilité est cohérente. - - - - La balance se construit automatiquement à partir du{" "} - grand livre : pour chaque - compte, on additionne tous les débits, tous les crédits, et on en déduit le solde. Si le grand livre - est juste, la balance l'est aussi. - - - - - - La balance permet de vérifier plusieurs équilibres qui découlent du principe de la{" "} - - partie double - - : - - - - - Si la balance n'est pas équilibrée, cela signifie qu'une ou plusieurs écritures ne respectent pas la - partie double. C'est la première vérification à effectuer avant d'établir le{" "} - bilan et le{" "} - compte de résultat. - - - - - - Reprend tous les comptes utilisés pendant l'exercice. C'est le document de contrôle principal. - - - Détaille un compte collectif (clients ou fournisseurs) en montrant le solde de chaque tiers - individuellement. Par exemple, la balance auxiliaire clients liste le solde dû par chaque client. - - - Ventile les soldes par ancienneté (moins de 30 jours, 30 à 60 jours, etc.). Elle est - particulièrement utile pour suivre les retards de paiement des clients ou des fournisseurs. - - - - - - -

- Total des débits (48 000) = Total des crédits (48 000). Total des soldes débiteurs (35 000) = - Total des soldes créditeurs (35 000). La balance est équilibrée. -

-
-
- - - - La balance est le pont entre les écritures quotidiennes et les documents de synthèse : - - - - - - Une balance juste est la condition nécessaire pour produire un{" "} - bilan et un{" "} - compte de résultat{" "} - fiables. C'est pourquoi la vérification de la balance est une étape incontournable de la clôture - comptable. - - - - - - Arrhes calcule automatiquement la balance à partir de vos{" "} - écritures. Vous pouvez la consulter à - tout moment pour vérifier l'état de votre comptabilité. Consultez le guide sur les{" "} - rapports pour en savoir plus. - - - - - - -
- ) -} diff --git a/packages/website/src/features/docs/accounting/reports/balanceSheetAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/reports/balanceSheetAccountingDocPage.tsx deleted file mode 100644 index f4558c94..00000000 --- a/packages/website/src/features/docs/accounting/reports/balanceSheetAccountingDocPage.tsx +++ /dev/null @@ -1,468 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" -import { DocDefinition } from "../../../../components/document/docDefinition.js" -import { DocExample } from "../../../../components/document/docExample.js" -import { DocHeader } from "../../../../components/document/docHeader.js" -import { DocLink } from "../../../../components/document/docLink.js" -import { DocList } from "../../../../components/document/docList.js" -import { DocNextPage } from "../../../../components/document/docNextPage.js" -import { DocParagraph } from "../../../../components/document/docParagraph.js" -import { DocRoot } from "../../../../components/document/docRoot.js" -import { DocSection } from "../../../../components/document/docSection.js" -import { DocSourceRef } from "../../../../components/document/docSourceRef.js" -import { DocSources } from "../../../../components/document/docSources.js" -import { DocTable } from "../../../../components/document/docTable.js" -import { DocTip } from "../../../../components/document/docTip.js" - -export function BalanceSheetAccountingDocPage() { - return ( - - - - - - Photographie du patrimoine de l'organisation à une date donnée. Il montre ce que l'organisation - possède (actif) et comment elle l'a financé (passif). - - - - Le bilan - est toujours établi à une date précise, généralement la date - de clôture de l'{" "} - - exercice comptable - - . Contrairement au{" "} - compte de résultat{" "} - qui couvre une période, le bilan est un instantané : il décrit la situation patrimoniale à un moment - donné. - - - - - - Le bilan est construit à partir des{" "} - - soldes des{" "} - - comptes d'agents - - {" "} - (classes 1 à 5 du{" "} - - plan comptable - - ) - : - - - - - - - Le bilan se présente sous forme d'un tableau à deux colonnes. À gauche, l'{" "} - - actif - {" "} - (ce que l'organisation possède). À droite, le{" "} - - passif - {" "} - (comment c'est financé). - - - - - - Biens destinés à rester durablement dans l'organisation : terrains, bâtiments, matériel, brevets. - Ils sont enregistrés dans les comptes de classe 2. - - - Éléments qui se renouvellent au cours du cycle d'exploitation : stocks de marchandises (classe 3), - créances clients (classe 4), trésorerie (classe 5). - - - Ressources appartenant aux propriétaires : capital social, réserves, résultat de l'exercice. Ils - représentent ce que l'organisation doit à ses propriétaires. - - - Sommes dues à des tiers : emprunts bancaires, dettes fournisseurs, dettes fiscales et sociales. - - - - - - L'équation fondamentale du bilan est :{" "} - - - Actif - {" "} - ={" "} - - Passif - {" "} - +{" "} - - Résultat - - - . - - - - Le résultat vient équilibrer le bilan : un{" "} - - bénéfice - {" "} - augmente le passif (les{" "} - - capitaux propres - - ), une{" "} - - perte - {" "} - le diminue. Le total de l'actif est donc toujours strictement égal au total du passif. - - - - Si l'actif et le passif ne sont pas égaux, c'est qu'il y a une erreur dans les écritures. La{" "} - balance permet de détecter ce - type de déséquilibre avant d'établir le bilan. - - - - - - En analyse financière, on distingue le haut de bilan et le{" "} - bas de bilan, une distinction essentielle pour comprendre la structure financière - de l'organisation : - - - - Éléments permanents : à l'actif, les immobilisations (biens durables) ; au passif, les capitaux - propres et les dettes à long terme (emprunts). Ces éléments constituent le socle stable du - financement. - - - Éléments circulants : à l'actif, les stocks, créances et trésorerie ; au passif, les dettes à court - terme (fournisseurs, dettes fiscales). Ces éléments se renouvellent au fil de l'activité. - - - - L'équilibre entre haut et bas de bilan est un indicateur clé de la santé financière. Idéalement, les - emplois permanents (immobilisations) doivent être financés par des ressources permanentes (capitaux - propres + dettes à long terme). - - - - - - Différence entre l'actif circulant (hors trésorerie) et les dettes à court terme. Il mesure le - besoin de financement lié au cycle d'exploitation de l'entreprise. - - - - Le BFR - apparaît lorsque l'entreprise doit financer ses stocks et ses créances - clients avant de recevoir les paiements correspondants. Un BFR positif signifie que l'entreprise a - besoin de trésorerie pour fonctionner ; un BFR négatif (rare) signifie que le cycle d'exploitation - génère de la trésorerie. - - - -

- Stocks : 5 000 + Créances clients : 8 000 = Actif circulant d'exploitation : 13 000 -

-

- Dettes fournisseurs : 4 000 + Dettes fiscales : 2 000 = Dettes d'exploitation : 6 000 -

-

- BFR = 13 000 - 6 000 = 7 000 euros -

-

- L'entreprise doit trouver 7 000 euros de financement pour couvrir son cycle d'exploitation. -

-
-
- - - Il existe en pratique plusieurs présentations du bilan selon l'usage : - - - Le bilan au sens strict, établi selon les règles du PCG et déposé au greffe du tribunal de commerce. - C'est le document obligatoire qui fait partie des comptes annuels. - - - Présentation analytique qui reclasse les postes par fonction (exploitation, investissement, - financement). Il sert à l'analyse financière interne et au calcul du BFR. - - - Bilan établi pour les besoins de la déclaration fiscale (liasse fiscale). Il reprend les mêmes - données que le bilan comptable avec des retraitements spécifiques. - - - - - - Certaines obligations de l'organisation n'apparaissent pas directement dans le bilan mais peuvent - représenter des risques ou des avantages significatifs. On les appelle les{" "} - engagements hors bilan : - - - - - - Ces engagements doivent être mentionnés dans l'{" "} - annexe pour que les lecteurs - des comptes aient une image complète de la situation de l'organisation. - - - - En normes IFRS (normes internationales) - , le bilan s'appelle « état de la situation financière » et sa présentation - est différente : les postes sont classés en éléments courants et non courants plutôt qu'en actif - immobilisé et circulant. Les crédits-baux sont également intégrés au bilan (norme IFRS 16). - - - - - -

- Une association présente le bilan suivant au 31 décembre : -

-
-
-

- ACTIF -

-
    -
  • Matériel informatique : 2 000
  • -
  • Créances adhérents : 500
  • -
  • Banque : 4 500
  • -
  • - Total : 7 000 -
  • -
-
-
-

- PASSIF -

-
    -
  • Fonds associatifs : 4 000
  • -
  • Résultat de l'exercice : 2 000
  • -
  • Dettes fournisseurs : 1 000
  • -
  • - Total : 7 000 -
  • -
-
-
-

- L'association possède 7 000 euros de biens, financés par ses fonds propres (6 000 euros, dont le - résultat de l'année) et une dette fournisseur (1 000 euros). Actif = Passif. -

-
-
- - - - Le bilan de clôture d'un exercice devient le bilan d'ouverture de l'exercice - suivant. Les soldes de tous les comptes de bilan (classes 1 à 5) sont reportés. Seuls les comptes de - charges et de produits (classes 6 et 7) sont remis à zéro, car ils alimentent le compte de résultat - d'une seule période. - - - - Cette continuité est un principe fondamental : le patrimoine de l'organisation se transmet d'un - exercice à l'autre sans interruption. - - - - - - -
- ) -} diff --git a/packages/website/src/features/docs/accounting/reports/fecAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/reports/fecAccountingDocPage.tsx deleted file mode 100644 index c330950d..00000000 --- a/packages/website/src/features/docs/accounting/reports/fecAccountingDocPage.tsx +++ /dev/null @@ -1,269 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" -import { DocDefinition } from "../../../../components/document/docDefinition.js" -import { DocExample } from "../../../../components/document/docExample.js" -import { DocHeader } from "../../../../components/document/docHeader.js" -import { DocLink } from "../../../../components/document/docLink.js" -import { DocList } from "../../../../components/document/docList.js" -import { DocNextPage } from "../../../../components/document/docNextPage.js" -import { DocParagraph } from "../../../../components/document/docParagraph.js" -import { DocRoot } from "../../../../components/document/docRoot.js" -import { DocSection } from "../../../../components/document/docSection.js" -import { DocSourceRef } from "../../../../components/document/docSourceRef.js" -import { DocSources } from "../../../../components/document/docSources.js" -import { DocTable } from "../../../../components/document/docTable.js" -import { DocTip } from "../../../../components/document/docTip.js" - -export function FecAccountingDocPage() { - return ( - - - - - - Fichier dématérialisé contenant l'ensemble des écritures comptables d'un exercice, dans un format - normé imposé par l'administration fiscale française. Il doit être remis en cas de contrôle fiscal - informatisé. - - - - Le FEC a été instauré par l'article L.47 A-I du Livre des Procédures Fiscales - , applicable depuis le 1er janvier 2014. Toute organisation tenant une - comptabilité informatisée doit être en mesure de produire ce fichier sur demande du vérificateur. - - - - Pour en savoir plus sur le FEC et ses spécificités, consultez{" "} - - fec.arrhes.com - - . - - - - - - Le FEC est un fichier texte tabulé (séparateur tabulation) encodé en UTF-8 avec BOM. La première - ligne contient les en-têtes normés. Chaque ligne suivante représente un mouvement comptable (une - ligne d' - écriture). - - - Le fichier comporte 18 colonnes obligatoires : - - - - - - - Les montants utilisent la virgule comme séparateur décimal (ex. : 1234,50). Les dates sont au format - AAAAMMJJ sans séparateur. Ces conventions sont imposées par le cahier des charges du FEC - . - - - - - - Le nom du fichier FEC suit une convention stricte : [SIREN]FEC[AAAAMMJJ].txt, où le - SIREN est le numéro d'identification de l'organisation et la date correspond à la date de clôture de - l'exercice. - - - -

- Pour une organisation dont le SIREN est 123456789, avec un exercice clos au 31 décembre 2024 : -

-

- 123456789FEC20241231.txt -

-
- - - Si le numéro SIREN n'est pas renseigné dans les paramètres de l'organisation, le nom du fichier ne - sera pas conforme aux exigences de l'administration fiscale. - -
- - - - L'obligation de présenter un FEC s'applique à toute organisation qui tient sa comptabilité de - manière informatisée, quelle que soit sa taille ou sa forme juridique - . Cela comprend : - - - - - - Seules les micro-entreprises (régime micro-BIC ou micro-BNC) sont dispensées, car elles ne tiennent - pas de comptabilité en partie double. - - - - - - Le défaut de présentation du FEC lors d'un contrôle fiscal peut entraîner des sanctions - significatives : - - - - - - Il est recommandé de vérifier régulièrement que votre FEC est conforme, sans attendre un contrôle - fiscal. Arrhes génère le FEC à partir de vos écritures validées. - - - - - - Arrhes permet d'exporter le FEC directement depuis la page des{" "} - écritures de l'exercice. L'export - respecte le format normé (tabulation, encodage UTF-8 avec BOM, colonnes obligatoires) et nomme - automatiquement le fichier à partir du SIREN de l'organisation. - - - - - - -
- ) -} diff --git a/packages/website/src/features/docs/accounting/reports/incomeStatementAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/reports/incomeStatementAccountingDocPage.tsx deleted file mode 100644 index 19b7a79d..00000000 --- a/packages/website/src/features/docs/accounting/reports/incomeStatementAccountingDocPage.tsx +++ /dev/null @@ -1,437 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" -import { DocDefinition } from "../../../../components/document/docDefinition.js" -import { DocExample } from "../../../../components/document/docExample.js" -import { DocHeader } from "../../../../components/document/docHeader.js" -import { DocLink } from "../../../../components/document/docLink.js" -import { DocList } from "../../../../components/document/docList.js" -import { DocNextPage } from "../../../../components/document/docNextPage.js" -import { DocParagraph } from "../../../../components/document/docParagraph.js" -import { DocRoot } from "../../../../components/document/docRoot.js" -import { DocSection } from "../../../../components/document/docSection.js" -import { DocSourceRef } from "../../../../components/document/docSourceRef.js" -import { DocSources } from "../../../../components/document/docSources.js" -import { DocTip } from "../../../../components/document/docTip.js" - -export function IncomeStatementAccountingDocPage() { - return ( - - - - - - Document qui récapitule l'ensemble des produits et des charges d'un exercice. La différence entre - les deux donne le résultat : bénéfice ou perte. - - - - Alors que le bilan est une - photographie à un instant donné, le compte de résultat est un film : il couvre - toute la durée de l'{" "} - - exercice comptable - - . Il répond à la question : l'organisation a-t-elle gagné ou perdu de l'argent sur la période ? - - - - - - - Le compte de résultat est construit à partir des{" "} - - - comptes d'opérations - - {" "} - (classes 6 et 7 du{" "} - - plan comptable - - ) - . Les comptes de classe 6 enregistrent les{" "} - - charges - {" "} - (ce que l'on dépense), les comptes de classe 7 enregistrent les{" "} - - produits - {" "} - (ce que l'on gagne). - - - Charges : bénéfice (ou excédent pour une association)", - "Si Produits < Charges : perte (ou déficit)", - ]} - /> - - - - - Le compte de résultat distingue trois catégories d'opérations, ce qui permet d'analyser finement la - performance de l'organisation : - - - - Différence entre les produits et les charges liés à l'activité courante (ventes, achats, salaires, - loyer...). C'est l'indicateur principal de la performance opérationnelle. - - - Différence entre les produits financiers (intérêts reçus, gains de change) et les charges - financières (intérêts d'emprunts, pertes de change). - - - Différence entre les produits et charges qui ne relèvent ni de l'exploitation ni du financier : - cessions d'immobilisations, pénalités, subventions exceptionnelles. - - - - Le résultat net est la somme de ces trois résultats, après déduction de l'impôt sur - les bénéfices le cas échéant. On parle aussi de{" "} - Résultat Courant Avant Impôts (RCAI), qui correspond à la somme du résultat - d'exploitation et du résultat financier, avant prise en compte des éléments exceptionnels et de - l'impôt. Le RCAI reflète la performance récurrente de l'entreprise. - - - - La distinction exploitation / financier / exceptionnel est propre au droit comptable français. Les - normes IFRS - n'utilisent pas cette catégorisation et présentent le compte de résultat - différemment. - - - - - -
-
-

- CHARGES -

-
    -
  • Achats de marchandises : 3 000
  • -
  • Loyer : 6 000
  • -
  • Salaires : 20 000
  • -
  • Charges sociales : 8 000
  • -
  • Amortissements : 1 000
  • -
  • Intérêts d'emprunt : 500
  • -
  • - Total : 38 500 -
  • -
-
-
-

- PRODUITS -

-
    -
  • Ventes de marchandises : 25 000
  • -
  • Prestations de services : 12 000
  • -
  • Subventions : 4 000
  • -
  • Intérêts reçus : 100
  • -
  • - Total : 41 100 -
  • -
-
-
-

- Résultat = 41 100 - 38 500 = 2 600 euros (bénéfice) -

-
-
- - - - Pour analyser plus finement la performance d'une entreprise, on calcule des indicateurs - intermédiaires appelés Soldes Intermédiaires de Gestion (SIG) - . Chaque solde s'obtient en cascade à partir du précédent, permettant de - comprendre étape par étape comment se forme le résultat : - - - - Ventes de marchandises - Coût d'achat des marchandises vendues. Indicateur clé pour les activités de - négoce. - - - Marge commerciale + Production - Consommations intermédiaires. Mesure la richesse créée par - l'entreprise grâce à son activité. - - - Valeur ajoutée + Subventions d'exploitation - Impôts et taxes - Charges de personnel. Indicateur de - la rentabilité opérationnelle avant amortissements et provisions. - - - - L'EBE est un indicateur particulièrement surveillé car il reflète la capacité de l'entreprise à - générer de la richesse par son activité courante, indépendamment de sa politique de financement et - d'investissement. - - - - - - Une erreur fréquente consiste à assimiler le résultat du compte de résultat à de l'argent gagné ou - perdu. En réalité, le compte de résultat ne mesure pas des flux de trésorerie mais - des flux d'enrichissement ou d'appauvrissement du patrimoine. - - - -

- Une entreprise facture 50 000 euros de prestations (produits) et engage 30 000 euros de charges. - Son résultat est un bénéfice de 20 000 euros. -

-

- Mais si ses clients n'ont pas encore payé leurs factures, sa trésorerie peut être négative : - elle a un bénéfice comptable mais pas d'argent en banque. -

-

- C'est pourquoi on complète souvent le compte de résultat par un tableau des flux de trésorerie, - qui mesure les mouvements réels d'argent. -

-
- - - Un bénéfice ne signifie pas que l'entreprise a de l'argent disponible, et une perte ne signifie pas - qu'elle n'en a plus. Le résultat mesure la variation du patrimoine, pas celle du compte en banque. - -
- - - - Les{" "} - - capitaux propres - {" "} - représentent ce que l'entreprise doit à ses propriétaires. Ils comprennent plusieurs éléments : - - - - Apports initiaux des associés ou fondateurs. Il reste généralement stable sauf augmentation ou - réduction de capital. - - - Bénéfices des années passées qui ont été conservés dans l'entreprise (non distribués aux associés). - - - Résultat de l'exercice précédent en attente d'affectation (mise en réserve ou distribution). - - - Bénéfice ou perte de l'année en cours, tel que calculé par le compte de résultat. - - - -

- Une entreprise réalise un bénéfice de 10 000 euros. Les associés décident : -

-
    -
  • Distribution de dividendes : 4 000 euros (versés aux associés)
  • -
  • Mise en réserve : 6 000 euros (conservés dans l'entreprise)
  • -
-

- Les réserves augmentent de 6 000 euros, renforçant les capitaux propres. -

-
-
- - - - Le résultat du compte de résultat vient s'ajouter aux{" "} - - capitaux propres - {" "} - du bilan. C'est ce qui fait le lien entre les deux documents : - - - - On peut aussi exprimer le résultat comme la variation du patrimoine net : - -

- Résultat = Variation des créances - Variation des dettes -

-

- Si les créances (ce qu'on possède) ont augmenté de 5 000 euros et les dettes de 2 000 euros, le - résultat est de 3 000 euros : l'entreprise s'est enrichie de la différence. -

-
- - - Retenez que le bilan répond à « combien vaut l'organisation ? » et le compte de résultat à « comment - s'est passée l'année ? ». Les deux documents se rejoignent par le résultat, qui apparaît dans les - capitaux propres du bilan. - -
- - - - Les associations utilisent une présentation adaptée. On parle d'excédent plutôt que - de bénéfice et de déficit plutôt que de perte. Certaines lignes sont spécifiques : - contributions volontaires en nature, valorisation du bénévolat, emplois des contributions. - - - - Le principe reste identique : comparer ce que l'association a reçu (cotisations, subventions, dons, - produits d'activité) à ce qu'elle a dépensé pour déterminer le résultat de l'exercice. - - - - - - -
- ) -} diff --git a/packages/website/src/features/docs/accounting/reports/journalAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/reports/journalAccountingDocPage.tsx deleted file mode 100644 index ea30c4ae..00000000 --- a/packages/website/src/features/docs/accounting/reports/journalAccountingDocPage.tsx +++ /dev/null @@ -1,374 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" -import { DocDefinition } from "../../../../components/document/docDefinition.js" -import { DocExample } from "../../../../components/document/docExample.js" -import { DocHeader } from "../../../../components/document/docHeader.js" -import { DocLink } from "../../../../components/document/docLink.js" -import { DocList } from "../../../../components/document/docList.js" -import { DocNextPage } from "../../../../components/document/docNextPage.js" -import { DocParagraph } from "../../../../components/document/docParagraph.js" -import { DocRoot } from "../../../../components/document/docRoot.js" -import { DocSection } from "../../../../components/document/docSection.js" -import { DocSourceRef } from "../../../../components/document/docSourceRef.js" -import { DocSources } from "../../../../components/document/docSources.js" -import { DocTable } from "../../../../components/document/docTable.js" -import { DocTip } from "../../../../components/document/docTip.js" - -export function JournalAccountingDocPage() { - return ( - - - - - - Registre dans lequel sont inscrites, par ordre chronologique, toutes les écritures comptables de - l'organisation. Chaque écriture y est enregistrée avec sa date, ses comptes mouvementés, ses - montants et un libellé explicatif. - - - - Le journal est le point d'entrée de la comptabilité - : c'est ici que chaque opération est enregistrée pour la première fois. Il - constitue la trace originale et chronologique de l'ensemble des{" "} - écritures passées au cours d'un{" "} - - exercice comptable - - . - - - - - - Chaque ligne du journal représente un mouvement comptable. Une{" "} - - écriture comptable - {" "} - comprend toujours au moins deux lignes (un{" "} - - débit - {" "} - et un{" "} - - crédit - - ), conformément au principe de la{" "} - - partie double - - . - - - - -

- Chaque écriture est équilibrée : le total des débits est égal au total des crédits. -

-
-
- - - - En pratique, pour des raisons d'organisation, les écritures ne sont pas toutes enregistrées dans un - seul journal. On utilise des journaux auxiliaires spécialisés, chacun dédié à un - type d'opération. Cela permet de répartir le travail et de vérifier plus facilement les opérations. - - - - Enregistre toutes les factures fournisseurs reçues. On y trouve les achats à crédit avant leur - règlement. - - - Enregistre toutes les factures clients émises. On y trouve les ventes à crédit avant leur - encaissement. - - - Enregistre tous les mouvements du compte bancaire : encaissements, décaissements, virements. - - - Enregistre tous les mouvements d'espèces : recettes et dépenses en liquide. - - - Enregistre les opérations qui ne rentrent pas dans les autres journaux : salaires, amortissements, - régularisations, écritures de clôture. - - - - L'ensemble de ces journaux auxiliaires forme le journal général, qui centralise - toutes les écritures de l'exercice. Cette organisation permet de répartir le travail de saisie et - facilite les contrôles. - - - - - - Lorsqu'une organisation utilise des journaux auxiliaires, elle doit tenir un{" "} - journal centralisateur (ou livre-journal). Ce document récapitule chaque mois les - totaux de chaque journal auxiliaire, créant ainsi un enregistrement unique et synthétique de toutes - les opérations. - - - - Le journal centralisateur est le document qui a valeur juridique : c'est lui qui est visé par les - obligations légales. Les journaux auxiliaires en sont les documents préparatoires. - - - - Lorsqu'une opération concerne plusieurs journaux auxiliaires (par exemple, un virement de la caisse - vers la banque), on utilise un compte de virements internes (classe 58) pour - assurer la liaison entre les deux journaux sans créer de double enregistrement. - - - - - - En début d'exercice, les soldes des comptes de{" "} - - bilan - {" "} - (classes 1 à 5) doivent être repris de l'exercice précédent. Cette reprise s'effectue par des - écritures d'ouverture enregistrées dans un journal particulier : le{" "} - journal des À-Nouveaux (AN). - - - - Journal spécial qui enregistre les écritures de report des soldes de l'exercice précédent vers le - nouvel exercice. Il porte les soldes de tous les comptes de bilan non soldés. - - - - Les comptes de{" "} - - charges - {" "} - et de{" "} - - produits - {" "} - (classes 6 et 7) ne sont pas reportés : ils sont remis à zéro car ils ne concernent qu'un seul - exercice. Le{" "} - - résultat - {" "} - de l'exercice précédent est affecté aux{" "} - - reports à nouveau - {" "} - ou aux{" "} - - réserves - - . - - - - - - Le journal est un document obligatoire. Plusieurs textes encadrent sa tenue : - - - - Le Code de commerce impose la tenue d'un livre-journal à toute personne ayant la qualité de - commerçant - - , - "Le Code Général des Impôts (CGI) reprend cette obligation pour les besoins fiscaux", - <> - Le Plan Comptable Général (PCG) définit les règles de forme du journal - - , - "L'article R.123-173 du Code de commerce autorise la tenue du journal sur support informatique, à condition de garantir l'authenticité et l'intégrité des écritures", - ]} - /> - - - Le journal doit être conservé pendant 10 ans (obligation comptable) et peut être - exigé en cas de contrôle fiscal, de litige commercial ou de procédure judiciaire. - - - - Le journal doit être tenu sans blanc ni rature. En comptabilité informatisée, les écritures validées - ne peuvent pas être supprimées : toute correction se fait par une écriture de contrepassation. Cette - règle d'irréversibilité garantit la fiabilité de la piste d'audit. - - - - - - Arrhes génère automatiquement le journal à partir de vos{" "} - écritures. Chaque saisie est datée, - numérotée et classée dans le journal approprié. Consultez le guide sur les{" "} - rapports pour apprendre à exporter et - consulter votre journal. - - - - - - -
- ) -} diff --git a/packages/website/src/features/docs/accounting/reports/ledgerAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/reports/ledgerAccountingDocPage.tsx deleted file mode 100644 index bc71db57..00000000 --- a/packages/website/src/features/docs/accounting/reports/ledgerAccountingDocPage.tsx +++ /dev/null @@ -1,290 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" -import { DocDefinition } from "../../../../components/document/docDefinition.js" -import { DocExample } from "../../../../components/document/docExample.js" -import { DocHeader } from "../../../../components/document/docHeader.js" -import { DocLink } from "../../../../components/document/docLink.js" -import { DocList } from "../../../../components/document/docList.js" -import { DocNextPage } from "../../../../components/document/docNextPage.js" -import { DocParagraph } from "../../../../components/document/docParagraph.js" -import { DocRoot } from "../../../../components/document/docRoot.js" -import { DocSection } from "../../../../components/document/docSection.js" -import { DocSourceRef } from "../../../../components/document/docSourceRef.js" -import { DocSources } from "../../../../components/document/docSources.js" -import { DocTable } from "../../../../components/document/docTable.js" -import { DocTip } from "../../../../components/document/docTip.js" - -export function LedgerAccountingDocPage() { - return ( - - - - - - Document qui reprend l'ensemble des comptes de l'organisation avec le détail de tous leurs - mouvements. C'est la vue par compte de la comptabilité, là où le journal offre une vue - chronologique. - - - - Le grand livre - et le{" "} - journal contiennent exactement - les mêmes informations, mais présentées différemment. Le journal classe les écritures par date ; le - grand livre les classe par{" "} - - compte - - . - - - - Bien que le grand livre ne soit pas explicitement mentionné comme document obligatoire au même titre - que le journal, il est indispensable en pratique - : c'est lui qui permet de justifier le solde de chaque compte et donc de - construire la balance, le{" "} - bilan et le{" "} - compte de résultat. - Il fait partie des documents pouvant être exigés lors d'un contrôle fiscal. - - - - - - Pour chaque compte, le grand livre affiche l'ensemble des mouvements avec leur date, leur libellé, - les montants au{" "} - - débit - {" "} - et au{" "} - - crédit - - , ainsi que le solde progressif. - - - - -

- Le solde progressif permet de suivre l'évolution du compte au fil des opérations. -

-
- - - -

- Le solde créditeur indique que l'organisation doit encore 620 euros à ses fournisseurs. -

-
-
- - - - Le journal et le grand livre sont les deux faces d'une même réalité. Le journal répond à la question - « que s'est-il passé à telle date ? », tandis que le grand livre répond à « quelle est la situation - de tel compte ? ». - - - - - - - Le grand livre est indispensable pour : - - - - - - - Certains comptes collectifs regroupent de nombreux tiers : le compte 411 (Clients) ou le compte 401 - (Fournisseurs), par exemple. Pour suivre chaque tiers individuellement, on tient un{" "} - grand livre auxiliaire, qui détaille les mouvements par sous-compte (un par client - ou par fournisseur). - - - - Déclinaison du grand livre qui détaille un compte collectif (clients, fournisseurs) en sous-comptes - individuels. Il permet de connaître le solde dû par chaque tiers sans consulter les écritures une - par une. - - - - Le total du grand livre auxiliaire doit toujours correspondre au solde du compte collectif dans le - grand livre général. Cette vérification est un contrôle important lors de la clôture. - - - - - - Le grand livre du compte 512 (Banque) est l'outil central du rapprochement bancaire - . Cette opération consiste à comparer, ligne par ligne, les mouvements enregistrés dans la - comptabilité avec ceux figurant sur le relevé de banque. - - - - Des écarts sont normaux : un chèque émis peut être enregistré en comptabilité mais pas encore - encaissé par le bénéficiaire, ou inversement, un prélèvement peut apparaître sur le relevé bancaire - avant d'être saisi dans les comptes. Le rapprochement bancaire permet d'identifier et d'expliquer - ces décalages. - - - - Le rapprochement bancaire doit être effectué régulièrement (idéalement chaque mois). C'est un outil - de contrôle interne essentiel qui permet de détecter les erreurs de saisie, les opérations oubliées - ou les mouvements non autorisés. - - - - - - Arrhes génère automatiquement le grand livre à partir de vos écritures. Vous pouvez le consulter - compte par compte ou le télécharger dans sa totalité. Consultez le guide sur les{" "} - rapports pour en savoir plus. - - - - - - -
- ) -} diff --git a/packages/website/src/features/docs/accounting/reports/notesAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/reports/notesAccountingDocPage.tsx deleted file mode 100644 index ef66eb51..00000000 --- a/packages/website/src/features/docs/accounting/reports/notesAccountingDocPage.tsx +++ /dev/null @@ -1,231 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" -import { DocDefinition } from "../../../../components/document/docDefinition.js" -import { DocExample } from "../../../../components/document/docExample.js" -import { DocHeader } from "../../../../components/document/docHeader.js" -import { DocLink } from "../../../../components/document/docLink.js" -import { DocList } from "../../../../components/document/docList.js" -import { DocNextPage } from "../../../../components/document/docNextPage.js" -import { DocParagraph } from "../../../../components/document/docParagraph.js" -import { DocRoot } from "../../../../components/document/docRoot.js" -import { DocSection } from "../../../../components/document/docSection.js" -import { DocSourceRef } from "../../../../components/document/docSourceRef.js" -import { DocSources } from "../../../../components/document/docSources.js" -import { DocTip } from "../../../../components/document/docTip.js" - -export function NotesAccountingDocPage() { - return ( - - - - - - Document obligatoire qui complète le bilan et le compte de résultat. L'annexe fournit les - informations nécessaires à la compréhension des comptes : méthodes comptables utilisées, détail de - certains postes, engagements hors bilan. - - - - Le bilan et le{" "} - compte de résultat{" "} - donnent des chiffres, mais pas toujours les clés pour les interpréter - . C'est le rôle de l'annexe : expliquer comment ces chiffres ont été - construits et quelles informations complémentaires sont nécessaires à leur lecture. - - - - L'annexe est obligatoire en France depuis la réforme du Plan Comptable Général de 1982 - (article 511 du PCG). D'inspiration anglo-saxonne (les{" "} - notes to the financial statements), elle répondait au besoin croissant de transparence - financière. Son objectif principal est de garantir l' - image fidèle de la situation de l'organisation : les comptes annuels doivent - refléter la réalité économique et pas seulement la stricte application des règles comptables. - - - - L'inscription d'une information dans l'annexe ne peut pas se substituer à son inscription au bilan - ou au compte de résultat. Si un élément doit figurer dans un document de synthèse, il doit y figurer - : l'annexe est un complément, pas un remplacement. - - - - - - L'annexe doit inclure toute information significative qui n'apparaît pas directement dans le bilan - ou le compte de résultat. Son contenu varie selon la taille et la nature de l' - - organisation - - , mais elle comprend généralement : - - - - Les conventions appliquées pour évaluer les éléments du bilan et du compte de résultat : méthodes - d'amortissement, règles de comptabilisation des stocks, traitement des opérations en devises - étrangères, etc. - - - Détail des immobilisations et de leurs amortissements, état des provisions, échéancier des créances - et des dettes, variation des capitaux propres. - - - Détail des produits et charges exceptionnels, ventilation du chiffre d'affaires, information sur les - impôts. - - - Obligations qui n'apparaissent pas au bilan mais qui représentent un risque ou un avantage potentiel - : cautions données, crédits-baux, engagements de retraite. - - - Parmi les informations fréquemment requises, on trouve notamment : - - - - - - - Toutes les organisations n'ont pas les mêmes obligations. Le droit français distingue plusieurs - niveaux : - - - - - -

- En France, une entreprise peut bénéficier de l'annexe simplifiée si elle ne dépasse pas deux des - trois seuils suivants : -

-
    -
  • Chiffre d'affaires : 12 millions d'euros
  • -
  • Total du bilan : 6 millions d'euros
  • -
  • Nombre de salariés : 50
  • -
-

- Ces seuils sont régulièrement révisés par le législateur. -

-
- - - Même lorsqu'elle n'est pas obligatoire, l'annexe reste utile pour documenter les choix comptables de - l'organisation et faciliter la compréhension des comptes par les tiers (banquier, commissaire aux - comptes, financeurs). - -
- - - - L'annexe joue un rôle central dans la transparence financière. Elle permet à toute personne qui lit - les comptes de comprendre : - - - - - - Sans l'annexe, le bilan et le compte de résultat ne forment qu'une image partielle de la situation - de l'organisation. C'est pourquoi ces trois documents constituent ensemble les{" "} - comptes annuels et sont indissociables. - - - - - - En normes IFRS, la norme IAS 1 impose des exigences similaires mais plus - structurées. L'annexe doit notamment inclure : - - - - - - Les entreprises cotées en bourse dans l'Union européenne sont tenues d'appliquer les normes IFRS - pour leurs comptes consolidés. L'annexe y est généralement beaucoup plus volumineuse que dans les - comptes établis selon le PCG français. - - - - - - Arrhes vous accompagne dans la préparation de vos comptes annuels. Consultez le guide sur les{" "} - rapports pour savoir comment exporter les - données nécessaires à la rédaction de votre annexe. - - - - - - -
- ) -} diff --git a/packages/website/src/features/docs/accounting/reports/reportsAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/reports/reportsAccountingDocPage.tsx deleted file mode 100644 index 8d137d18..00000000 --- a/packages/website/src/features/docs/accounting/reports/reportsAccountingDocPage.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import { DocDefinition } from "../../../../components/document/docDefinition.js" -import { DocHeader } from "../../../../components/document/docHeader.js" -import { DocLink } from "../../../../components/document/docLink.js" -import { DocList } from "../../../../components/document/docList.js" -import { DocNextPage } from "../../../../components/document/docNextPage.js" -import { DocParagraph } from "../../../../components/document/docParagraph.js" -import { DocRoot } from "../../../../components/document/docRoot.js" -import { DocSection } from "../../../../components/document/docSection.js" -import { DocSourceRef } from "../../../../components/document/docSourceRef.js" -import { DocSources } from "../../../../components/document/docSources.js" -import { DocTip } from "../../../../components/document/docTip.js" - -export function ReportsAccountingDocPage() { - return ( - - - - - - À la fin de chaque{" "} - - exercice comptable - - , les écritures sont agrégées pour produire des documents de synthèse. Ces documents offrent une - vision globale de la situation financière et des performances de l' - - organisation - - . - - - Les deux documents principaux sont le{" "} - - - bilan - - {" "} - et le{" "} - - - compte de résultat - - - . Ils sont complémentaires et se lisent ensemble. - - - - - - En droit français, les comptes annuels désignent les trois documents obligatoires - que toute organisation doit produire à la clôture de chaque exercice - : - - - - - - Ces trois documents forment un tout indissociable. Le bilan et le compte de résultat donnent les - chiffres ; l' - annexe fournit les clés pour - les interpréter. Leur objectif commun est de donner une image fidèle - du patrimoine, de la situation financière et du résultat de l'organisation. - - - - Le principe d'image fidèle est le fil conducteur de la comptabilité française. Il signifie que les - comptes doivent refléter la réalité économique de l'organisation, quitte à s'écarter dans de rares - cas d'une règle comptable si son application stricte donnerait une image trompeuse. Toute dérogation - doit être expliquée dans l'annexe. - - - - Pour les sociétés, les comptes annuels doivent être présentés et approuvés par l'assemblée générale - des associés ou actionnaires dans les six mois suivant la clôture de l'exercice, puis déposés au - greffe du tribunal de commerce - . - - - - - - En complément des comptes annuels, la comptabilité produit des documents utilisés tout au long de - l'année pour contrôler et piloter l'activité : - - - - Registre chronologique de toutes les écritures passées. C'est le document de base de la - comptabilité. - - - Reprise des mêmes écritures que le journal, mais classées par compte. Il permet d'analyser chaque - compte individuellement. - - - Liste de tous les comptes avec leurs totaux et soldes. C'est l'outil de contrôle qui vérifie que la - comptabilité est équilibrée. - - - - - - Le{" "} - - bénéfice - {" "} - (ou la{" "} - - perte - - ) d'une entreprise peut être calculé de deux manières équivalentes, ce qui constitue un contrôle - fondamental de la comptabilité : - - - - Bénéfice = Patrimoine à la fin - Patrimoine au début. Si l'entreprise possède plus à la fin qu'au - début, elle s'est enrichie. - - - Bénéfice = Produits - Charges. La somme de ce qu'on a gagné moins la somme de ce qu'on a dépensé. - - - - Ces deux calculs doivent donner le même résultat. C'est la cohérence entre le{" "} - bilan (patrimoine) et le{" "} - compte de résultat{" "} - (opérations) qui garantit l'exactitude de la comptabilité. - - - - Cette double vérification est au cœur de la comptabilité en{" "} - - partie double - - . Si les deux méthodes ne donnent pas le même résultat, c'est qu'il y a une erreur dans les - écritures. - - - - - - - - ) -} diff --git a/packages/website/src/features/docs/accounting/resources/accounts/AccountResourcesAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/resources/accounts/AccountResourcesAccountingDocPage.tsx new file mode 100644 index 00000000..de0f5d79 --- /dev/null +++ b/packages/website/src/features/docs/accounting/resources/accounts/AccountResourcesAccountingDocPage.tsx @@ -0,0 +1,68 @@ +import { ButtonOutlineContent, LinkButton } from "@arrhes/ui" +import { IconArrowLeft } from "@tabler/icons-react" +import { useParams } from "@tanstack/react-router" +import { DocHeader } from "../../../../../components/document/DocHeader.js" +import { DocRoot } from "../../../../../components/document/DocRoot.js" +import { DataError } from "../../../components/DataError.js" +import { getAccount, getAccountBySlug, getDirectChildren } from "./accountsData.js" +import { AccountInfoCard } from "./components/AccountInfoCard.js" +import { AccountNotFound } from "./components/AccountNotFound.js" +import { AccountScenariosSection } from "./components/AccountScenariosSection.js" +import { ChildrenListSection } from "./components/ChildrenListSection.js" +import { DebitCreditSection } from "./components/DebitCreditSection.js" +import { ParentAccountSection } from "./components/ParentAccountSection.js" +import { SummaryAccountBanner } from "./components/SummaryAccountBanner.js" + +export function AccountResourcesAccountingDocPage() { + const { account: slug } = useParams({ + strict: false, + }) as { + account: string + } + const entry = getAccountBySlug(slug) + + if (!entry) { + return + } + + const parentAccount = entry.parent ? getAccount(entry.parent) : null + const children = getDirectChildren(entry.number) + const isSummary = entry.number.length <= 2 + + return ( + + + } + text="Retour aux comptes" + /> + + + + + + + {isSummary ? ( + <> + + + {parentAccount && } + + + + ) : ( + <> + + + {parentAccount && } + + + )} + + + + ) +} diff --git a/packages/website/src/features/docs/accounting/resources/accounts/AccountsResourcesAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/resources/accounts/AccountsResourcesAccountingDocPage.tsx new file mode 100644 index 00000000..86601a47 --- /dev/null +++ b/packages/website/src/features/docs/accounting/resources/accounts/AccountsResourcesAccountingDocPage.tsx @@ -0,0 +1,265 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconLoader2, IconSearch } from "@tabler/icons-react" +import { useVirtualizer } from "@tanstack/react-virtual" +import { useRef, useState, useTransition } from "react" +import { DocHeader } from "../../../../../components/document/DocHeader.js" +import { DocParagraph } from "../../../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../../../components/document/DocRoot.js" +import { DocSection } from "../../../../../components/document/DocSection.js" +import { DocSourceRef } from "../../../../../components/document/DocSourceRef.js" +import { LinkButton } from "../../../../../components/LinkButton.js" +import { type AccountEntry, accountEntries, searchAccounts } from "./accountsData.js" + +// ── Account row ───────────────────────────────────────────────────────────── + +const ROW_HEIGHT = 36 + +function AccountRow(props: { account: AccountEntry }) { + const { account } = props + const isFacultatif = account.isOptional + const depth = account.number.length - 1 + return ( +
+ + + {account.number} + + + {account.label} + + +
+ ) +} + +// ── Main page ────────────────────────────────────────────────────────────── + +export function AccountsResourcesAccountingDocPage() { + const [query, setQuery] = useState("") + const [filteredAccounts, setFilteredAccounts] = useState(accountEntries) + const [isPending, startTransition] = useTransition() + const parentRef = useRef(null) + const hasQuery = query.trim().length > 0 + + const virtualizer = useVirtualizer({ + count: filteredAccounts.length, + getScrollElement: () => parentRef.current, + estimateSize: () => ROW_HEIGHT, + overscan: 20, + }) + + function handleSearch(value: string) { + setQuery(value) + startTransition(() => { + if (!value.trim()) { + setFilteredAccounts(accountEntries) + } else { + setFilteredAccounts(searchAccounts(value)) + } + }) + } + + return ( + + + + + + Parcourez les {accountEntries.length} comptes du Plan Comptable Général. + Utilisez la barre de recherche pour filtrer par numéro, intitulé ou + description. Les comptes en italique sont facultatifs. Pour une présentation détaillée de + chaque classe, consultez la page{" "} + + classes de comptes + + . + + + + {/* Search bar */} +
+ {isPending ? ( + + ) : ( + + )} + handleSearch(e.target.value)} + className={css({ + width: "100%", + padding: "0.75rem 0.75rem 0.75rem 2.5rem", + fontSize: "sm", + borderRadius: "lg", + border: "1px solid", + borderColor: "neutral/15", + backgroundColor: "white", + color: "neutral", + outline: "none", + _focus: { + borderColor: "primary/50", + boxShadow: "0 0 0 3px token(colors.primary/10)", + }, + _placeholder: { + color: "neutral/40", + }, + transition: "all 0.15s", + })} + /> +
+ + {/* Result count */} + + {filteredAccounts.length} compte{filteredAccounts.length !== 1 ? "s" : ""} + {hasQuery ? " trouvé" : ""} + {hasQuery && filteredAccounts.length !== 1 ? "s" : ""} + + + {/* Virtualized list */} + {filteredAccounts.length > 0 ? ( +
+
+ {virtualizer.getVirtualItems().map((virtualItem) => ( +
+ +
+ ))} +
+
+ ) : ( + !isPending && ( +

+ Aucun compte ne correspond à votre recherche. +

+ ) + )} +
+ ) +} diff --git a/packages/website/src/features/docs/accounting/resources/accounts/accountResourcesAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/resources/accounts/accountResourcesAccountingDocPage.tsx deleted file mode 100644 index 3435ec70..00000000 --- a/packages/website/src/features/docs/accounting/resources/accounts/accountResourcesAccountingDocPage.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { ButtonOutlineContent, LinkButton } from "@arrhes/ui" -import { IconArrowLeft } from "@tabler/icons-react" -import { useParams } from "@tanstack/react-router" -import { DocHeader } from "../../../../../components/document/docHeader.js" -import { DocRoot } from "../../../../../components/document/docRoot.js" -import { DataError } from "../../../components/DataError.js" -import { getAccount, getAccountBySlug, getDirectChildren } from "./accountsData.js" -import { AccountInfoCard } from "./components/accountInfoCard.js" -import { AccountNotFound } from "./components/accountNotFound.js" -import { AccountScenariosSection } from "./components/accountScenariosSection.js" -import { ChildrenListSection } from "./components/childrenListSection.js" -import { DebitCreditSection } from "./components/debitCreditSection.js" -import { ParentAccountSection } from "./components/parentAccountSection.js" -import { SummaryAccountBanner } from "./components/summaryAccountBanner.js" - -export function AccountResourcesAccountingDocPage() { - const { account: slug } = useParams({ - strict: false, - }) as { - account: string - } - const entry = getAccountBySlug(slug) - - if (!entry) { - return - } - - const parentAccount = entry.parent ? getAccount(entry.parent) : null - const children = getDirectChildren(entry.number) - const isSummary = entry.number.length <= 2 - - return ( - - - } - text="Retour aux comptes" - /> - - - - - - - {isSummary ? ( - <> - - - {parentAccount && } - - - - ) : ( - <> - - - {parentAccount && } - - - )} - - - - ) -} diff --git a/packages/website/src/features/docs/accounting/resources/accounts/accountsResourcesAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/resources/accounts/accountsResourcesAccountingDocPage.tsx deleted file mode 100644 index 36faa35d..00000000 --- a/packages/website/src/features/docs/accounting/resources/accounts/accountsResourcesAccountingDocPage.tsx +++ /dev/null @@ -1,265 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconLoader2, IconSearch } from "@tabler/icons-react" -import { useVirtualizer } from "@tanstack/react-virtual" -import { useRef, useState, useTransition } from "react" -import { DocHeader } from "../../../../../components/document/docHeader.js" -import { DocParagraph } from "../../../../../components/document/docParagraph.js" -import { DocRoot } from "../../../../../components/document/docRoot.js" -import { DocSection } from "../../../../../components/document/docSection.js" -import { DocSourceRef } from "../../../../../components/document/docSourceRef.js" -import { LinkButton } from "../../../../../components/linkButton.js" -import { type AccountEntry, accountEntries, searchAccounts } from "./accountsData.js" - -// ── Account row ───────────────────────────────────────────────────────────── - -const ROW_HEIGHT = 36 - -function AccountRow(props: { account: AccountEntry }) { - const { account } = props - const isFacultatif = account.isOptional - const depth = account.number.length - 1 - return ( -
- - - {account.number} - - - {account.label} - - -
- ) -} - -// ── Main page ────────────────────────────────────────────────────────────── - -export function AccountsResourcesAccountingDocPage() { - const [query, setQuery] = useState("") - const [filteredAccounts, setFilteredAccounts] = useState(accountEntries) - const [isPending, startTransition] = useTransition() - const parentRef = useRef(null) - const hasQuery = query.trim().length > 0 - - const virtualizer = useVirtualizer({ - count: filteredAccounts.length, - getScrollElement: () => parentRef.current, - estimateSize: () => ROW_HEIGHT, - overscan: 20, - }) - - function handleSearch(value: string) { - setQuery(value) - startTransition(() => { - if (!value.trim()) { - setFilteredAccounts(accountEntries) - } else { - setFilteredAccounts(searchAccounts(value)) - } - }) - } - - return ( - - - - - - Parcourez les {accountEntries.length} comptes du Plan Comptable Général. - Utilisez la barre de recherche pour filtrer par numéro, intitulé ou - description. Les comptes en italique sont facultatifs. Pour une présentation détaillée de - chaque classe, consultez la page{" "} - - classes de comptes - - . - - - - {/* Search bar */} -
- {isPending ? ( - - ) : ( - - )} - handleSearch(e.target.value)} - className={css({ - width: "100%", - padding: "0.75rem 0.75rem 0.75rem 2.5rem", - fontSize: "sm", - borderRadius: "lg", - border: "1px solid", - borderColor: "neutral/15", - backgroundColor: "white", - color: "neutral", - outline: "none", - _focus: { - borderColor: "primary/50", - boxShadow: "0 0 0 3px token(colors.primary/10)", - }, - _placeholder: { - color: "neutral/40", - }, - transition: "all 0.15s", - })} - /> -
- - {/* Result count */} - - {filteredAccounts.length} compte{filteredAccounts.length !== 1 ? "s" : ""} - {hasQuery ? " trouvé" : ""} - {hasQuery && filteredAccounts.length !== 1 ? "s" : ""} - - - {/* Virtualized list */} - {filteredAccounts.length > 0 ? ( -
-
- {virtualizer.getVirtualItems().map((virtualItem) => ( -
- -
- ))} -
-
- ) : ( - !isPending && ( -

- Aucun compte ne correspond à votre recherche. -

- ) - )} -
- ) -} diff --git a/packages/website/src/features/docs/accounting/resources/accounts/components/AccountInfoCard.tsx b/packages/website/src/features/docs/accounting/resources/accounts/components/AccountInfoCard.tsx new file mode 100644 index 00000000..fc69d565 --- /dev/null +++ b/packages/website/src/features/docs/accounting/resources/accounts/components/AccountInfoCard.tsx @@ -0,0 +1,101 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconBookmark } from "@tabler/icons-react" +import type { AccountEntry } from "../accountsData.js" +import { InfoRow } from "./InfoRow.js" + +export function AccountInfoCard(props: { entry: AccountEntry }) { + const { entry } = props + + return ( +
+
+ + + Fiche du compte + +
+ +
+ + + + + + +
+ + {entry.description && ( +

+ {entry.description} +

+ )} +
+ ) +} diff --git a/packages/website/src/features/docs/accounting/resources/accounts/components/AccountNotFound.tsx b/packages/website/src/features/docs/accounting/resources/accounts/components/AccountNotFound.tsx new file mode 100644 index 00000000..110e4238 --- /dev/null +++ b/packages/website/src/features/docs/accounting/resources/accounts/components/AccountNotFound.tsx @@ -0,0 +1,22 @@ +import { ButtonOutlineContent } from "@arrhes/ui" +import { IconArrowLeft } from "@tabler/icons-react" +import { DocHeader } from "../../../../../../components/document/DocHeader.js" +import { DocRoot } from "../../../../../../components/document/DocRoot.js" +import { LinkButton } from "../../../../../../components/LinkButton.js" + +export function AccountNotFound() { + return ( + + + + } + text="Retour au plan comptable" + /> + + + ) +} diff --git a/packages/website/src/features/docs/accounting/resources/accounts/components/AccountScenariosSection.tsx b/packages/website/src/features/docs/accounting/resources/accounts/components/AccountScenariosSection.tsx new file mode 100644 index 00000000..08875cab --- /dev/null +++ b/packages/website/src/features/docs/accounting/resources/accounts/components/AccountScenariosSection.tsx @@ -0,0 +1,43 @@ +import { ButtonOutlineContent, FormatNull } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconMasksTheater } from "@tabler/icons-react" +import { DocSection } from "../../../../../../components/document/DocSection.js" +import { LinkButton } from "../../../../../../components/LinkButton.js" +import { getScenariosByAccountNumber } from "../../scenarios/scenariosData.js" +import type { AccountEntry } from "../accountsData.js" + +export function AccountScenariosSection(props: { entry: AccountEntry }) { + const { entry } = props + const scenarios = getScenariosByAccountNumber(entry.number) + + return ( + + {scenarios.length === 0 ? ( + + ) : ( +
+ {scenarios.map((scenario) => ( + + } + text={scenario.title} + /> + + ))} +
+ )} +
+ ) +} diff --git a/packages/website/src/features/docs/accounting/resources/accounts/components/ChildrenListSection.tsx b/packages/website/src/features/docs/accounting/resources/accounts/components/ChildrenListSection.tsx new file mode 100644 index 00000000..1d6b712a --- /dev/null +++ b/packages/website/src/features/docs/accounting/resources/accounts/components/ChildrenListSection.tsx @@ -0,0 +1,76 @@ +import { ButtonOutlineContent, FormatNull } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconCornerDownRight } from "@tabler/icons-react" +import { DocSection } from "../../../../../../components/document/DocSection.js" +import { LinkButton } from "../../../../../../components/LinkButton.js" +import type { AccountEntry } from "../accountsData.js" + +export function ChildrenListSection(props: { children: AccountEntry[] }) { + return ( + +
+ {props.children.length === 0 ? ( + + ) : ( + props.children.map((child) => ( + + } + text={undefined} + > +
+ + {child.number} + + + {child.label} + +
+
+
+ )) + )} +
+
+ ) +} diff --git a/packages/website/src/features/docs/accounting/resources/accounts/components/DebitCreditSection.tsx b/packages/website/src/features/docs/accounting/resources/accounts/components/DebitCreditSection.tsx new file mode 100644 index 00000000..b588a442 --- /dev/null +++ b/packages/website/src/features/docs/accounting/resources/accounts/components/DebitCreditSection.tsx @@ -0,0 +1,28 @@ +import { DocSection } from "../../../../../../components/document/DocSection.js" +import { DocTable } from "../../../../../../components/document/DocTable.js" +import type { AccountEntry } from "../accountsData.js" + +export function DebitCreditSection(props: { entry: AccountEntry }) { + const { entry } = props + + return ( + + + + ) +} diff --git a/packages/website/src/features/docs/accounting/resources/accounts/components/infoRow.tsx b/packages/website/src/features/docs/accounting/resources/accounts/components/InfoRow.tsx similarity index 100% rename from packages/website/src/features/docs/accounting/resources/accounts/components/infoRow.tsx rename to packages/website/src/features/docs/accounting/resources/accounts/components/InfoRow.tsx diff --git a/packages/website/src/features/docs/accounting/resources/accounts/components/JournalEntryExamples.tsx b/packages/website/src/features/docs/accounting/resources/accounts/components/JournalEntryExamples.tsx new file mode 100644 index 00000000..0d2d8f58 --- /dev/null +++ b/packages/website/src/features/docs/accounting/resources/accounts/components/JournalEntryExamples.tsx @@ -0,0 +1,99 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconMasksTheater } from "@tabler/icons-react" +import { DocSection } from "../../../../../../components/document/DocSection.js" +import { DocTable } from "../../../../../../components/document/DocTable.js" +import { DocTip } from "../../../../../../components/document/DocTip.js" +import { getScenariosByAccountNumber } from "../../scenarios/scenariosData.js" +import type { AccountEntry } from "../accountsData.js" + +export function JournalEntryExamples(props: { entry: AccountEntry }) { + const { entry } = props + const scenarios = getScenariosByAccountNumber(entry.number) + + if (scenarios.length === 0) return null + + return ( + + {entry.usageTips.length > 0 && ( +
+ {entry.usageTips.map((tip) => ( + + {tip} + + ))} +
+ )} +
+ {scenarios.flatMap((scenario) => + scenario.examples.map((example) => ( +
+
+ + + {example.description} + +
+ +
+ )), + )} +
+
+ ) +} diff --git a/packages/website/src/features/docs/accounting/resources/accounts/components/ParentAccountSection.tsx b/packages/website/src/features/docs/accounting/resources/accounts/components/ParentAccountSection.tsx new file mode 100644 index 00000000..15c975b2 --- /dev/null +++ b/packages/website/src/features/docs/accounting/resources/accounts/components/ParentAccountSection.tsx @@ -0,0 +1,54 @@ +import { ButtonOutlineContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconCornerUpLeft } from "@tabler/icons-react" +import { DocSection } from "../../../../../../components/document/DocSection.js" +import { LinkButton } from "../../../../../../components/LinkButton.js" +import type { AccountEntry } from "../accountsData.js" + +export function ParentAccountSection(props: { parentAccount: AccountEntry }) { + const { parentAccount } = props + + return ( + + + } + text={undefined} + > +
+ + {parentAccount.number} + + + {parentAccount.label} + +
+
+
+
+ ) +} diff --git a/packages/website/src/features/docs/accounting/resources/accounts/components/SummaryAccountBanner.tsx b/packages/website/src/features/docs/accounting/resources/accounts/components/SummaryAccountBanner.tsx new file mode 100644 index 00000000..93eeb95f --- /dev/null +++ b/packages/website/src/features/docs/accounting/resources/accounts/components/SummaryAccountBanner.tsx @@ -0,0 +1,60 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconInfoCircle } from "@tabler/icons-react" +import { DocLink } from "../../../../../../components/document/DocLink.js" +import type { AccountEntry } from "../accountsData.js" + +export function SummaryAccountBanner(props: { entry: AccountEntry }) { + const { entry } = props + + return ( +
+ +
+ + Compte de regroupement + + + Ce compte à {entry.number.length} chiffre{entry.number.length > 1 ? "s" : ""} est un compte de + classification. Il ne peut pas être utilisé directement dans une{" "} + écriture comptable. Les écritures + doivent être passées dans les sous-comptes à 3 chiffres ou plus. + +
+
+ ) +} diff --git a/packages/website/src/features/docs/accounting/resources/accounts/components/accountInfoCard.tsx b/packages/website/src/features/docs/accounting/resources/accounts/components/accountInfoCard.tsx deleted file mode 100644 index e9ba81f4..00000000 --- a/packages/website/src/features/docs/accounting/resources/accounts/components/accountInfoCard.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconBookmark } from "@tabler/icons-react" -import type { AccountEntry } from "../accountsData.js" -import { InfoRow } from "./infoRow.js" - -export function AccountInfoCard(props: { entry: AccountEntry }) { - const { entry } = props - - return ( -
-
- - - Fiche du compte - -
- -
- - - - - - -
- - {entry.description && ( -

- {entry.description} -

- )} -
- ) -} diff --git a/packages/website/src/features/docs/accounting/resources/accounts/components/accountNotFound.tsx b/packages/website/src/features/docs/accounting/resources/accounts/components/accountNotFound.tsx deleted file mode 100644 index 9d4aedae..00000000 --- a/packages/website/src/features/docs/accounting/resources/accounts/components/accountNotFound.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { ButtonOutlineContent } from "@arrhes/ui" -import { IconArrowLeft } from "@tabler/icons-react" -import { DocHeader } from "../../../../../../components/document/docHeader.js" -import { DocRoot } from "../../../../../../components/document/docRoot.js" -import { LinkButton } from "../../../../../../components/linkButton.js" - -export function AccountNotFound() { - return ( - - - - } - text="Retour au plan comptable" - /> - - - ) -} diff --git a/packages/website/src/features/docs/accounting/resources/accounts/components/accountScenariosSection.tsx b/packages/website/src/features/docs/accounting/resources/accounts/components/accountScenariosSection.tsx deleted file mode 100644 index 04ac08fc..00000000 --- a/packages/website/src/features/docs/accounting/resources/accounts/components/accountScenariosSection.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { ButtonOutlineContent, FormatNull } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconMasksTheater } from "@tabler/icons-react" -import { DocSection } from "../../../../../../components/document/docSection.js" -import { LinkButton } from "../../../../../../components/linkButton.js" -import { getScenariosByAccountNumber } from "../../scenarios/scenariosData.js" -import type { AccountEntry } from "../accountsData.js" - -export function AccountScenariosSection(props: { entry: AccountEntry }) { - const { entry } = props - const scenarios = getScenariosByAccountNumber(entry.number) - - return ( - - {scenarios.length === 0 ? ( - - ) : ( -
- {scenarios.map((scenario) => ( - - } - text={scenario.title} - /> - - ))} -
- )} -
- ) -} diff --git a/packages/website/src/features/docs/accounting/resources/accounts/components/childrenListSection.tsx b/packages/website/src/features/docs/accounting/resources/accounts/components/childrenListSection.tsx deleted file mode 100644 index 300160cb..00000000 --- a/packages/website/src/features/docs/accounting/resources/accounts/components/childrenListSection.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { ButtonOutlineContent, FormatNull } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconCornerDownRight } from "@tabler/icons-react" -import { DocSection } from "../../../../../../components/document/docSection.js" -import { LinkButton } from "../../../../../../components/linkButton.js" -import type { AccountEntry } from "../accountsData.js" - -export function ChildrenListSection(props: { children: AccountEntry[] }) { - return ( - -
- {props.children.length === 0 ? ( - - ) : ( - props.children.map((child) => ( - - } - text={undefined} - > -
- - {child.number} - - - {child.label} - -
-
-
- )) - )} -
-
- ) -} diff --git a/packages/website/src/features/docs/accounting/resources/accounts/components/debitCreditSection.tsx b/packages/website/src/features/docs/accounting/resources/accounts/components/debitCreditSection.tsx deleted file mode 100644 index f5745cf8..00000000 --- a/packages/website/src/features/docs/accounting/resources/accounts/components/debitCreditSection.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { DocSection } from "../../../../../../components/document/docSection.js" -import { DocTable } from "../../../../../../components/document/docTable.js" -import type { AccountEntry } from "../accountsData.js" - -export function DebitCreditSection(props: { entry: AccountEntry }) { - const { entry } = props - - return ( - - - - ) -} diff --git a/packages/website/src/features/docs/accounting/resources/accounts/components/journalEntryExamples.tsx b/packages/website/src/features/docs/accounting/resources/accounts/components/journalEntryExamples.tsx deleted file mode 100644 index c92d3eee..00000000 --- a/packages/website/src/features/docs/accounting/resources/accounts/components/journalEntryExamples.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconMasksTheater } from "@tabler/icons-react" -import { DocSection } from "../../../../../../components/document/docSection.js" -import { DocTable } from "../../../../../../components/document/docTable.js" -import { DocTip } from "../../../../../../components/document/docTip.js" -import { getScenariosByAccountNumber } from "../../scenarios/scenariosData.js" -import type { AccountEntry } from "../accountsData.js" - -export function JournalEntryExamples(props: { entry: AccountEntry }) { - const { entry } = props - const scenarios = getScenariosByAccountNumber(entry.number) - - if (scenarios.length === 0) return null - - return ( - - {entry.usageTips.length > 0 && ( -
- {entry.usageTips.map((tip) => ( - - {tip} - - ))} -
- )} -
- {scenarios.flatMap((scenario) => - scenario.examples.map((example) => ( -
-
- - - {example.description} - -
- -
- )), - )} -
-
- ) -} diff --git a/packages/website/src/features/docs/accounting/resources/accounts/components/parentAccountSection.tsx b/packages/website/src/features/docs/accounting/resources/accounts/components/parentAccountSection.tsx deleted file mode 100644 index b3e797ee..00000000 --- a/packages/website/src/features/docs/accounting/resources/accounts/components/parentAccountSection.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { ButtonOutlineContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconCornerUpLeft } from "@tabler/icons-react" -import { DocSection } from "../../../../../../components/document/docSection.js" -import { LinkButton } from "../../../../../../components/linkButton.js" -import type { AccountEntry } from "../accountsData.js" - -export function ParentAccountSection(props: { parentAccount: AccountEntry }) { - const { parentAccount } = props - - return ( - - - } - text={undefined} - > -
- - {parentAccount.number} - - - {parentAccount.label} - -
-
-
-
- ) -} diff --git a/packages/website/src/features/docs/accounting/resources/accounts/components/summaryAccountBanner.tsx b/packages/website/src/features/docs/accounting/resources/accounts/components/summaryAccountBanner.tsx deleted file mode 100644 index 3ff26b33..00000000 --- a/packages/website/src/features/docs/accounting/resources/accounts/components/summaryAccountBanner.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconInfoCircle } from "@tabler/icons-react" -import { DocLink } from "../../../../../../components/document/docLink.js" -import type { AccountEntry } from "../accountsData.js" - -export function SummaryAccountBanner(props: { entry: AccountEntry }) { - const { entry } = props - - return ( -
- -
- - Compte de regroupement - - - Ce compte à {entry.number.length} chiffre{entry.number.length > 1 ? "s" : ""} est un compte de - classification. Il ne peut pas être utilisé directement dans une{" "} - écriture comptable. Les écritures - doivent être passées dans les sous-comptes à 3 chiffres ou plus. - -
-
- ) -} diff --git a/packages/website/src/features/docs/accounting/resources/glossary/GlossaryListItem.tsx b/packages/website/src/features/docs/accounting/resources/glossary/GlossaryListItem.tsx new file mode 100644 index 00000000..257148b2 --- /dev/null +++ b/packages/website/src/features/docs/accounting/resources/glossary/GlossaryListItem.tsx @@ -0,0 +1,79 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import { LinkButton } from "../../../../../components/LinkButton.js" + +export function GlossaryListItem(props: { + term: string + englishTranslation: string + slug: string + definition: string +}) { + return ( + +
+
+ + {props.term} + + + {props.englishTranslation} + +
+ + {props.definition} + +
+
+ ) +} diff --git a/packages/website/src/features/docs/accounting/resources/glossary/GlossaryResourcesAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/resources/glossary/GlossaryResourcesAccountingDocPage.tsx new file mode 100644 index 00000000..5512e362 --- /dev/null +++ b/packages/website/src/features/docs/accounting/resources/glossary/GlossaryResourcesAccountingDocPage.tsx @@ -0,0 +1,167 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconSearch } from "@tabler/icons-react" +import { useState } from "react" +import { DocHeader } from "../../../../../components/document/DocHeader.js" +import { DocLink } from "../../../../../components/document/DocLink.js" +import { DocParagraph } from "../../../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../../../components/document/DocRoot.js" +import { DocTip } from "../../../../../components/document/DocTip.js" +import { GlossaryListItem } from "./GlossaryListItem.js" +import { getGlossaryTermsByLetter, searchGlossaryTerms } from "./glossaryData.js" + +export function GlossaryResourcesAccountingDocPage() { + const [query, setQuery] = useState("") + const isSearching = query.trim().length > 0 + const filteredTerms = searchGlossaryTerms(query) + const termsByLetter = getGlossaryTermsByLetter() + + return ( + + + + + Ce glossaire regroupe les définitions des termes comptables utilisés dans le cours. Cliquez sur un terme + pour voir sa définition complète et les concepts associés. + + + + Ce glossaire est un aide-mémoire. Pour comprendre ces concepts en profondeur, consultez les pages du + cours : Introduction,{" "} + Les comptes,{" "} + Les écritures et{" "} + Les documents. + + + {/* Search bar */} +
+ + setQuery(e.target.value)} + className={css({ + width: "100%", + padding: "0.625rem 0.75rem 0.625rem 2.25rem", + fontSize: "sm", + borderRadius: "lg", + border: "1px solid", + borderColor: "neutral/15", + backgroundColor: "white", + color: "neutral", + outline: "none", + _focus: { + borderColor: "primary/50", + boxShadow: "0 0 0 3px token(colors.primary/10)", + }, + _placeholder: { + color: "neutral/40", + }, + transition: "all 0.15s", + })} + /> +
+ + {/* Search results */} + {isSearching ? ( +
+ + {filteredTerms.length} résultat{filteredTerms.length !== 1 ? "s" : ""} + + {filteredTerms.map((term) => ( + + ))} + {filteredTerms.length === 0 && ( +

+ Aucun terme ne correspond à votre recherche. +

+ )} +
+ ) : ( + /* Alphabetical listing */ +
+ {Array.from(termsByLetter.entries()).map(([letter, terms]) => ( +
+

+ {letter} +

+ {terms.map((term) => ( + + ))} +
+ ))} +
+ )} +
+ ) +} diff --git a/packages/website/src/features/docs/accounting/resources/glossary/GlossaryTermResourcesAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/resources/glossary/GlossaryTermResourcesAccountingDocPage.tsx new file mode 100644 index 00000000..4851095e --- /dev/null +++ b/packages/website/src/features/docs/accounting/resources/glossary/GlossaryTermResourcesAccountingDocPage.tsx @@ -0,0 +1,137 @@ +import { ButtonOutlineContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconArrowLeft, IconLanguage, IconLink } from "@tabler/icons-react" +import { useParams } from "@tanstack/react-router" +import { DocDefinition } from "../../../../../components/document/DocDefinition.js" +import { DocHeader } from "../../../../../components/document/DocHeader.js" +import { DocParagraph } from "../../../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../../../components/document/DocRoot.js" +import { DocSection } from "../../../../../components/document/DocSection.js" +import { DocSources } from "../../../../../components/document/DocSources.js" +import { DocTip } from "../../../../../components/document/DocTip.js" +import { LinkButton } from "../../../../../components/LinkButton.js" +import { getGlossaryTermBySlug, glossaryTerms } from "./glossaryData.js" + +export function GlossaryTermResourcesAccountingDocPage() { + const { term: slug } = useParams({ + strict: false, + }) as { + term: string + } + const entry = getGlossaryTermBySlug(slug) + + if (!entry) { + return ( + + + + } + text="Retour au glossaire" + /> + + + ) + } + + return ( + + + } + text="Retour au glossaire" + /> + + + + + {/* Definition card */} +
+ {entry.definition} + + {entry.englishTranslation} + +
+ + {/* Related terms */} + {entry.relatedTerms && entry.relatedTerms.length > 0 && ( + +
+ {entry.relatedTerms.map((related) => { + const relatedEntry = glossaryTerms.find((t) => t.term === related) + if (!relatedEntry) return null + return ( + + } + text={relatedEntry.term} + /> + + ) + })} +
+
+ )} + + {/* Related pages */} + {entry.relatedPages && entry.relatedPages.length > 0 && ( + + + {entry.relatedPages.map((page) => ( + + } + text={page.label} + /> + + ))} + + + )} + + +
+ ) +} diff --git a/packages/website/src/features/docs/accounting/resources/glossary/glossaryListItem.tsx b/packages/website/src/features/docs/accounting/resources/glossary/glossaryListItem.tsx deleted file mode 100644 index 22241fc7..00000000 --- a/packages/website/src/features/docs/accounting/resources/glossary/glossaryListItem.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" -import { LinkButton } from "../../../../../components/linkButton.js" - -export function GlossaryListItem(props: { - term: string - englishTranslation: string - slug: string - definition: string -}) { - return ( - -
-
- - {props.term} - - - {props.englishTranslation} - -
- - {props.definition} - -
-
- ) -} diff --git a/packages/website/src/features/docs/accounting/resources/glossary/glossaryResourcesAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/resources/glossary/glossaryResourcesAccountingDocPage.tsx deleted file mode 100644 index 90ad6815..00000000 --- a/packages/website/src/features/docs/accounting/resources/glossary/glossaryResourcesAccountingDocPage.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconSearch } from "@tabler/icons-react" -import { useState } from "react" -import { DocHeader } from "../../../../../components/document/docHeader.js" -import { DocLink } from "../../../../../components/document/docLink.js" -import { DocParagraph } from "../../../../../components/document/docParagraph.js" -import { DocRoot } from "../../../../../components/document/docRoot.js" -import { DocTip } from "../../../../../components/document/docTip.js" -import { getGlossaryTermsByLetter, searchGlossaryTerms } from "./glossaryData.js" -import { GlossaryListItem } from "./glossaryListItem.js" - -export function GlossaryResourcesAccountingDocPage() { - const [query, setQuery] = useState("") - const isSearching = query.trim().length > 0 - const filteredTerms = searchGlossaryTerms(query) - const termsByLetter = getGlossaryTermsByLetter() - - return ( - - - - - Ce glossaire regroupe les définitions des termes comptables utilisés dans le cours. Cliquez sur un terme - pour voir sa définition complète et les concepts associés. - - - - Ce glossaire est un aide-mémoire. Pour comprendre ces concepts en profondeur, consultez les pages du - cours : Introduction,{" "} - Les comptes,{" "} - Les écritures et{" "} - Les documents. - - - {/* Search bar */} -
- - setQuery(e.target.value)} - className={css({ - width: "100%", - padding: "0.625rem 0.75rem 0.625rem 2.25rem", - fontSize: "sm", - borderRadius: "lg", - border: "1px solid", - borderColor: "neutral/15", - backgroundColor: "white", - color: "neutral", - outline: "none", - _focus: { - borderColor: "primary/50", - boxShadow: "0 0 0 3px token(colors.primary/10)", - }, - _placeholder: { - color: "neutral/40", - }, - transition: "all 0.15s", - })} - /> -
- - {/* Search results */} - {isSearching ? ( -
- - {filteredTerms.length} résultat{filteredTerms.length !== 1 ? "s" : ""} - - {filteredTerms.map((term) => ( - - ))} - {filteredTerms.length === 0 && ( -

- Aucun terme ne correspond à votre recherche. -

- )} -
- ) : ( - /* Alphabetical listing */ -
- {Array.from(termsByLetter.entries()).map(([letter, terms]) => ( -
-

- {letter} -

- {terms.map((term) => ( - - ))} -
- ))} -
- )} -
- ) -} diff --git a/packages/website/src/features/docs/accounting/resources/glossary/glossaryTermResourcesAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/resources/glossary/glossaryTermResourcesAccountingDocPage.tsx deleted file mode 100644 index 86f97ccd..00000000 --- a/packages/website/src/features/docs/accounting/resources/glossary/glossaryTermResourcesAccountingDocPage.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { ButtonOutlineContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconArrowLeft, IconLanguage, IconLink } from "@tabler/icons-react" -import { useParams } from "@tanstack/react-router" -import { DocDefinition } from "../../../../../components/document/docDefinition.js" -import { DocHeader } from "../../../../../components/document/docHeader.js" -import { DocParagraph } from "../../../../../components/document/docParagraph.js" -import { DocRoot } from "../../../../../components/document/docRoot.js" -import { DocSection } from "../../../../../components/document/docSection.js" -import { DocSources } from "../../../../../components/document/docSources.js" -import { DocTip } from "../../../../../components/document/docTip.js" -import { LinkButton } from "../../../../../components/linkButton.js" -import { getGlossaryTermBySlug, glossaryTerms } from "./glossaryData.js" - -export function GlossaryTermResourcesAccountingDocPage() { - const { term: slug } = useParams({ - strict: false, - }) as { - term: string - } - const entry = getGlossaryTermBySlug(slug) - - if (!entry) { - return ( - - - - } - text="Retour au glossaire" - /> - - - ) - } - - return ( - - - } - text="Retour au glossaire" - /> - - - - - {/* Definition card */} -
- {entry.definition} - - {entry.englishTranslation} - -
- - {/* Related terms */} - {entry.relatedTerms && entry.relatedTerms.length > 0 && ( - -
- {entry.relatedTerms.map((related) => { - const relatedEntry = glossaryTerms.find((t) => t.term === related) - if (!relatedEntry) return null - return ( - - } - text={relatedEntry.term} - /> - - ) - })} -
-
- )} - - {/* Related pages */} - {entry.relatedPages && entry.relatedPages.length > 0 && ( - - - {entry.relatedPages.map((page) => ( - - } - text={page.label} - /> - - ))} - - - )} - - -
- ) -} diff --git a/packages/website/src/features/docs/accounting/resources/scenarios/ScenarioResourcesAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/resources/scenarios/ScenarioResourcesAccountingDocPage.tsx new file mode 100644 index 00000000..5598633a --- /dev/null +++ b/packages/website/src/features/docs/accounting/resources/scenarios/ScenarioResourcesAccountingDocPage.tsx @@ -0,0 +1,103 @@ +import { ButtonOutlineContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconArrowLeft, IconLink } from "@tabler/icons-react" +import { useParams } from "@tanstack/react-router" +import { DocHeader } from "../../../../../components/document/DocHeader.js" +import { DocParagraph } from "../../../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../../../components/document/DocRoot.js" +import { DocSection } from "../../../../../components/document/DocSection.js" +import { DocTable } from "../../../../../components/document/DocTable.js" +import { LinkButton } from "../../../../../components/LinkButton.js" +import { DataError } from "../../../components/DataError.js" +import { getScenarioAccounts, getScenarioById } from "./scenariosData.js" + +export function ScenarioResourcesAccountingDocPage() { + const { scenario: id } = useParams({ + strict: false, + }) as { + scenario: string + } + const scenario = getScenarioById(id) + + if (!scenario) { + return ( + + + + } + text="Retour aux scénarios" + /> + + + ) + } + + const accounts = getScenarioAccounts(scenario) + + return ( + + + } + text="Retour aux scénarios" + /> + + + + + {scenario.description} + + {scenario.examples.map((example, index) => ( + 1 ? `Exemple ${index + 1}` : "Exemple d'écriture"} + > + {example.description} + + + ))} + + +
+ {accounts.map((account) => ( + + } + text={`${account.number} - ${account.label}`} + /> + + ))} +
+
+ + +
+ ) +} diff --git a/packages/website/src/features/docs/accounting/resources/scenarios/ScenariosResourcesAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/resources/scenarios/ScenariosResourcesAccountingDocPage.tsx new file mode 100644 index 00000000..4c90e4dd --- /dev/null +++ b/packages/website/src/features/docs/accounting/resources/scenarios/ScenariosResourcesAccountingDocPage.tsx @@ -0,0 +1,202 @@ +import { ButtonGhostContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconLoader2, IconSearch } from "@tabler/icons-react" +import { useVirtualizer } from "@tanstack/react-virtual" +import { useRef, useState, useTransition } from "react" +import { DocHeader } from "../../../../../components/document/DocHeader.js" +import { DocParagraph } from "../../../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../../../components/document/DocRoot.js" +import { DocSection } from "../../../../../components/document/DocSection.js" +import { LinkButton } from "../../../../../components/LinkButton.js" +import { type ScenarioEntry, scenarioEntries, searchScenarios } from "./scenariosData.js" + +const ROW_HEIGHT = 32 +const ROW_GAP = 4 // 0.25rem + +function ScenarioRow(props: { scenario: ScenarioEntry }) { + const { scenario } = props + + return ( + + + + ) +} + +export function ScenariosResourcesAccountingDocPage() { + const [query, setQuery] = useState("") + const [filteredScenarios, setFilteredScenarios] = useState(scenarioEntries) + const [isPending, startTransition] = useTransition() + const parentRef = useRef(null) + const hasQuery = query.trim().length > 0 + + const virtualizer = useVirtualizer({ + count: filteredScenarios.length, + getScrollElement: () => parentRef.current, + estimateSize: () => ROW_HEIGHT + ROW_GAP, + overscan: 20, + }) + + function handleSearch(value: string) { + setQuery(value) + startTransition(() => { + if (!value.trim()) { + setFilteredScenarios(scenarioEntries) + } else { + setFilteredScenarios(searchScenarios(value)) + } + }) + } + + return ( + + + + + + Cette page regroupe les cas d'usage issus des exemples de comptes. Chaque scénario contient un + exemple d'écriture et les comptes concernés pour faciliter la navigation entre pratique et théorie. + + + +
+ {isPending ? ( + + ) : ( + + )} + handleSearch(e.target.value)} + className={css({ + width: "100%", + padding: "0.75rem 0.75rem 0.75rem 2.5rem", + fontSize: "sm", + borderRadius: "lg", + border: "1px solid", + borderColor: "neutral/15", + backgroundColor: "white", + color: "neutral", + outline: "none", + _focus: { + borderColor: "primary/50", + boxShadow: "0 0 0 3px token(colors.primary/10)", + }, + _placeholder: { + color: "neutral/40", + }, + transition: "all 0.15s", + })} + /> +
+ + + {filteredScenarios.length} scénario{filteredScenarios.length !== 1 ? "s" : ""} + {hasQuery ? " trouvé" : ""} + {hasQuery && filteredScenarios.length !== 1 ? "s" : ""} + + + {filteredScenarios.length > 0 ? ( +
+
+ {virtualizer.getVirtualItems().map((virtualItem) => ( +
+ +
+ ))} +
+
+ ) : ( + !isPending && ( +

+ Aucun scénario ne correspond à votre recherche. +

+ ) + )} +
+ ) +} diff --git a/packages/website/src/features/docs/accounting/resources/scenarios/scenarioResourcesAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/resources/scenarios/scenarioResourcesAccountingDocPage.tsx deleted file mode 100644 index be24fef3..00000000 --- a/packages/website/src/features/docs/accounting/resources/scenarios/scenarioResourcesAccountingDocPage.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { ButtonOutlineContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconArrowLeft, IconLink } from "@tabler/icons-react" -import { useParams } from "@tanstack/react-router" -import { DocHeader } from "../../../../../components/document/docHeader.js" -import { DocParagraph } from "../../../../../components/document/docParagraph.js" -import { DocRoot } from "../../../../../components/document/docRoot.js" -import { DocSection } from "../../../../../components/document/docSection.js" -import { DocTable } from "../../../../../components/document/docTable.js" -import { LinkButton } from "../../../../../components/linkButton.js" -import { DataError } from "../../../components/DataError.js" -import { getScenarioAccounts, getScenarioById } from "./scenariosData.js" - -export function ScenarioResourcesAccountingDocPage() { - const { scenario: id } = useParams({ - strict: false, - }) as { - scenario: string - } - const scenario = getScenarioById(id) - - if (!scenario) { - return ( - - - - } - text="Retour aux scénarios" - /> - - - ) - } - - const accounts = getScenarioAccounts(scenario) - - return ( - - - } - text="Retour aux scénarios" - /> - - - - - {scenario.description} - - {scenario.examples.map((example, index) => ( - 1 ? `Exemple ${index + 1}` : "Exemple d'écriture"} - > - {example.description} - - - ))} - - -
- {accounts.map((account) => ( - - } - text={`${account.number} - ${account.label}`} - /> - - ))} -
-
- - -
- ) -} diff --git a/packages/website/src/features/docs/accounting/resources/scenarios/scenariosResourcesAccountingDocPage.tsx b/packages/website/src/features/docs/accounting/resources/scenarios/scenariosResourcesAccountingDocPage.tsx deleted file mode 100644 index 5c2469db..00000000 --- a/packages/website/src/features/docs/accounting/resources/scenarios/scenariosResourcesAccountingDocPage.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import { ButtonGhostContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconLoader2, IconSearch } from "@tabler/icons-react" -import { useVirtualizer } from "@tanstack/react-virtual" -import { useRef, useState, useTransition } from "react" -import { DocHeader } from "../../../../../components/document/docHeader.js" -import { DocParagraph } from "../../../../../components/document/docParagraph.js" -import { DocRoot } from "../../../../../components/document/docRoot.js" -import { DocSection } from "../../../../../components/document/docSection.js" -import { LinkButton } from "../../../../../components/linkButton.js" -import { type ScenarioEntry, scenarioEntries, searchScenarios } from "./scenariosData.js" - -const ROW_HEIGHT = 32 -const ROW_GAP = 4 // 0.25rem - -function ScenarioRow(props: { scenario: ScenarioEntry }) { - const { scenario } = props - - return ( - - - - ) -} - -export function ScenariosResourcesAccountingDocPage() { - const [query, setQuery] = useState("") - const [filteredScenarios, setFilteredScenarios] = useState(scenarioEntries) - const [isPending, startTransition] = useTransition() - const parentRef = useRef(null) - const hasQuery = query.trim().length > 0 - - const virtualizer = useVirtualizer({ - count: filteredScenarios.length, - getScrollElement: () => parentRef.current, - estimateSize: () => ROW_HEIGHT + ROW_GAP, - overscan: 20, - }) - - function handleSearch(value: string) { - setQuery(value) - startTransition(() => { - if (!value.trim()) { - setFilteredScenarios(scenarioEntries) - } else { - setFilteredScenarios(searchScenarios(value)) - } - }) - } - - return ( - - - - - - Cette page regroupe les cas d'usage issus des exemples de comptes. Chaque scénario contient un - exemple d'écriture et les comptes concernés pour faciliter la navigation entre pratique et théorie. - - - -
- {isPending ? ( - - ) : ( - - )} - handleSearch(e.target.value)} - className={css({ - width: "100%", - padding: "0.75rem 0.75rem 0.75rem 2.5rem", - fontSize: "sm", - borderRadius: "lg", - border: "1px solid", - borderColor: "neutral/15", - backgroundColor: "white", - color: "neutral", - outline: "none", - _focus: { - borderColor: "primary/50", - boxShadow: "0 0 0 3px token(colors.primary/10)", - }, - _placeholder: { - color: "neutral/40", - }, - transition: "all 0.15s", - })} - /> -
- - - {filteredScenarios.length} scénario{filteredScenarios.length !== 1 ? "s" : ""} - {hasQuery ? " trouvé" : ""} - {hasQuery && filteredScenarios.length !== 1 ? "s" : ""} - - - {filteredScenarios.length > 0 ? ( -
-
- {virtualizer.getVirtualItems().map((virtualItem) => ( -
- -
- ))} -
-
- ) : ( - !isPending && ( -

- Aucun scénario ne correspond à votre recherche. -

- ) - )} -
- ) -} diff --git a/packages/website/src/features/docs/ai/ModelsAiDocPage.tsx b/packages/website/src/features/docs/ai/ModelsAiDocPage.tsx new file mode 100644 index 00000000..fea44ee8 --- /dev/null +++ b/packages/website/src/features/docs/ai/ModelsAiDocPage.tsx @@ -0,0 +1,96 @@ +import { DocHeader } from "../../../components/document/DocHeader.tsx" +import { DocList } from "../../../components/document/DocList.tsx" +import { DocParagraph } from "../../../components/document/DocParagraph.tsx" +import { DocRoot } from "../../../components/document/DocRoot.tsx" +import { DocSection } from "../../../components/document/DocSection.tsx" +import { DocTable } from "../../../components/document/DocTable.tsx" +import { DocTip } from "../../../components/document/DocTip.tsx" + +export function ModelsAiDocPage() { + return ( + + + + + + L'assistant utilise deux modèles distincts dans son architecture en deux passes : + + + + + + + L'assistant utilise actuellement le modèle Mistral Small (mistral-small-2603) de + Mistral AI. Ce modèle offre un bon équilibre entre performance et coût pour les tâches comptables : + + + + + + + L'infrastructure d'Arrhes est conçue pour être agnostique vis-à-vis du fournisseur de modèle. Deux + fournisseurs sont actuellement supportés : + + + + Le choix du fournisseur est configuré au niveau de l'infrastructure et ne nécessite aucune action de + la part de l'utilisateur. + + + + + + Comme tout modèle de langage, l'assistant peut occasionnellement produire des réponses inexactes. Il + est recommandé de vérifier les résultats pour les opérations critiques. L'assistant est limité à 10 + appels d'outils par message pour garantir des temps de réponse raisonnables. + + + + ) +} diff --git a/packages/website/src/features/docs/ai/OcrAiDocPage.tsx b/packages/website/src/features/docs/ai/OcrAiDocPage.tsx new file mode 100644 index 00000000..b1b39cbb --- /dev/null +++ b/packages/website/src/features/docs/ai/OcrAiDocPage.tsx @@ -0,0 +1,56 @@ +import { DocCode } from "../../../components/document/DocCode.tsx" +import { DocHeader } from "../../../components/document/DocHeader.tsx" +import { DocLink } from "../../../components/document/DocLink.tsx" +import { DocList } from "../../../components/document/DocList.tsx" +import { DocParagraph } from "../../../components/document/DocParagraph.tsx" +import { DocRoot } from "../../../components/document/DocRoot.tsx" +import { DocSection } from "../../../components/document/DocSection.tsx" +import { DocTip } from "../../../components/document/DocTip.tsx" + +export function OcrAiDocPage() { + return ( + + + + + + La fonctionnalité OCR permet d'extraire automatiquement le texte depuis un document source (image ou + PDF) et de créer un nouveau fichier au format Markdown dans votre espace de stockage. + + + L'OCR est disponible depuis l'interface de stockage et via l'outil assistant{" "} + ocr_file. + + + + + L'OCR peut être utilisé pour : + + + + + + + + + Pour voir la liste complète des capacités de l'assistant, consultez la page{" "} + Outils. + + + ) +} diff --git a/packages/website/src/features/docs/ai/RootAiDocPage.tsx b/packages/website/src/features/docs/ai/RootAiDocPage.tsx new file mode 100644 index 00000000..4f23a063 --- /dev/null +++ b/packages/website/src/features/docs/ai/RootAiDocPage.tsx @@ -0,0 +1,115 @@ +import { ButtonOutlineContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconChevronRight } from "@tabler/icons-react" +import { DocHeader } from "../../../components/document/DocHeader.tsx" +import { DocLink } from "../../../components/document/DocLink.tsx" +import { DocList } from "../../../components/document/DocList.tsx" +import { DocParagraph } from "../../../components/document/DocParagraph.tsx" +import { DocRoot } from "../../../components/document/DocRoot.tsx" +import { DocSection } from "../../../components/document/DocSection.tsx" +import { DocTip } from "../../../components/document/DocTip.tsx" +import { LinkButton } from "../../../components/LinkButton.tsx" + +export function RootAiDocPage() { + return ( + + + +
+

+ L'assistant IA d'Arrhes est un agent comptable conversationnel capable de consulter, créer, modifier + et supprimer vos données comptables. Il comprend le langage naturel et utilise plus de 70 outils + spécialisés pour répondre à vos demandes. +

+
+ + } + /> + +
+
+ + + L'assistant IA est une fonctionnalité premium. Un abonnement au plan Avancé est requis pour y accéder. + + + + Politique d'usage raisonnable: les requêtes assistant sont limitées à 500 messages par mois et par + organisation (réinitialisation au début de chaque mois calendaire). + + + + L'assistant comptable peut vous aider pour : + + + + + + L'assistant utilise une architecture en deux passes pour répondre à vos demandes : + + + + L'assistant conserve l'historique de la conversation pour maintenir le contexte au fil des échanges. + Vous pouvez également présélectionner un exercice fiscal et fournir des instructions personnalisées + pour affiner les réponses. + + + + + + L'assistant opère exclusivement dans le périmètre de votre organisation. Il ne peut accéder qu'aux + données de l'organisation sélectionnée et les actions sont soumises aux mêmes contrôles de + permissions que l'interface web. Aucune donnée n'est partagée entre organisations. + + + + + Pour en savoir plus sur les modèles utilisés, consultez la page{" "} + Modèles. Pour la liste complète des + outils disponibles, consultez la page{" "} + Outils. + +
+ ) +} diff --git a/packages/website/src/features/docs/ai/ToolsAiDocPage.tsx b/packages/website/src/features/docs/ai/ToolsAiDocPage.tsx new file mode 100644 index 00000000..ad82ead0 --- /dev/null +++ b/packages/website/src/features/docs/ai/ToolsAiDocPage.tsx @@ -0,0 +1,280 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import { useMemo, useState } from "react" +import { DocHeader } from "../../../components/document/DocHeader.tsx" +import { DocList } from "../../../components/document/DocList.tsx" +import { DocParagraph } from "../../../components/document/DocParagraph.tsx" +import { DocRoot } from "../../../components/document/DocRoot.tsx" +import { DocSection } from "../../../components/document/DocSection.tsx" +import { DocTip } from "../../../components/document/DocTip.tsx" +import { type AgentToolDefinition, agentToolsCatalog } from "../../dashboard/$idOrganization/agent/agentToolsCatalog.ts" + +const categoryOrder = [ + "Exercices fiscaux", + "Écritures comptables", + "Lignes d'écriture", + "Étiquettes d'écriture", + "Plan comptable", + "Journaux", + "Étiquettes", + "Bilan", + "Compte de résultat", + "Calculs et rubriques", + "Fichiers", + "Dossiers", + "Rapports", + "Exercice (général)", + "Documentation", + "Traitement de données", + "Autres", +] as const + +function getToolCategory(toolName: string): string { + if (toolName === "search_documentation") return "Documentation" + if (toolName === "process_array") return "Traitement de données" + + if (toolName === "read_all_years") return "Exercices fiscaux" + if (toolName.includes("one_year") || toolName.startsWith("close_year") || toolName.startsWith("open_year")) { + return "Exercice (général)" + } + + if (toolName.includes("entry_line")) return "Lignes d'écriture" + if (toolName.includes("entry_tag")) return "Étiquettes d'écriture" + if (toolName.includes("entry") || toolName.includes("entries")) return "Écritures comptables" + if (toolName.includes("account")) return "Plan comptable" + if (toolName.includes("journal")) return "Journaux" + if (toolName.includes("tag")) return "Étiquettes" + if (toolName.includes("balance_sheet")) return "Bilan" + if (toolName.includes("income_statement")) return "Compte de résultat" + if (toolName.includes("computation")) return "Calculs et rubriques" + if (toolName.includes("file")) return "Fichiers" + if (toolName.includes("folder")) return "Dossiers" + if (toolName.includes("document") || toolName.includes("report")) return "Rapports" + + return "Autres" +} + +export function ToolsAiDocPage() { + const [search, setSearch] = useState("") + + const groupedTools = useMemo(() => { + const normalizedSearch = search.trim().toLowerCase() + + const filteredTools = normalizedSearch.length + ? agentToolsCatalog.filter((tool) => { + const category = getToolCategory(tool.name) + return ( + tool.name.toLowerCase().includes(normalizedSearch) || + tool.labelFr.toLowerCase().includes(normalizedSearch) || + tool.descriptionFr.toLowerCase().includes(normalizedSearch) || + category.toLowerCase().includes(normalizedSearch) + ) + }) + : agentToolsCatalog + + const grouped = new Map() + + for (const tool of filteredTools) { + const category = getToolCategory(tool.name) + const bucket = grouped.get(category) + if (bucket) { + bucket.push(tool) + } else { + grouped.set(category, [ + tool, + ]) + } + } + + for (const [, tools] of grouped) { + tools.sort((a, b) => a.labelFr.localeCompare(b.labelFr, "fr")) + } + + return [ + ...grouped.entries(), + ].sort((a, b) => { + const indexA = categoryOrder.indexOf(a[0] as (typeof categoryOrder)[number]) + const indexB = categoryOrder.indexOf(b[0] as (typeof categoryOrder)[number]) + const safeA = indexA === -1 ? categoryOrder.length : indexA + const safeB = indexB === -1 ? categoryOrder.length : indexB + return safeA - safeB + }) + }, [ + search, + ]) + + const totalToolCount = agentToolsCatalog.length + + return ( + + + + + + Les outils sont les actions concrètes que l'assistant IA peut effectuer sur vos données comptables. + Lorsque vous envoyez un message, l'assistant analyse votre demande, identifie les outils + nécessaires, puis les exécute automatiquement pour vous fournir une réponse. + + + Un seul message peut déclencher plusieurs outils en chaîne. Par exemple, pour répondre à « Quel est + le solde du compte 411 ? », l'assistant peut d'abord rechercher le compte, puis consulter ses + mouvements, et enfin calculer le solde. + + + + + + Vous ne trouvez pas l'outil dont vous avez besoin, ou l'assistant ne parvient pas à réaliser une tâche ? + N'hésitez pas à nous contacter pour suggérer de nouveaux outils. L'assistant est en amélioration + continue et de nouvelles capacités sont ajoutées régulièrement. + + + {/* Search bar */} +
+ setSearch(e.target.value)} + placeholder="Rechercher un outil..." + className={css({ + width: "100%", + maxWidth: "24rem", + padding: "0.5rem 0.75rem", + borderRadius: "md", + border: "1px solid", + borderColor: "neutral/20", + fontSize: "sm", + color: "neutral", + outline: "none", + _focus: { + borderColor: "primary", + }, + _placeholder: { + color: "neutral/30", + }, + })} + /> +
+ + {/* Tool categories */} +
+ {groupedTools.map(([category, tools]) => ( +
+

+ {category} ({tools.length}) +

+ + {tools.map((tool) => ( +
+
+ + {tool.labelFr} + + + {tool.name} + +
+ +

+ {tool.descriptionFr} +

+
+ ))} +
+ ))} + + {groupedTools.length === 0 && ( +

+ Aucun outil ne correspond à votre recherche. +

+ )} +
+
+ ) +} diff --git a/packages/website/src/features/docs/ai/modelsAiDocPage.tsx b/packages/website/src/features/docs/ai/modelsAiDocPage.tsx deleted file mode 100644 index 50ef6049..00000000 --- a/packages/website/src/features/docs/ai/modelsAiDocPage.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { DocHeader } from "../../../components/document/docHeader.tsx" -import { DocList } from "../../../components/document/docList.tsx" -import { DocNextPage } from "../../../components/document/docNextPage.tsx" -import { DocParagraph } from "../../../components/document/docParagraph.tsx" -import { DocRoot } from "../../../components/document/docRoot.tsx" -import { DocSection } from "../../../components/document/docSection.tsx" -import { DocTable } from "../../../components/document/docTable.tsx" -import { DocTip } from "../../../components/document/docTip.tsx" - -export function ModelsAiDocPage() { - return ( - - - - - - L'assistant utilise deux modèles distincts dans son architecture en deux passes : - - - - - - - L'assistant utilise actuellement le modèle Mistral Small (mistral-small-2603) de - Mistral AI. Ce modèle offre un bon équilibre entre performance et coût pour les tâches comptables : - - - - - - - L'infrastructure d'Arrhes est conçue pour être agnostique vis-à-vis du fournisseur de modèle. Deux - fournisseurs sont actuellement supportés : - - - - Le choix du fournisseur est configuré au niveau de l'infrastructure et ne nécessite aucune action de - la part de l'utilisateur. - - - - - - Comme tout modèle de langage, l'assistant peut occasionnellement produire des réponses inexactes. Il - est recommandé de vérifier les résultats pour les opérations critiques. L'assistant est limité à 10 - appels d'outils par message pour garantir des temps de réponse raisonnables. - - - - - - ) -} diff --git a/packages/website/src/features/docs/ai/ocrAiDocPage.tsx b/packages/website/src/features/docs/ai/ocrAiDocPage.tsx deleted file mode 100644 index e6b3c860..00000000 --- a/packages/website/src/features/docs/ai/ocrAiDocPage.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { DocHeader } from "../../../components/document/docHeader.tsx" -import { DocLink } from "../../../components/document/docLink.tsx" -import { DocList } from "../../../components/document/docList.tsx" -import { DocParagraph } from "../../../components/document/docParagraph.tsx" -import { DocRoot } from "../../../components/document/docRoot.tsx" -import { DocSection } from "../../../components/document/docSection.tsx" -import { DocTip } from "../../../components/document/docTip.tsx" - -export function OcrAiDocPage() { - return ( - - - - - - La fonctionnalité OCR permet d'extraire automatiquement le texte depuis un document source (image ou - PDF) et de créer un nouveau fichier au format Markdown dans votre espace de stockage. - - - L'OCR est disponible depuis l'interface de stockage et via l'outil assistant ocr_file. - - - - - L'OCR peut être utilisé pour : - - - - - - - - - Pour voir la liste complète des capacités de l'assistant, consultez la page{" "} - Outils. - - - ) -} diff --git a/packages/website/src/features/docs/ai/rootAiDocPage.tsx b/packages/website/src/features/docs/ai/rootAiDocPage.tsx deleted file mode 100644 index 830b9727..00000000 --- a/packages/website/src/features/docs/ai/rootAiDocPage.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { ButtonOutlineContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconChevronRight } from "@tabler/icons-react" -import { DocHeader } from "../../../components/document/docHeader.tsx" -import { DocLink } from "../../../components/document/docLink.tsx" -import { DocList } from "../../../components/document/docList.tsx" -import { DocParagraph } from "../../../components/document/docParagraph.tsx" -import { DocRoot } from "../../../components/document/docRoot.tsx" -import { DocSection } from "../../../components/document/docSection.tsx" -import { DocTip } from "../../../components/document/docTip.tsx" -import { LinkButton } from "../../../components/linkButton.tsx" - -export function RootAiDocPage() { - return ( - - - -
-

- L'assistant IA d'Arrhes est un agent comptable conversationnel capable de consulter, créer, modifier - et supprimer vos données comptables. Il comprend le langage naturel et utilise plus de 70 outils - spécialisés pour répondre à vos demandes. -

-
- - } - /> - -
-
- - - L'assistant IA est une fonctionnalité premium. Un abonnement au plan Avancé est requis pour y accéder. - - - - Politique d'usage raisonnable: les requêtes assistant sont limitées à 500 messages par mois et par - organisation (réinitialisation au début de chaque mois calendaire). - - - - L'assistant comptable peut vous aider pour : - - - - - - L'assistant utilise une architecture en deux passes pour répondre à vos demandes : - - - - L'assistant conserve l'historique de la conversation pour maintenir le contexte au fil des échanges. - Vous pouvez également présélectionner un exercice fiscal et fournir des instructions personnalisées - pour affiner les réponses. - - - - - - L'assistant opère exclusivement dans le périmètre de votre organisation. Il ne peut accéder qu'aux - données de l'organisation sélectionnée et les actions sont soumises aux mêmes contrôles de - permissions que l'interface web. Aucune donnée n'est partagée entre organisations. - - - - - Pour en savoir plus sur les modèles utilisés, consultez la page{" "} - Modèles. Pour la liste complète des - outils disponibles, consultez la page{" "} - Outils. - -
- ) -} diff --git a/packages/website/src/features/docs/ai/toolsAiDocPage.tsx b/packages/website/src/features/docs/ai/toolsAiDocPage.tsx deleted file mode 100644 index 86643702..00000000 --- a/packages/website/src/features/docs/ai/toolsAiDocPage.tsx +++ /dev/null @@ -1,280 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" -import { useMemo, useState } from "react" -import { DocHeader } from "../../../components/document/docHeader.tsx" -import { DocList } from "../../../components/document/docList.tsx" -import { DocParagraph } from "../../../components/document/docParagraph.tsx" -import { DocRoot } from "../../../components/document/docRoot.tsx" -import { DocSection } from "../../../components/document/docSection.tsx" -import { DocTip } from "../../../components/document/docTip.tsx" -import { type AgentToolDefinition, agentToolsCatalog } from "../../dashboard/$idOrganization/agent/agentToolsCatalog.ts" - -const categoryOrder = [ - "Exercices fiscaux", - "Écritures comptables", - "Lignes d'écriture", - "Étiquettes d'écriture", - "Plan comptable", - "Journaux", - "Étiquettes", - "Bilan", - "Compte de résultat", - "Calculs et rubriques", - "Fichiers", - "Dossiers", - "Rapports", - "Exercice (général)", - "Documentation", - "Traitement de données", - "Autres", -] as const - -function getToolCategory(toolName: string): string { - if (toolName === "search_documentation") return "Documentation" - if (toolName === "process_array") return "Traitement de données" - - if (toolName === "read_all_years") return "Exercices fiscaux" - if (toolName.includes("one_year") || toolName.startsWith("close_year") || toolName.startsWith("open_year")) { - return "Exercice (général)" - } - - if (toolName.includes("entry_line")) return "Lignes d'écriture" - if (toolName.includes("entry_tag")) return "Étiquettes d'écriture" - if (toolName.includes("entry") || toolName.includes("entries")) return "Écritures comptables" - if (toolName.includes("account")) return "Plan comptable" - if (toolName.includes("journal")) return "Journaux" - if (toolName.includes("tag")) return "Étiquettes" - if (toolName.includes("balance_sheet")) return "Bilan" - if (toolName.includes("income_statement")) return "Compte de résultat" - if (toolName.includes("computation")) return "Calculs et rubriques" - if (toolName.includes("file")) return "Fichiers" - if (toolName.includes("folder")) return "Dossiers" - if (toolName.includes("document") || toolName.includes("report")) return "Rapports" - - return "Autres" -} - -export function ToolsAiDocPage() { - const [search, setSearch] = useState("") - - const groupedTools = useMemo(() => { - const normalizedSearch = search.trim().toLowerCase() - - const filteredTools = normalizedSearch.length - ? agentToolsCatalog.filter((tool) => { - const category = getToolCategory(tool.name) - return ( - tool.name.toLowerCase().includes(normalizedSearch) || - tool.labelFr.toLowerCase().includes(normalizedSearch) || - tool.descriptionFr.toLowerCase().includes(normalizedSearch) || - category.toLowerCase().includes(normalizedSearch) - ) - }) - : agentToolsCatalog - - const grouped = new Map() - - for (const tool of filteredTools) { - const category = getToolCategory(tool.name) - const bucket = grouped.get(category) - if (bucket) { - bucket.push(tool) - } else { - grouped.set(category, [ - tool, - ]) - } - } - - for (const [, tools] of grouped) { - tools.sort((a, b) => a.labelFr.localeCompare(b.labelFr, "fr")) - } - - return [ - ...grouped.entries(), - ].sort((a, b) => { - const indexA = categoryOrder.indexOf(a[0] as (typeof categoryOrder)[number]) - const indexB = categoryOrder.indexOf(b[0] as (typeof categoryOrder)[number]) - const safeA = indexA === -1 ? categoryOrder.length : indexA - const safeB = indexB === -1 ? categoryOrder.length : indexB - return safeA - safeB - }) - }, [ - search, - ]) - - const totalToolCount = agentToolsCatalog.length - - return ( - - - - - - Les outils sont les actions concrètes que l'assistant IA peut effectuer sur vos données comptables. - Lorsque vous envoyez un message, l'assistant analyse votre demande, identifie les outils - nécessaires, puis les exécute automatiquement pour vous fournir une réponse. - - - Un seul message peut déclencher plusieurs outils en chaîne. Par exemple, pour répondre à « Quel est - le solde du compte 411 ? », l'assistant peut d'abord rechercher le compte, puis consulter ses - mouvements, et enfin calculer le solde. - - - - - - Vous ne trouvez pas l'outil dont vous avez besoin, ou l'assistant ne parvient pas à réaliser une tâche ? - N'hésitez pas à nous contacter pour suggérer de nouveaux outils. L'assistant est en amélioration - continue et de nouvelles capacités sont ajoutées régulièrement. - - - {/* Search bar */} -
- setSearch(e.target.value)} - placeholder="Rechercher un outil..." - className={css({ - width: "100%", - maxWidth: "24rem", - padding: "0.5rem 0.75rem", - borderRadius: "md", - border: "1px solid", - borderColor: "neutral/20", - fontSize: "sm", - color: "neutral", - outline: "none", - _focus: { - borderColor: "primary", - }, - _placeholder: { - color: "neutral/30", - }, - })} - /> -
- - {/* Tool categories */} -
- {groupedTools.map(([category, tools]) => ( -
-

- {category} ({tools.length}) -

- - {tools.map((tool) => ( -
-
- - {tool.labelFr} - - - {tool.name} - -
- -

- {tool.descriptionFr} -

-
- ))} -
- ))} - - {groupedTools.length === 0 && ( -

- Aucun outil ne correspond à votre recherche. -

- )} -
-
- ) -} diff --git a/packages/website/src/features/docs/api/AccountsApiDocPage.tsx b/packages/website/src/features/docs/api/AccountsApiDocPage.tsx new file mode 100644 index 00000000..3c78e827 --- /dev/null +++ b/packages/website/src/features/docs/api/AccountsApiDocPage.tsx @@ -0,0 +1,35 @@ +import { + createOneAccountRouteDefinition, + deleteOneAccountRouteDefinition, + readAllAccountsRouteDefinition, + readOneAccountRouteDefinition, + updateOneAccountRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { DocHeader } from "../../../components/document/DocHeader.tsx" +import { DocParagraph } from "../../../components/document/DocParagraph.tsx" +import { DocRoot } from "../../../components/document/DocRoot.tsx" +import { DocRouteRequest } from "../../../components/document/DocRouteRequest.tsx" +import { DocSection } from "../../../components/document/DocSection.tsx" + +export function AccountsApiDocPage() { + return ( + + + + + + Le plan comptable de l'exercice. Les comptes sont organisés en arborescence avec des classes (1 à 7) + à la racine. + + + + + + + + + ) +} diff --git a/packages/website/src/features/docs/api/ApiKeysApiDocPage.tsx b/packages/website/src/features/docs/api/ApiKeysApiDocPage.tsx new file mode 100644 index 00000000..63fc22d6 --- /dev/null +++ b/packages/website/src/features/docs/api/ApiKeysApiDocPage.tsx @@ -0,0 +1,40 @@ +import { + createOneApiKeyRouteDefinition, + deleteOneApiKeyRouteDefinition, + readAllApiKeysRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { DocCode } from "../../../components/document/DocCode.tsx" +import { DocHeader } from "../../../components/document/DocHeader.tsx" +import { DocParagraph } from "../../../components/document/DocParagraph.tsx" +import { DocRoot } from "../../../components/document/DocRoot.tsx" +import { DocRouteRequest } from "../../../components/document/DocRouteRequest.tsx" +import { DocSection } from "../../../components/document/DocSection.tsx" +import { DocTip } from "../../../components/document/DocTip.tsx" + +export function ApiKeysApiDocPage() { + return ( + + + + + + Les clés API permettent un accès programmatique sans passer par le cookie de session. Chaque clé est + liée à une organisation et nécessite un abonnement premium actif. + + + + + + La clé brute (rawKey) n'est retournée qu'au moment de la création. Conservez-la + précieusement car elle ne pourra pas être récupérée ultérieurement. + + + + ) +} diff --git a/packages/website/src/features/docs/api/AuthenticationApiDocPage.tsx b/packages/website/src/features/docs/api/AuthenticationApiDocPage.tsx new file mode 100644 index 00000000..c3620d33 --- /dev/null +++ b/packages/website/src/features/docs/api/AuthenticationApiDocPage.tsx @@ -0,0 +1,143 @@ +import { DocCode } from "../../../components/document/DocCode.tsx" +import { DocHeader } from "../../../components/document/DocHeader.tsx" +import { DocList } from "../../../components/document/DocList.tsx" +import { DocParagraph } from "../../../components/document/DocParagraph.tsx" +import { DocRoot } from "../../../components/document/DocRoot.tsx" +import { DocSection } from "../../../components/document/DocSection.tsx" +import { DocTable } from "../../../components/document/DocTable.tsx" +import { DocTip } from "../../../components/document/DocTip.tsx" + +export function AuthenticationApiDocPage() { + return ( + + + + + + L'API supporte deux méthodes d'authentification pour accéder aux routes protégées. Chaque requête + doit utiliser l'une des deux méthodes suivantes : + + ", + "Intégration programmatique", + ], + ]} + /> + + + + + Lors de la connexion via l'interface web, un cookie arrhes_id_user_session{" "} + (httpOnly) est automatiquement défini. Ce cookie identifie l'utilisateur pour toutes les requêtes + suivantes. + + + Avec cette méthode, l'organisation cible doit être spécifiée séparément via l'en-tête{" "} + X-Organization-Id ou le cookie arrhes_id_organization. + + + L'en-tête X-Organization-Id a la priorité sur le cookie{", "} + arrhes_id_organization. + + + + + + Les clés API permettent un accès programmatique à l'API. Elles sont liées à une organisation + spécifique et nécessitent un abonnement premium. + + + L'en-tête Authorization doit contenir le token au format Bearer : + + ", + ], + ]} + /> + + Avec une clé API, l'organisation est automatiquement déterminée par la clé elle-même. Il n'est pas + nécessaire de fournir l'en-tête X-Organization-Id. + + + La clé brute (rawKey) n'est retournée qu'au moment de la création via la route + {", "} + POST /v1/organizations/:idOrganization/api-keys. Conservez-la précieusement, elle + ne pourra pas être récupérée ultérieurement. + + + + + Résumé des en-têtes nécessaires selon la méthode d'authentification : + ", + "Non utilisé", + "Requis", + ], + ]} + /> + + * L'en-tête X-Organization-Id peut être remplacé par le cookie{", "} + arrhes_id_organization. + + + + + + Certaines routes nécessitent des permissions supplémentaires au-delà de l'authentification : + + + Administrateur : les routes de gestion d'organisation (suppression, + paiements, abonnement) nécessitent que l'utilisateur soit administrateur de l'organisation. + , + <> + Plan avancé : les routes de gestion des clés API nécessitent un abonnement + avancé actif sur l'organisation. + , + ]} + /> + + + ) +} diff --git a/packages/website/src/features/docs/api/BalanceSheetsApiDocPage.tsx b/packages/website/src/features/docs/api/BalanceSheetsApiDocPage.tsx new file mode 100644 index 00000000..cad234b3 --- /dev/null +++ b/packages/website/src/features/docs/api/BalanceSheetsApiDocPage.tsx @@ -0,0 +1,35 @@ +import { + createOneBalanceSheetRouteDefinition, + deleteOneBalanceSheetRouteDefinition, + readAllBalanceSheetsRouteDefinition, + readOneBalanceSheetRouteDefinition, + updateOneBalanceSheetRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { DocHeader } from "../../../components/document/DocHeader.tsx" +import { DocParagraph } from "../../../components/document/DocParagraph.tsx" +import { DocRoot } from "../../../components/document/DocRoot.tsx" +import { DocRouteRequest } from "../../../components/document/DocRouteRequest.tsx" +import { DocSection } from "../../../components/document/DocSection.tsx" + +export function BalanceSheetsApiDocPage() { + return ( + + + + + + Structure du bilan comptable (actif et passif). Les lignes de bilan sont liées aux comptes pour + calculer automatiquement les soldes. + + + + + + + + + ) +} diff --git a/packages/website/src/features/docs/api/ComputationsApiDocPage.tsx b/packages/website/src/features/docs/api/ComputationsApiDocPage.tsx new file mode 100644 index 00000000..3c1038ed --- /dev/null +++ b/packages/website/src/features/docs/api/ComputationsApiDocPage.tsx @@ -0,0 +1,52 @@ +import { + createOneComputationIncomeStatementRouteDefinition, + createOneComputationRouteDefinition, + deleteOneComputationIncomeStatementRouteDefinition, + deleteOneComputationRouteDefinition, + readAllComputationIncomeStatementsRouteDefinition, + readAllComputationsRouteDefinition, + readOneComputationIncomeStatementRouteDefinition, + readOneComputationRouteDefinition, + updateOneComputationIncomeStatementRouteDefinition, + updateOneComputationRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { DocHeader } from "../../../components/document/DocHeader.tsx" +import { DocParagraph } from "../../../components/document/DocParagraph.tsx" +import { DocRoot } from "../../../components/document/DocRoot.tsx" +import { DocRouteRequest } from "../../../components/document/DocRouteRequest.tsx" +import { DocSection } from "../../../components/document/DocSection.tsx" + +export function ComputationsApiDocPage() { + return ( + + + + + + Les calculs sont des formules personnalisées qui combinent des lignes de compte de résultat pour + produire des valeurs dérivées (résultat d'exploitation, EBE, etc.). + + + + + + + + + + + Liens entre les calculs et les lignes de compte de résultat. Chaque lien définit si la ligne est + ajoutée ou soustraite dans le calcul. + + + + + + + + + ) +} diff --git a/packages/website/src/features/docs/api/EntriesApiDocPage.tsx b/packages/website/src/features/docs/api/EntriesApiDocPage.tsx new file mode 100644 index 00000000..a270f246 --- /dev/null +++ b/packages/website/src/features/docs/api/EntriesApiDocPage.tsx @@ -0,0 +1,80 @@ +import { + addOneEntryTagRouteDefinition, + computeOneEntryRouteDefinition, + createOneEntryFromTemplateRouteDefinition, + createOneEntryLineRouteDefinition, + createOneEntryRouteDefinition, + deleteOneEntryLineRouteDefinition, + deleteOneEntryRouteDefinition, + duplicateOneEntryRouteDefinition, + readAllEntriesRouteDefinition, + readAllEntryLinesRouteDefinition, + readAllEntryTagsRouteDefinition, + readOneEntryLineRouteDefinition, + readOneEntryRouteDefinition, + removeOneEntryTagRouteDefinition, + reverseOneEntryRouteDefinition, + updateManyEntryLinesRouteDefinition, + updateOneEntryLineRouteDefinition, + updateOneEntryRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { DocHeader } from "../../../components/document/DocHeader.tsx" +import { DocParagraph } from "../../../components/document/DocParagraph.tsx" +import { DocRoot } from "../../../components/document/DocRoot.tsx" +import { DocRouteRequest } from "../../../components/document/DocRouteRequest.tsx" +import { DocSection } from "../../../components/document/DocSection.tsx" + +export function EntriesApiDocPage() { + return ( + + + + + + Les écritures comptables sont les opérations enregistrées dans les journaux. Chaque écriture + contient une ou plusieurs lignes (débit/crédit). + + + + + + + + + + + + + + + + Chaque ligne d'écriture représente un mouvement de débit ou de crédit sur un compte. + + + + + + + + + + + Association de libellés (tags) à une écriture pour la catégoriser. + + + + + ) +} diff --git a/packages/website/src/features/docs/api/EntryLinesApiDocPage.tsx b/packages/website/src/features/docs/api/EntryLinesApiDocPage.tsx new file mode 100644 index 00000000..6355d65c --- /dev/null +++ b/packages/website/src/features/docs/api/EntryLinesApiDocPage.tsx @@ -0,0 +1,47 @@ +import { + addOneEntryTagRouteDefinition, + createOneEntryLineRouteDefinition, + deleteOneEntryLineRouteDefinition, + readAllEntryLinesRouteDefinition, + readOneEntryLineRouteDefinition, + removeOneEntryTagRouteDefinition, + updateManyEntryLinesRouteDefinition, + updateOneEntryLineRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { DocHeader } from "../../../components/document/DocHeader.tsx" +import { DocParagraph } from "../../../components/document/DocParagraph.tsx" +import { DocRoot } from "../../../components/document/DocRoot.tsx" +import { DocRouteRequest } from "../../../components/document/DocRouteRequest.tsx" +import { DocSection } from "../../../components/document/DocSection.tsx" + +export function EntryLinesApiDocPage() { + return ( + + + + + + Chaque ligne d'écriture représente un mouvement de débit ou de crédit sur un compte. + + + + + + + + + + + Association de libellés (tags) à une écriture pour la catégoriser. + + + + + ) +} diff --git a/packages/website/src/features/docs/api/FilesApiDocPage.tsx b/packages/website/src/features/docs/api/FilesApiDocPage.tsx new file mode 100644 index 00000000..a2c75209 --- /dev/null +++ b/packages/website/src/features/docs/api/FilesApiDocPage.tsx @@ -0,0 +1,83 @@ +import { + createOneFileRouteDefinition, + createOneFolderRouteDefinition, + deleteOneFileRouteDefinition, + deleteOneFolderRouteDefinition, + finalizeFileUploadRouteDefinition, + generateFileDeleteSignedUrlRouteDefinition, + generateFileGetSignedUrlRouteDefinition, + generateFilePutSignedUrlRouteDefinition, + ocrFileRouteDefinition, + readAllFilesRouteDefinition, + readAllFoldersRouteDefinition, + readOneFileRouteDefinition, + readOneFolderRouteDefinition, + updateOneFileRouteDefinition, + updateOneFolderRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { DocCode } from "../../../components/document/DocCode.tsx" +import { DocHeader } from "../../../components/document/DocHeader.tsx" +import { DocParagraph } from "../../../components/document/DocParagraph.tsx" +import { DocRoot } from "../../../components/document/DocRoot.tsx" +import { DocRouteRequest } from "../../../components/document/DocRouteRequest.tsx" +import { DocSection } from "../../../components/document/DocSection.tsx" +import { DocTip } from "../../../components/document/DocTip.tsx" + +export function FilesApiDocPage() { + return ( + + + + + + Les fichiers sont les pièces justificatives rattachées à un exercice comptable. Le stockage utilise + des URLs signées compatibles S3. + + + + + + + + + + + + + La taille maximale par fichier est de 50 Mo. Au-delà, l'API retourne une erreur{" "} + 400. + + + + + + Les dossiers permettent d'organiser les fichiers au sein d'un exercice. Ils supportent une + hiérarchie imbriquée via le champ idFolderParent. + + + + + + + + + ) +} diff --git a/packages/website/src/features/docs/api/FoldersApiDocPage.tsx b/packages/website/src/features/docs/api/FoldersApiDocPage.tsx new file mode 100644 index 00000000..fb92b4e3 --- /dev/null +++ b/packages/website/src/features/docs/api/FoldersApiDocPage.tsx @@ -0,0 +1,36 @@ +import { + createOneFolderRouteDefinition, + deleteOneFolderRouteDefinition, + readAllFoldersRouteDefinition, + readOneFolderRouteDefinition, + updateOneFolderRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { DocCode } from "../../../components/document/DocCode.tsx" +import { DocHeader } from "../../../components/document/DocHeader.tsx" +import { DocParagraph } from "../../../components/document/DocParagraph.tsx" +import { DocRoot } from "../../../components/document/DocRoot.tsx" +import { DocRouteRequest } from "../../../components/document/DocRouteRequest.tsx" +import { DocSection } from "../../../components/document/DocSection.tsx" + +export function FoldersApiDocPage() { + return ( + + + + + + Les dossiers permettent d'organiser les fichiers au sein d'un exercice. Ils supportent une + hiérarchie imbriquée via le champ idFolderParent. + + + + + + + + + ) +} diff --git a/packages/website/src/features/docs/api/IncomeStatementsApiDocPage.tsx b/packages/website/src/features/docs/api/IncomeStatementsApiDocPage.tsx new file mode 100644 index 00000000..c97a2021 --- /dev/null +++ b/packages/website/src/features/docs/api/IncomeStatementsApiDocPage.tsx @@ -0,0 +1,69 @@ +import { + createOneComputationIncomeStatementRouteDefinition, + createOneComputationRouteDefinition, + createOneIncomeStatementRouteDefinition, + deleteOneComputationIncomeStatementRouteDefinition, + deleteOneComputationRouteDefinition, + deleteOneIncomeStatementRouteDefinition, + readAllComputationIncomeStatementsRouteDefinition, + readAllComputationsRouteDefinition, + readAllIncomeStatementsRouteDefinition, + readOneComputationIncomeStatementRouteDefinition, + readOneComputationRouteDefinition, + readOneIncomeStatementRouteDefinition, + updateOneComputationIncomeStatementRouteDefinition, + updateOneComputationRouteDefinition, + updateOneIncomeStatementRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { DocHeader } from "../../../components/document/DocHeader.tsx" +import { DocParagraph } from "../../../components/document/DocParagraph.tsx" +import { DocRoot } from "../../../components/document/DocRoot.tsx" +import { DocRouteRequest } from "../../../components/document/DocRouteRequest.tsx" +import { DocSection } from "../../../components/document/DocSection.tsx" + +export function IncomeStatementsApiDocPage() { + return ( + + + + + + Structure du compte de résultat (produits et charges). Fonctionne de manière similaire aux bilans : + les lignes sont liées aux comptes pour le calcul automatique des soldes. + + + + + + + + + + + Les calculs sont des formules personnalisées qui combinent des lignes de compte de résultat pour + produire des valeurs dérivées (résultat d'exploitation, EBE, etc.). + + + + + + + + + + + Liens entre les calculs et les lignes de compte de résultat. Chaque lien définit si la ligne est + ajoutée ou soustraite dans le calcul. + + + + + + + + + ) +} diff --git a/packages/website/src/features/docs/api/IntroductionApiDocPage.tsx b/packages/website/src/features/docs/api/IntroductionApiDocPage.tsx new file mode 100644 index 00000000..f9d551cd --- /dev/null +++ b/packages/website/src/features/docs/api/IntroductionApiDocPage.tsx @@ -0,0 +1,235 @@ +import { DocCode } from "../../../components/document/DocCode.tsx" +import { DocHeader } from "../../../components/document/DocHeader.tsx" +import { DocLink } from "../../../components/document/DocLink.tsx" +import { DocList } from "../../../components/document/DocList.tsx" +import { DocParagraph } from "../../../components/document/DocParagraph.tsx" +import { DocRoot } from "../../../components/document/DocRoot.tsx" +import { DocSection } from "../../../components/document/DocSection.tsx" +import { DocTable } from "../../../components/document/DocTable.tsx" +import { DocTip } from "../../../components/document/DocTip.tsx" + +export function IntroductionApiDocPage() { + return ( + + + + + L'API d'Arrhes suit les conventions REST standard : + + + Les identifiants d'entités (idYear, idEntry, idAccount, etc.) sont passés dans les paramètres d'URL + (ex : :idOrganization, :idYear). L'organisation est identifiée + via le token d'authentification ou l'en-tête X-Organization-Id, et non dans le + corps de la requête. + + + Les chemins affichés dans cette documentation n'incluent pas le préfixe de version{" "} + /v1. Utilisez toujours la dernière version du CLI Arrhes pour + bénéficier automatiquement du bon préfixe de version. Si vous appelez l'API directement, préfixez + chaque chemin avec /v1. + + + + + + Toutes les routes documentées ici sont protégées et nécessitent une authentification. + + Voir les méthodes d'authentification + + + + Toutes les erreurs sont retournées avec un message en français : + + Les messages d'erreur courants incluent : + + + + + + L'API expose 110 routes protégées réparties en 19 catégories. Le tableau ci-dessous résume chaque + catégorie : + + + + + ) +} diff --git a/packages/website/src/features/docs/api/JournalsApiDocPage.tsx b/packages/website/src/features/docs/api/JournalsApiDocPage.tsx new file mode 100644 index 00000000..8b0cd7eb --- /dev/null +++ b/packages/website/src/features/docs/api/JournalsApiDocPage.tsx @@ -0,0 +1,35 @@ +import { + createOneJournalRouteDefinition, + deleteOneJournalRouteDefinition, + readAllJournalsRouteDefinition, + readOneJournalRouteDefinition, + updateOneJournalRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { DocHeader } from "../../../components/document/DocHeader.tsx" +import { DocParagraph } from "../../../components/document/DocParagraph.tsx" +import { DocRoot } from "../../../components/document/DocRoot.tsx" +import { DocRouteRequest } from "../../../components/document/DocRouteRequest.tsx" +import { DocSection } from "../../../components/document/DocSection.tsx" + +export function JournalsApiDocPage() { + return ( + + + + + + Les journaux comptables regroupent les écritures par type d'opération (achats, ventes, trésorerie, + OD, etc.). + + + + + + + + + ) +} diff --git a/packages/website/src/features/docs/api/OrgUsersApiDocPage.tsx b/packages/website/src/features/docs/api/OrgUsersApiDocPage.tsx new file mode 100644 index 00000000..e0056c53 --- /dev/null +++ b/packages/website/src/features/docs/api/OrgUsersApiDocPage.tsx @@ -0,0 +1,42 @@ +import { + activateOrganizationMembershipRouteDefinition, + createOneOrganizationUserRouteDefinition, + deleteOneOrganizationUserRouteDefinition, + readAllOrganizationUsersRouteDefinition, + readOneOrganizationUserRouteDefinition, + updateOneOrganizationUserRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { DocHeader } from "../../../components/document/DocHeader.tsx" +import { DocParagraph } from "../../../components/document/DocParagraph.tsx" +import { DocRoot } from "../../../components/document/DocRoot.tsx" +import { DocRouteRequest } from "../../../components/document/DocRouteRequest.tsx" +import { DocSection } from "../../../components/document/DocSection.tsx" + +export function OrgUsersApiDocPage() { + return ( + + + + + + Invitation, lecture, modification et suppression des membres d'une organisation. + + + + + + + + + + ) +} diff --git a/packages/website/src/features/docs/api/OrganizationApiDocPage.tsx b/packages/website/src/features/docs/api/OrganizationApiDocPage.tsx new file mode 100644 index 00000000..d66955d1 --- /dev/null +++ b/packages/website/src/features/docs/api/OrganizationApiDocPage.tsx @@ -0,0 +1,44 @@ +import { + deleteOneOrganizationRouteDefinition, + readOneOrganizationRouteDefinition, + updateOneOrganizationRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { DocHeader } from "../../../components/document/DocHeader.tsx" +import { DocParagraph } from "../../../components/document/DocParagraph.tsx" +import { DocRoot } from "../../../components/document/DocRoot.tsx" +import { DocRouteRequest } from "../../../components/document/DocRouteRequest.tsx" +import { DocSection } from "../../../components/document/DocSection.tsx" +import { DocTip } from "../../../components/document/DocTip.tsx" + +export function OrganizationApiDocPage() { + return ( + + + + + + Lecture, modification et suppression de l'organisation. La modification et la suppression + nécessitent que l'utilisateur soit administrateur. + + + + + + La suppression d'une organisation est irréversible et supprime toutes les données associées. + + + + ) +} diff --git a/packages/website/src/features/docs/api/RootApiDocPage.tsx b/packages/website/src/features/docs/api/RootApiDocPage.tsx new file mode 100644 index 00000000..f7f5d10c --- /dev/null +++ b/packages/website/src/features/docs/api/RootApiDocPage.tsx @@ -0,0 +1,71 @@ +import { ButtonOutlineContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconChevronRight } from "@tabler/icons-react" +import { DocCode } from "../../../components/document/DocCode.tsx" +import { DocHeader } from "../../../components/document/DocHeader.tsx" +import { DocLink } from "../../../components/document/DocLink.tsx" +import { DocRoot } from "../../../components/document/DocRoot.tsx" +import { DocTip } from "../../../components/document/DocTip.tsx" +import { LinkButton } from "../../../components/LinkButton.tsx" + +export function RootApiDocPage() { + return ( + + + + {/* About section */} +
+

+ L'API d'Arrhes suit les conventions REST (GET, POST, PATCH, DELETE). L'organisation est identifiée + via la clé API (Bearer token) ou l'en-tête X-Organization-Id. L'API expose 110 + routes protégées réparties en 19 catégories. +

+
+ + } + /> + +
+
+ + + L'API supporte deux méthodes d'authentification : les cookies de session (pour l'interface web) et les + clés API avec l'en-tête Authorization: Bearer <clé> (pour l'intégration + programmatique). + + + + Pour comprendre les concepts métier utilisés dans l'API, consultez le{" "} + cours de comptabilité et le{" "} + guide d'utilisation. + +
+ ) +} diff --git a/packages/website/src/features/docs/api/TagsApiDocPage.tsx b/packages/website/src/features/docs/api/TagsApiDocPage.tsx new file mode 100644 index 00000000..cbb82668 --- /dev/null +++ b/packages/website/src/features/docs/api/TagsApiDocPage.tsx @@ -0,0 +1,35 @@ +import { + createOneTagRouteDefinition, + deleteOneTagRouteDefinition, + readAllTagsRouteDefinition, + readOneTagRouteDefinition, + updateOneTagRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { DocHeader } from "../../../components/document/DocHeader.tsx" +import { DocParagraph } from "../../../components/document/DocParagraph.tsx" +import { DocRoot } from "../../../components/document/DocRoot.tsx" +import { DocRouteRequest } from "../../../components/document/DocRouteRequest.tsx" +import { DocSection } from "../../../components/document/DocSection.tsx" + +export function TagsApiDocPage() { + return ( + + + + + + Les libellés (tags) permettent de catégoriser les écritures comptables avec des étiquettes + réutilisables définies au niveau de l'exercice. + + + + + + + + + ) +} diff --git a/packages/website/src/features/docs/api/XBRLReportsApiDocPage.tsx b/packages/website/src/features/docs/api/XBRLReportsApiDocPage.tsx new file mode 100644 index 00000000..9e57d629 --- /dev/null +++ b/packages/website/src/features/docs/api/XBRLReportsApiDocPage.tsx @@ -0,0 +1,52 @@ +import { + generateBalanceSheetXmlRouteDefinition, + generateFecRouteDefinition, + generateIncomeStatementXmlRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { DocHeader } from "../../../components/document/DocHeader.tsx" +import { DocParagraph } from "../../../components/document/DocParagraph.tsx" +import { DocRoot } from "../../../components/document/DocRoot.tsx" +import { DocRouteRequest } from "../../../components/document/DocRouteRequest.tsx" +import { DocSection } from "../../../components/document/DocSection.tsx" +import { DocTip } from "../../../components/document/DocTip.tsx" + +export function XBRLReportsApiDocPage() { + return ( + + + + + + Le Fichier des Écritures Comptables (FEC) est un export normalisé requis par l'administration + fiscale française. Généré côté serveur et retourne une URL signée pour téléchargement. + + + + + + + Génération de rapports comptables en XBRL (taxonomie ANC française) : bilan et compte de résultat. + Ces rapports sont générés côté serveur et retournent une URL signée pour le téléchargement. + + + + + Les URLs signées retournent une durée de validité limitée. Téléchargez le fichier immédiatement + après génération. + + + + ) +} diff --git a/packages/website/src/features/docs/api/YearApiDocPage.tsx b/packages/website/src/features/docs/api/YearApiDocPage.tsx new file mode 100644 index 00000000..c82ec18e --- /dev/null +++ b/packages/website/src/features/docs/api/YearApiDocPage.tsx @@ -0,0 +1,50 @@ +import { + closeYearRouteDefinition, + createOneYearRouteDefinition, + deleteOneYearRouteDefinition, + openYearRouteDefinition, + readAllYearsRouteDefinition, + readOneYearRouteDefinition, + settleBalanceSheetRouteDefinition, + settleIncomeStatementRouteDefinition, + updateOneYearRouteDefinition, +} from "@arrhes/application-metadata/routes" +import { DocHeader } from "../../../components/document/DocHeader.tsx" +import { DocParagraph } from "../../../components/document/DocParagraph.tsx" +import { DocRoot } from "../../../components/document/DocRoot.tsx" +import { DocRouteRequest } from "../../../components/document/DocRouteRequest.tsx" +import { DocSection } from "../../../components/document/DocSection.tsx" +import { DocTip } from "../../../components/document/DocTip.tsx" + +export function YearApiDocPage() { + return ( + + + + + Création et listing des exercices comptables d'une organisation. + + + + + + + + Modification, suppression et gestion du cycle de vie d'un exercice comptable. + + + + + + + + + La clôture d'un exercice empêche toute modification ultérieure des écritures. + + + + ) +} diff --git a/packages/website/src/features/docs/api/authenticationApiDocPage.tsx b/packages/website/src/features/docs/api/authenticationApiDocPage.tsx deleted file mode 100644 index 8544ef13..00000000 --- a/packages/website/src/features/docs/api/authenticationApiDocPage.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { DocHeader } from "../../../components/document/docHeader.tsx" -import { DocList } from "../../../components/document/docList.tsx" -import { DocNextPage } from "../../../components/document/docNextPage.tsx" -import { DocParagraph } from "../../../components/document/docParagraph.tsx" -import { DocRoot } from "../../../components/document/docRoot.tsx" -import { DocSection } from "../../../components/document/docSection.tsx" -import { DocTable } from "../../../components/document/docTable.tsx" -import { DocTip } from "../../../components/document/docTip.tsx" - -export function AuthenticationApiDocPage() { - return ( - - - - - - L'API supporte deux méthodes d'authentification pour accéder aux routes protégées. Chaque requête - doit utiliser l'une des deux méthodes suivantes : - - ", - "Intégration programmatique", - ], - ]} - /> - - - - - Lors de la connexion via l'interface web, un cookie arrhes_id_user_session (httpOnly) - est automatiquement défini. Ce cookie identifie l'utilisateur pour toutes les requêtes suivantes. - - - Avec cette méthode, l'organisation cible doit être spécifiée séparément via l'en-tête{" "} - X-Organization-Id ou le cookie arrhes_id_organization. - - - L'en-tête X-Organization-Id a la priorité sur le cookie{" "} - arrhes_id_organization. - - - - - - Les clés API permettent un accès programmatique à l'API. Elles sont liées à une organisation - spécifique et nécessitent un abonnement premium. - - - L'en-tête Authorization doit contenir le token au format Bearer : - - ", - ], - ]} - /> - - Avec une clé API, l'organisation est automatiquement déterminée par la clé elle-même. Il n'est pas - nécessaire de fournir l'en-tête X-Organization-Id. - - - La clé brute (rawKey) n'est retournée qu'au moment de la création via la route{" "} - POST /auth/create-one-api-key. Conservez-la précieusement, elle ne pourra pas être - récupérée ultérieurement. - - - - - Résumé des en-têtes nécessaires selon la méthode d'authentification : - ", - "Non utilisé", - "Requis", - ], - ]} - /> - - * L'en-tête X-Organization-Id peut être remplacé par le cookie{" "} - arrhes_id_organization. - - - - - - Certaines routes nécessitent des permissions supplémentaires au-delà de l'authentification : - - - Administrateur : les routes de gestion d'organisation (suppression, - paiements, abonnement) nécessitent que l'utilisateur soit administrateur de l'organisation. - , - <> - Plan avancé : les routes de gestion des clés API nécessitent un abonnement - avancé actif sur l'organisation. - , - ]} - /> - - - - - ) -} diff --git a/packages/website/src/features/docs/api/filesApiDocPage.tsx b/packages/website/src/features/docs/api/filesApiDocPage.tsx deleted file mode 100644 index fc937a97..00000000 --- a/packages/website/src/features/docs/api/filesApiDocPage.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { - createOneFileRouteDefinition, - createOneFolderRouteDefinition, - deleteOneFileRouteDefinition, - deleteOneFolderRouteDefinition, - generateBalanceSheetXmlRouteDefinition, - generateFileDeleteSignedUrlRouteDefinition, - generateFileGetSignedUrlRouteDefinition, - generateFilePutSignedUrlRouteDefinition, - generateIncomeStatementXmlRouteDefinition, - readAllFilesRouteDefinition, - readAllFoldersRouteDefinition, - readOneFileRouteDefinition, - readOneFolderRouteDefinition, - updateOneFileRouteDefinition, - updateOneFolderRouteDefinition, -} from "@arrhes/application-metadata/routes" -import { DocHeader } from "../../../components/document/docHeader.tsx" -import { DocParagraph } from "../../../components/document/docParagraph.tsx" -import { DocRoot } from "../../../components/document/docRoot.tsx" -import { DocRouteRequest } from "../../../components/document/docRouteRequest.tsx" -import { DocSection } from "../../../components/document/docSection.tsx" -import { DocTip } from "../../../components/document/docTip.tsx" - -export function FilesApiDocPage() { - return ( - - - - - - Les fichiers sont les pièces justificatives rattachées à un exercice comptable. Le stockage utilise - des URLs signées compatibles S3. - - - - - - - - - - - La taille maximale par fichier est de 50 Mo. Au-delà, l'API retourne une erreur 400. - - - - - Les dossiers permettent d'organiser les fichiers au sein d'un exercice. - - - - - - - - - - Génération de rapports comptables en XBRL (taxonomie ANC française) : bilans et comptes de résultat. - - - - - - ) -} diff --git a/packages/website/src/features/docs/api/introductionApiDocPage.tsx b/packages/website/src/features/docs/api/introductionApiDocPage.tsx deleted file mode 100644 index ab913571..00000000 --- a/packages/website/src/features/docs/api/introductionApiDocPage.tsx +++ /dev/null @@ -1,221 +0,0 @@ -import { DocHeader } from "../../../components/document/docHeader.tsx" -import { DocLink } from "../../../components/document/docLink.tsx" -import { DocList } from "../../../components/document/docList.tsx" -import { DocNextPage } from "../../../components/document/docNextPage.tsx" -import { DocParagraph } from "../../../components/document/docParagraph.tsx" -import { DocRoot } from "../../../components/document/docRoot.tsx" -import { DocSection } from "../../../components/document/docSection.tsx" -import { DocTable } from "../../../components/document/docTable.tsx" -import { DocTip } from "../../../components/document/docTip.tsx" - -export function IntroductionApiDocPage() { - return ( - - - - - L'API d'Arrhes suit un ensemble de conventions simples et uniformes : - - - Les identifiants d'entités (idYear, idEntry, idAccount, etc.) sont passés dans le corps de la - requête. L'organisation est identifiée via le token d'authentification, et non dans le corps de la - requête, ni dans l'URL. - - - - - - Toutes les routes documentées ici sont protégées et nécessitent une authentification. - - Voir les méthodes d'authentification - - - - Toutes les erreurs sont retournées avec un message en français : - - Les messages d'erreur courants incluent : - - - - - - L'API expose 90 routes protégées réparties en 17 catégories. Le tableau ci-dessous résume chaque - catégorie : - - - - - - - ) -} diff --git a/packages/website/src/features/docs/api/organizationApiDocPage.tsx b/packages/website/src/features/docs/api/organizationApiDocPage.tsx deleted file mode 100644 index f97d38f5..00000000 --- a/packages/website/src/features/docs/api/organizationApiDocPage.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { - cancelSubscriptionRouteDefinition, - createFirstPaymentRouteDefinition, - createOneApiKeyRouteDefinition, - createOneOrganizationUserRouteDefinition, - createOneYearRouteDefinition, - deleteOneApiKeyRouteDefinition, - deleteOneOrganizationRouteDefinition, - deleteOneOrganizationUserRouteDefinition, - readAllApiKeysRouteDefinition, - readAllOrganizationPaymentsRouteDefinition, - readAllOrganizationUsersRouteDefinition, - readAllYearsRouteDefinition, - readOneOrganizationRouteDefinition, - readOneOrganizationUserRouteDefinition, - readOrganizationBillingRouteDefinition, - updateOneOrganizationRouteDefinition, - updateOneOrganizationUserRouteDefinition, -} from "@arrhes/application-metadata/routes" -import { DocHeader } from "../../../components/document/docHeader.tsx" -import { DocNextPage } from "../../../components/document/docNextPage.tsx" -import { DocParagraph } from "../../../components/document/docParagraph.tsx" -import { DocRoot } from "../../../components/document/docRoot.tsx" -import { DocRouteRequest } from "../../../components/document/docRouteRequest.tsx" -import { DocSection } from "../../../components/document/docSection.tsx" -import { DocTip } from "../../../components/document/docTip.tsx" - -export function OrganizationApiDocPage() { - return ( - - - - - - Lecture, modification et suppression de l'organisation. Les routes de modification et suppression - nécessitent que l'utilisateur soit administrateur. - - - - - - La suppression d'une organisation est irréversible et supprime toutes les données associées. Cela - supprimera également la clé utilisée pour la suppression. - - - - - - Gestion des clés API de l'organisation. Nécessite un abonnement premium actif. - - - - - - La clé brute (rawKey) n'est retournée qu'au moment de la création. Conservez-la - précieusement. - - - - - - Gestion de l'abonnement premium et de l'historique des paiements. Les routes de paiement et - d'annulation nécessitent le rôle administrateur. - - - - - - - - - Gestion des membres d'une organisation. - - - - - - - - - Création et listing des exercices comptables d'une organisation. - - - - - - - ) -} diff --git a/packages/website/src/features/docs/api/rootApiDocPage.tsx b/packages/website/src/features/docs/api/rootApiDocPage.tsx deleted file mode 100644 index ffa478a6..00000000 --- a/packages/website/src/features/docs/api/rootApiDocPage.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { ButtonOutlineContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconChevronRight } from "@tabler/icons-react" -import { DocHeader } from "../../../components/document/docHeader.tsx" -import { DocLink } from "../../../components/document/docLink.tsx" -import { DocRoot } from "../../../components/document/docRoot.tsx" -import { DocTip } from "../../../components/document/docTip.tsx" -import { LinkButton } from "../../../components/linkButton.tsx" - -export function RootApiDocPage() { - return ( - - - - {/* About section */} -
-

- L'API d'Arrhes utilise exclusivement la méthode POST. L'organisation est identifiée via la clé API - (Bearer token). L'API expose 90 routes protégées réparties en 17 catégories. -

-
- - } - /> - -
-
- - - L'API supporte deux méthodes d'authentification : les cookies de session (pour l'interface web) et les - clés API avec l'en-tête Authorization: Bearer <clé> (pour l'intégration - programmatique). - - - - Pour comprendre les concepts métier utilisés dans l'API, consultez le{" "} - cours de comptabilité et le{" "} - guide d'utilisation. - -
- ) -} diff --git a/packages/website/src/features/docs/api/yearApiDocPage.tsx b/packages/website/src/features/docs/api/yearApiDocPage.tsx deleted file mode 100644 index 4f493ecb..00000000 --- a/packages/website/src/features/docs/api/yearApiDocPage.tsx +++ /dev/null @@ -1,209 +0,0 @@ -import { - closeYearRouteDefinition, - computeOneEntryRouteDefinition, - createOneAccountRouteDefinition, - createOneBalanceSheetRouteDefinition, - createOneComputationIncomeStatementRouteDefinition, - createOneComputationRouteDefinition, - createOneEntryFromTemplateRouteDefinition, - createOneEntryLineRouteDefinition, - createOneEntryRouteDefinition, - createOneIncomeStatementRouteDefinition, - createOneJournalRouteDefinition, - createOneTagRouteDefinition, - deleteOneAccountRouteDefinition, - deleteOneBalanceSheetRouteDefinition, - deleteOneComputationIncomeStatementRouteDefinition, - deleteOneComputationRouteDefinition, - deleteOneEntryLineRouteDefinition, - deleteOneEntryRouteDefinition, - deleteOneIncomeStatementRouteDefinition, - deleteOneJournalRouteDefinition, - deleteOneTagRouteDefinition, - deleteOneYearRouteDefinition, - duplicateOneEntryRouteDefinition, - openYearRouteDefinition, - readAllAccountsRouteDefinition, - readAllBalanceSheetsRouteDefinition, - readAllComputationIncomeStatementsRouteDefinition, - readAllComputationsRouteDefinition, - readAllEntriesRouteDefinition, - readAllEntryLinesRouteDefinition, - readAllIncomeStatementsRouteDefinition, - readAllJournalsRouteDefinition, - readAllTagsRouteDefinition, - readOneAccountRouteDefinition, - readOneBalanceSheetRouteDefinition, - readOneComputationIncomeStatementRouteDefinition, - readOneComputationRouteDefinition, - readOneEntryLineRouteDefinition, - readOneEntryRouteDefinition, - readOneIncomeStatementRouteDefinition, - readOneJournalRouteDefinition, - readOneTagRouteDefinition, - readOneYearRouteDefinition, - settleBalanceSheetRouteDefinition, - settleIncomeStatementRouteDefinition, - updateManyEntryLinesRouteDefinition, - updateOneAccountRouteDefinition, - updateOneBalanceSheetRouteDefinition, - updateOneComputationIncomeStatementRouteDefinition, - updateOneComputationRouteDefinition, - updateOneEntryLineRouteDefinition, - updateOneEntryRouteDefinition, - updateOneIncomeStatementRouteDefinition, - updateOneJournalRouteDefinition, - updateOneTagRouteDefinition, - updateOneYearRouteDefinition, -} from "@arrhes/application-metadata/routes" -import { DocHeader } from "../../../components/document/docHeader.tsx" -import { DocNextPage } from "../../../components/document/docNextPage.tsx" -import { DocParagraph } from "../../../components/document/docParagraph.tsx" -import { DocRoot } from "../../../components/document/docRoot.tsx" -import { DocRouteRequest } from "../../../components/document/docRouteRequest.tsx" -import { DocSection } from "../../../components/document/docSection.tsx" -import { DocTip } from "../../../components/document/docTip.tsx" - -export function YearApiDocPage() { - return ( - - - - - - Routes de lecture, modification, clôture et ouverture d'un exercice comptable. - - - - - - - - - - La clôture d'un exercice empêche toute modification ultérieure des écritures. - - - - - - Le plan comptable de l'organisation. Les comptes sont organisés en arborescence avec des classes (1 - à 7) à la racine. - - - - - - - - - - Les journaux comptables regroupent les écritures par type d'opération. - - - - - - - - - - Structure du bilan comptable (actif et passif). Les lignes de bilan sont liées aux comptes pour - calculer automatiquement les soldes. - - - - - - - - - - - Structure du compte de résultat. Fonctionne de manière similaire aux bilans. - - - - - - - - - - - Les libellés permettent de catégoriser les écritures comptables avec des étiquettes réutilisables. - - - - - - - - - - - Les calculs sont des formules personnalisées qui combinent des lignes de compte de résultat pour - produire des valeurs dérivées (par exemple le résultat d'exploitation). - - - - - - - - - - - Liens entre les calculs et les lignes de compte de résultat. Chaque lien définit si la ligne est - ajoutée ou soustraite dans le calcul. - - - - - - - - - - - Les écritures comptables sont les opérations enregistrées dans les journaux. Chaque écriture - contient une ou plusieurs lignes (débit/crédit). - - - - - - - - - - - - - - Chaque ligne d'écriture représente un mouvement de débit ou de crédit sur un compte. - - - - - - - - - - - - ) -} diff --git a/packages/website/src/features/docs/cli/AuthenticationCliDocPage.tsx b/packages/website/src/features/docs/cli/AuthenticationCliDocPage.tsx new file mode 100644 index 00000000..dc1b86fa --- /dev/null +++ b/packages/website/src/features/docs/cli/AuthenticationCliDocPage.tsx @@ -0,0 +1,80 @@ +import { DocCode } from "../../../components/document/DocCode.js" +import { DocCodeBlock } from "../../../components/document/DocCodeBlock.js" +import { DocExample } from "../../../components/document/DocExample.js" +import { DocHeader } from "../../../components/document/DocHeader.js" +import { DocLink } from "../../../components/document/DocLink.js" +import { DocParagraph } from "../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../components/document/DocRoot.js" +import { DocSection } from "../../../components/document/DocSection.js" +import { DocTip } from "../../../components/document/DocTip.js" + +export function AuthenticationCliDocPage() { + return ( + + + + + + Le CLI s'authentifie exclusivement par clé API. Pour en créer une, rendez-vous dans le dashboard : + + + Dashboard → Organisation → API → Clés → Nouvelle clé + + + Copiez la clé immédiatement après sa création — elle ne sera plus affichée ensuite. + + + + + + {"arrhes login --api-key --org "} + + + + {"arrhes login --api-key --org --url https://api.mondomaine.com"} + + + + Le CLI vérifie la clé auprès de l'API, puis enregistre la configuration dans{" "} + ~/.arrhes/config.json. + + + L'identifiant d'organisation (--org) est requis — il est automatiquement injecté + dans toutes les commandes. + + + L'option --url est facultative. Par défaut, le CLI se connecte à{" "} + https://api.arrhes.com. Si vous hébergez l'API vous-même, passez l'URL de votre + instance — elle sera sauvegardée dans ~/.arrhes/config.json et réutilisée pour + toutes les commandes suivantes. + + + + + arrhes whoami + + + + Pour effacer les identifiants stockés localement : + arrhes logout + + + + + Les identifiants sont stockés en clair dans ~/.arrhes/config.json. Vous pouvez + éditer ce fichier directement si nécessaire. + + + Pour utiliser plusieurs comptes ou environnements, il suffit de relancer{", "} + arrhes login avec une autre clé — le fichier est écrasé. + + + En savoir plus sur l'authentification par clé API + + + + ) +} diff --git a/packages/website/src/features/docs/cli/CommandsCliDocPage.tsx b/packages/website/src/features/docs/cli/CommandsCliDocPage.tsx new file mode 100644 index 00000000..88152c02 --- /dev/null +++ b/packages/website/src/features/docs/cli/CommandsCliDocPage.tsx @@ -0,0 +1,145 @@ +import { DocCode } from "../../../components/document/DocCode.js" +import { DocCodeBlock } from "../../../components/document/DocCodeBlock.js" +import { DocExample } from "../../../components/document/DocExample.js" +import { DocHeader } from "../../../components/document/DocHeader.js" +import { DocList } from "../../../components/document/DocList.js" +import { DocParagraph } from "../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../components/document/DocRoot.js" +import { DocSection } from "../../../components/document/DocSection.js" +import { DocTable } from "../../../components/document/DocTable.js" +import { DocTip } from "../../../components/document/DocTip.js" + +export function CommandsCliDocPage() { + return ( + + + + + + Chaque commande accepte --help pour afficher ses options. + + {"arrhes --help\narrhes entries create --help"} + + Toutes les réponses sont en JSON. Utilisez{", "} + | jq pour les filtrer ou les formater. + + + + + {"arrhes login --api-key --url --org "}, + "Enregistre les identifiants", + ], + [ + arrhes whoami, + "Affiche l'utilisateur connecté", + ], + [ + arrhes logout, + "Supprime les identifiants locaux", + ], + ]} + /> + + + + arrhes years list, + "Liste les exercices de l'organisation", + ], + [ + {"arrhes years get "}, + "Détails d'un exercice", + ], + [ + {"arrhes years create --start --end "}, + "Crée un nouvel exercice", + ], + ]} + /> + + {"arrhes years list | jq '.[].id'"} + + + + + {"arrhes entries list --year "}, + "Liste les écritures d'un exercice", + ], + [ + {"arrhes entries get --year "}, + "Détails d'une écriture", + ], + [ + + {"arrhes entries create --year --journal [--label ] [--date ]"} + , + "Crée une écriture", + ], + [ + {"arrhes entries delete --year "}, + "Supprime une écriture", + ], + ]} + /> + + + { + 'arrhes entries create \\\n --year \\\n --journal \\\n --label "Facture fournisseur" \\\n --date 2025-03-15' + } + + + + + + {"arrhes files list --year "}, + "Liste les fichiers d'un exercice", + ], + [ + {"arrhes files create --year --name [--folder ]"}, + "Enregistre un fichier", + ], + ]} + /> + + + + + + + ) +} diff --git a/packages/website/src/features/docs/cli/DemarrerCliDocPage.tsx b/packages/website/src/features/docs/cli/DemarrerCliDocPage.tsx new file mode 100644 index 00000000..1cdd50f1 --- /dev/null +++ b/packages/website/src/features/docs/cli/DemarrerCliDocPage.tsx @@ -0,0 +1,68 @@ +import { DocCode } from "../../../components/document/DocCode.js" +import { DocCodeBlock } from "../../../components/document/DocCodeBlock.js" +import { DocHeader } from "../../../components/document/DocHeader.js" +import { DocLink } from "../../../components/document/DocLink.js" +import { DocParagraph } from "../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../components/document/DocRoot.js" +import { DocSection } from "../../../components/document/DocSection.js" +import { DocTip } from "../../../components/document/DocTip.js" + +export function DemarrerCliDocPage() { + return ( + + + + + + Le CLI est un binaire autonome — aucune installation de Node.js ou de dépendance requise. Collez + l'une des commandes suivantes dans votre terminal : + + curl -fsSL https://arrhes.com/cli/install.sh | sh + Sur Windows (PowerShell) : + irm https://arrhes.com/cli/install.ps1 | iex + + Pour les options d'installation avancées (installation manuelle, autres architectures), consultez la{" "} + page Installation. + + + + + arrhes --version + + + + + Le CLI s'authentifie exclusivement par clé API. Rendez-vous dans le dashboard pour en créer une : + + + Dashboard → Organisation → API → Clés → Nouvelle clé + + + Copiez la clé immédiatement après sa création — elle ne sera plus affichée ensuite. + + + + + + {"arrhes login --api-key --url https://api.arrhes.com --org "} + + + Le CLI vérifie la clé et enregistre la configuration dans ~/.arrhes/config.json. + L'identifiant d'organisation est requis — il est utilisé automatiquement pour toutes les commandes. + + + + + Vérifiez que tout fonctionne en listant vos exercices : + arrhes years list + + Toutes les réponses du CLI sont en JSON. Combinez-les avec jq pour filtrer ou + formater les résultats. + + + + ) +} diff --git a/packages/website/src/features/docs/cli/InstallationCliDocPage.tsx b/packages/website/src/features/docs/cli/InstallationCliDocPage.tsx new file mode 100644 index 00000000..0faf37cd --- /dev/null +++ b/packages/website/src/features/docs/cli/InstallationCliDocPage.tsx @@ -0,0 +1,37 @@ +import { DocCode } from "../../../components/document/DocCode.js" +import { DocCodeBlock } from "../../../components/document/DocCodeBlock.js" +import { DocExample } from "../../../components/document/DocExample.js" +import { DocHeader } from "../../../components/document/DocHeader.js" +import { DocParagraph } from "../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../components/document/DocRoot.js" +import { DocSection } from "../../../components/document/DocSection.js" +import { DocTip } from "../../../components/document/DocTip.js" + +export function InstallationCliDocPage() { + return ( + + + + + + Collez la commande suivante dans votre terminal. Elle télécharge le script CLI et l'installe dans{" "} + ~/.local/bin. + + + curl -fsSL https://arrhes.com/cli/install.sh | sh + + + Si ~/.local/bin n'est pas dans votre PATH, le script vous + indique la ligne à ajouter dans votre ~/.bashrc ou ~/.zshrc. + + + + + arrhes --version + + + ) +} diff --git a/packages/website/src/features/docs/cli/RootCliDocPage.tsx b/packages/website/src/features/docs/cli/RootCliDocPage.tsx new file mode 100644 index 00000000..6c1387b9 --- /dev/null +++ b/packages/website/src/features/docs/cli/RootCliDocPage.tsx @@ -0,0 +1,73 @@ +import { DocHeader } from "../../../components/document/DocHeader.js" +import { DocLink } from "../../../components/document/DocLink.js" +import { DocList } from "../../../components/document/DocList.js" +import { DocParagraph } from "../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../components/document/DocRoot.js" +import { DocSection } from "../../../components/document/DocSection.js" +import { DocTip } from "../../../components/document/DocTip.js" + +export function RootCliDocPage() { + return ( + + + + + + Le CLI Arrhes est une interface en ligne de commande qui vous permet de gérer vos organisations, + exercices, écritures et fichiers directement depuis votre terminal, sans passer par l'interface web. + + + + + + + + Le CLI utilise la même API REST que l'interface web. Tout ce qui est possible dans le dashboard peut + être automatisé via le CLI. + + + + + + Installer le CLI + , + + Se connecter avec une clé API + , + + Référence des commandes + , + ]} + /> + + + ) +} diff --git a/packages/website/src/features/docs/cli/commands/BalanceSheetsCommandsCliDocPage.tsx b/packages/website/src/features/docs/cli/commands/BalanceSheetsCommandsCliDocPage.tsx new file mode 100644 index 00000000..5a459d07 --- /dev/null +++ b/packages/website/src/features/docs/cli/commands/BalanceSheetsCommandsCliDocPage.tsx @@ -0,0 +1,111 @@ +import { DocCode } from "../../../../components/document/DocCode.js" +import { DocCodeBlock } from "../../../../components/document/DocCodeBlock.js" +import { DocHeader } from "../../../../components/document/DocHeader.js" +import { DocParagraph } from "../../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../../components/document/DocRoot.js" +import { DocSection } from "../../../../components/document/DocSection.js" +import { DocTable } from "../../../../components/document/DocTable.js" + +export function BalanceSheetsCommandsCliDocPage() { + return ( + + + + + {"arrhes balance-sheets list "}, + "Liste les nœuds du bilan", + ], + [ + {"arrhes balance-sheets get "}, + "Détails d'un nœud", + ], + [ + {"arrhes balance-sheets create "}, + "Crée un nœud de bilan", + ], + [ + {"arrhes balance-sheets update "}, + "Modifie un nœud", + ], + [ + {"arrhes balance-sheets delete "}, + "Supprime un nœud", + ], + ]} + /> + + + + arrhes balance-sheets list year_xyz + + + + arrhes balance-sheets get year_xyz bs_abc + + + + Crée un nœud de bilan. Le parent est facultatif (nœud racine si absent). + ", + "Non", + "ID du nœud parent", + ], + [ + "--label ", + "Non", + "Libellé du nœud", + ], + ]} + /> + arrhes balance-sheets create year_xyz --label "Actif immobilisé" + + arrhes balance-sheets create year_xyz --parent bs_root --label "Immobilisations corporelles" + + + + + ", + "Non", + "ID du nœud parent", + ], + [ + "--label ", + "Non", + "Libellé du nœud", + ], + ]} + /> + arrhes balance-sheets update year_xyz bs_abc --label "Actif circulant" + + + + arrhes balance-sheets delete year_xyz bs_abc + + + ) +} diff --git a/packages/website/src/features/docs/cli/commands/ClesApiCommandsCliDocPage.tsx b/packages/website/src/features/docs/cli/commands/ClesApiCommandsCliDocPage.tsx new file mode 100644 index 00000000..6e16f6f5 --- /dev/null +++ b/packages/website/src/features/docs/cli/commands/ClesApiCommandsCliDocPage.tsx @@ -0,0 +1,80 @@ +import { DocCode } from "../../../../components/document/DocCode.js" +import { DocCodeBlock } from "../../../../components/document/DocCodeBlock.js" +import { DocHeader } from "../../../../components/document/DocHeader.js" +import { DocParagraph } from "../../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../../components/document/DocRoot.js" +import { DocSection } from "../../../../components/document/DocSection.js" +import { DocTable } from "../../../../components/document/DocTable.js" +import { DocTip } from "../../../../components/document/DocTip.js" + +export function ClesApiCommandsCliDocPage() { + return ( + + + + + arrhes api-keys list, + "Liste les clés API de l'organisation", + ], + [ + arrhes api-keys create, + "Crée une nouvelle clé API", + ], + [ + {"arrhes api-keys delete "}, + "Révoque une clé API", + ], + ]} + /> + + + + arrhes api-keys list + + + + + Crée une nouvelle clé API. La valeur brute de la clé n'est affichée qu'une seule fois à la création + — conservez-la en lieu sûr. + + ", + "Non", + "Nom mnémotechnique pour la clé", + ], + ]} + /> + arrhes api-keys create --name "CI/CD pipeline" + + La valeur du champ rawKey dans la réponse est la clé secrète. Elle ne sera plus + jamais accessible après cette commande. + + + + + + Révoque définitivement une clé API. Toutes les intégrations utilisant cette clé cesseront de + fonctionner immédiatement. + + arrhes api-keys delete key_abc + + + ) +} diff --git a/packages/website/src/features/docs/cli/commands/ComptesCommandsCliDocPage.tsx b/packages/website/src/features/docs/cli/commands/ComptesCommandsCliDocPage.tsx new file mode 100644 index 00000000..62ef14f3 --- /dev/null +++ b/packages/website/src/features/docs/cli/commands/ComptesCommandsCliDocPage.tsx @@ -0,0 +1,146 @@ +import { DocCode } from "../../../../components/document/DocCode.js" +import { DocCodeBlock } from "../../../../components/document/DocCodeBlock.js" +import { DocHeader } from "../../../../components/document/DocHeader.js" +import { DocParagraph } from "../../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../../components/document/DocRoot.js" +import { DocSection } from "../../../../components/document/DocSection.js" +import { DocTable } from "../../../../components/document/DocTable.js" + +export function ComptesCommandsCliDocPage() { + return ( + + + + + {"arrhes accounts list --year "}, + "Liste les comptes d'un exercice", + ], + [ + {"arrhes accounts get --year "}, + "Détails d'un compte", + ], + [ + + {"arrhes accounts create --year --number --label --type --parent "} + , + "Crée un compte", + ], + [ + {"arrhes accounts update --year "}, + "Modifie un compte", + ], + [ + {"arrhes accounts delete --year "}, + "Supprime un compte", + ], + ]} + /> + + + + Retourne l'ensemble des comptes du plan comptable de l'exercice. + arrhes accounts list --year year_xyz + {"arrhes accounts list --year year_xyz | jq '.[].number'"} + + + + arrhes accounts get acc_abc --year year_xyz + + + + ", + "Oui", + "Identifiant de l'exercice", + ], + [ + "--number ", + "Oui", + "Numéro de compte (ex. 401)", + ], + [ + "--label ", + "Oui", + "Libellé du compte", + ], + [ + "--type ", + "Oui", + "Type de compte", + ], + [ + "--parent ", + "Oui", + "Identifiant du compte parent", + ], + [ + "--no-selectable", + "Non", + "Marque le compte comme non sélectionnable pour les écritures", + ], + ]} + /> + + arrhes accounts create --year year_xyz --number 401 --label "Fournisseurs" --type liability --parent + acc_root + + + + + ", + "Non", + "Nouveau numéro de compte", + ], + [ + "--label ", + "Non", + "Nouveau libellé", + ], + [ + "--type ", + "Non", + "Nouveau type", + ], + [ + "--parent ", + "Non", + "Nouveau compte parent", + ], + ]} + /> + + arrhes accounts update acc_abc --year year_xyz --label "Fournisseurs divers" + + + + + arrhes accounts delete acc_abc --year year_xyz + + + ) +} diff --git a/packages/website/src/features/docs/cli/commands/EcrituresCommandsCliDocPage.tsx b/packages/website/src/features/docs/cli/commands/EcrituresCommandsCliDocPage.tsx new file mode 100644 index 00000000..2a261c8a --- /dev/null +++ b/packages/website/src/features/docs/cli/commands/EcrituresCommandsCliDocPage.tsx @@ -0,0 +1,349 @@ +import { DocCode } from "../../../../components/document/DocCode.js" +import { DocCodeBlock } from "../../../../components/document/DocCodeBlock.js" +import { DocHeader } from "../../../../components/document/DocHeader.js" +import { DocParagraph } from "../../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../../components/document/DocRoot.js" +import { DocSection } from "../../../../components/document/DocSection.js" +import { DocTable } from "../../../../components/document/DocTable.js" +import { DocTip } from "../../../../components/document/DocTip.js" + +export function EcrituresCommandsCliDocPage() { + return ( + + + + + {"arrhes entries list --year "}, + "Liste les écritures d'un exercice", + ], + [ + {"arrhes entries get --year "}, + "Détails d'une écriture", + ], + [ + {"arrhes entries create --year --journal "}, + "Crée une écriture", + ], + [ + {"arrhes entries update --year "}, + "Modifie une écriture", + ], + [ + {"arrhes entries duplicate --year "}, + "Duplique une écriture", + ], + [ + {"arrhes entries reverse --year "}, + "Extourne une écriture", + ], + [ + {"arrhes entries delete --year "}, + "Supprime une écriture", + ], + [ + {"arrhes entries compute --year "}, + "Calcule les totaux d'une écriture", + ], + [ + {"arrhes entries lines list --year "}, + "Liste les lignes d'une écriture", + ], + [ + {"arrhes entries lines get --year "}, + "Détails d'une ligne", + ], + [ + + {"arrhes entries lines create --year --account "} + , + "Crée une ligne d'écriture", + ], + [ + + {"arrhes entries lines update --year "} + , + "Modifie une ligne", + ], + [ + + {"arrhes entries lines delete --year "} + , + "Supprime une ligne", + ], + [ + {"arrhes entries tags add --year --tag "}, + "Ajoute un libellé à une écriture", + ], + [ + {"arrhes entries tags remove --year "}, + "Retire un libellé", + ], + ]} + /> + + + + arrhes entries list --year year_xyz + {"arrhes entries list --year year_xyz | jq '.[].label'"} + + + + arrhes entries get entry_123 --year year_xyz + + + + ", + "Oui", + "Identifiant de l'exercice", + ], + [ + "--journal ", + "Oui", + "Identifiant du journal", + ], + [ + "--label ", + "Non", + "Libellé de l'écriture", + ], + [ + "--date ", + "Non", + "Date au format YYYY-MM-DD (défaut : aujourd'hui)", + ], + ]} + /> + + { + 'arrhes entries create \\\n --year year_xyz \\\n --journal AC \\\n --label "Facture fournisseur" \\\n --date 2025-03-15' + } + + + Utilisez arrhes journals list --year <id> pour retrouver les identifiants + de journaux disponibles dans un exercice. + + + + + ", + "Oui", + "Identifiant de l'exercice", + ], + [ + "--label ", + "Non", + "Nouveau libellé", + ], + [ + "--date ", + "Non", + "Nouvelle date (YYYY-MM-DD)", + ], + [ + "--journal ", + "Non", + "Nouveau journal", + ], + [ + "--file ", + "Non", + "Pièce justificative associée", + ], + ]} + /> + arrhes entries update entry_123 --year year_xyz --label "Libellé corrigé" + + + + Crée une copie de l'écriture avec les mêmes lignes. + arrhes entries duplicate entry_123 --year year_xyz + + + + + Crée une écriture d'extourne (contrepartie) pour annuler l'écriture d'origine. + + arrhes entries reverse entry_123 --year year_xyz + + + + Supprime définitivement une écriture et ses lignes d'écriture associées. + arrhes entries delete entry_123 --year year_xyz + + La suppression est irréversible. L'ensemble des lignes d'écriture liées sera également supprimé. + + + + + Calcule les totaux (débit / crédit) d'une écriture. + ", + "Oui", + "Identifiant de l'exercice", + ], + ]} + /> + arrhes entries compute entry_123 --year year_xyz + + + + arrhes entries lines list entry_123 --year year_xyz + + + + arrhes entries lines get entry_123 line_abc --year year_xyz + + + + ", + "Oui", + "Identifiant de l'exercice", + ], + [ + "--account ", + "Oui", + "Identifiant du compte", + ], + [ + "--label ", + "Non", + "Libellé de la ligne", + ], + [ + "--debit ", + "Non", + "Montant au débit", + ], + [ + "--credit ", + "Non", + "Montant au crédit", + ], + ]} + /> + + arrhes entries lines create entry_123 --year year_xyz --account acc_xyz --debit 1000 + + + + + ", + "Oui", + "Identifiant de l'exercice", + ], + [ + "--label ", + "Non", + "Nouveau libellé", + ], + [ + "--debit ", + "Non", + "Nouveau montant au débit", + ], + [ + "--credit ", + "Non", + "Nouveau montant au crédit", + ], + ]} + /> + arrhes entries lines update entry_123 line_abc --year year_xyz --debit 1500 + + + + arrhes entries lines delete entry_123 line_abc --year year_xyz + + + + ", + "Oui", + "Identifiant de l'exercice", + ], + [ + "--tag ", + "Oui", + "Identifiant du libellé", + ], + ]} + /> + arrhes entries tags add entry_123 --year year_xyz --tag tag_abc + + + + ", + "Oui", + "Identifiant de l'exercice", + ], + ]} + /> + arrhes entries tags remove entry_123 tag_abc --year year_xyz + + + ) +} diff --git a/packages/website/src/features/docs/cli/commands/ExercicesCommandsCliDocPage.tsx b/packages/website/src/features/docs/cli/commands/ExercicesCommandsCliDocPage.tsx new file mode 100644 index 00000000..c2f63659 --- /dev/null +++ b/packages/website/src/features/docs/cli/commands/ExercicesCommandsCliDocPage.tsx @@ -0,0 +1,215 @@ +import { DocCode } from "../../../../components/document/DocCode.js" +import { DocCodeBlock } from "../../../../components/document/DocCodeBlock.js" +import { DocHeader } from "../../../../components/document/DocHeader.js" +import { DocParagraph } from "../../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../../components/document/DocRoot.js" +import { DocSection } from "../../../../components/document/DocSection.js" +import { DocTable } from "../../../../components/document/DocTable.js" +import { DocTip } from "../../../../components/document/DocTip.js" + +export function ExercicesCommandsCliDocPage() { + return ( + + + + + arrhes years list, + "Liste les exercices de l'organisation", + ], + [ + {"arrhes years get "}, + "Détails d'un exercice", + ], + [ + {"arrhes years create --start --end "}, + "Crée un exercice", + ], + [ + {"arrhes years update "}, + "Modifie un exercice", + ], + [ + {"arrhes years delete "}, + "Supprime un exercice", + ], + [ + {"arrhes years close "}, + "Clôture un exercice", + ], + [ + {"arrhes years open --journal-opening "}, + "Réouvre un exercice", + ], + [ + + {"arrhes years settle-balance-sheet --journal-closing "} + , + "Solde le bilan", + ], + [ + + {"arrhes years settle-income-statement --journal-closing "} + , + "Solde le compte de résultat", + ], + ]} + /> + + + + Liste tous les exercices comptables de l'organisation configurée. + arrhes years list + {"arrhes years list | jq '.[].id'"} + + + + Retourne les détails d'un exercice par son identifiant. + arrhes years get year_xyz + + + + Crée un nouvel exercice dans l'organisation spécifiée. + ", + "Oui", + "Date de début (YYYY-MM-DD)", + ], + [ + "--end ", + "Oui", + "Date de fin (YYYY-MM-DD)", + ], + [ + "--label ", + "Non", + "Libellé de l'exercice", + ], + ]} + /> + + arrhes years create --start 2025-01-01 --end 2025-12-31 --label "Exercice 2025" + + + La commande retourne l'objet exercice créé, incluant son identifiant à utiliser dans les commandes{" "} + entries, journals et files. + + + + + ", + "Non", + "Nouvelle date de début", + ], + [ + "--end ", + "Non", + "Nouvelle date de fin", + ], + [ + "--label ", + "Non", + "Nouveau libellé", + ], + ]} + /> + arrhes years update year_xyz --label "FY 2025" + + + + Supprime définitivement un exercice et toutes ses données. + arrhes years delete year_xyz + + + + + Clôture un exercice. Une fois clôturé, les écritures ne peuvent plus être modifiées. + + arrhes years close year_xyz + + + + Réouvre un exercice clôturé. Nécessite un journal d'ouverture. + ", + "Oui", + "Identifiant du journal d'ouverture", + ], + ]} + /> + arrhes years open year_xyz --journal-opening jrn_abc + + + + Génère les écritures de solde du bilan dans le journal de clôture spécifié. + ", + "Oui", + "Identifiant du journal de clôture", + ], + ]} + /> + arrhes years settle-balance-sheet year_xyz --journal-closing jrn_closing + + + + + Génère les écritures de solde du compte de résultat dans le journal de clôture spécifié. + + ", + "Oui", + "Identifiant du journal de clôture", + ], + ]} + /> + arrhes years settle-income-statement year_xyz --journal-closing jrn_closing + + + ) +} diff --git a/packages/website/src/features/docs/cli/commands/ExportsCommandsCliDocPage.tsx b/packages/website/src/features/docs/cli/commands/ExportsCommandsCliDocPage.tsx new file mode 100644 index 00000000..ec9e68c5 --- /dev/null +++ b/packages/website/src/features/docs/cli/commands/ExportsCommandsCliDocPage.tsx @@ -0,0 +1,120 @@ +import { DocCode } from "../../../../components/document/DocCode.js" +import { DocCodeBlock } from "../../../../components/document/DocCodeBlock.js" +import { DocHeader } from "../../../../components/document/DocHeader.js" +import { DocParagraph } from "../../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../../components/document/DocRoot.js" +import { DocSection } from "../../../../components/document/DocSection.js" +import { DocTable } from "../../../../components/document/DocTable.js" +import { DocTip } from "../../../../components/document/DocTip.js" + +export function ExportsCommandsCliDocPage() { + return ( + + + + + {"arrhes exports fec --year "}, + "Génère un export FEC", + ], + [ + {"arrhes exports xbrl-balance-sheet --year "}, + "Génère un export XBRL du bilan", + ], + [ + {"arrhes exports xbrl-income-statement --year "}, + "Génère un export XBRL du compte de résultat", + ], + ]} + /> + + + + + Génère un Fichier des Écritures Comptables (FEC) pour l'exercice spécifié. La commande retourne une + URL signée pour télécharger le fichier. + + ", + "Oui", + "Identifiant de l'exercice", + ], + ]} + /> + arrhes exports fec --year year_xyz + {'URL=$(arrhes exports fec --year year_xyz)\ncurl -o fec.txt "$URL"'} + + Le FEC est un fichier texte au format réglementaire requis par l'administration fiscale française. + Il est généré à partir de toutes les écritures validées de l'exercice. Utilisez la commande{" "} + arrhes years close avant de générer le FEC final. + + + + + + Génère un fichier XBRL du bilan comptable pour l'exercice spécifié. La commande retourne une URL + signée pour télécharger le fichier. + + ", + "Oui", + "Identifiant de l'exercice", + ], + ]} + /> + arrhes exports xbrl-balance-sheet --year year_xyz + + {'URL=$(arrhes exports xbrl-balance-sheet --year year_xyz)\ncurl -o bilan.xml "$URL"'} + + + + + + Génère un fichier XBRL du compte de résultat pour l'exercice spécifié. La commande retourne une URL + signée pour télécharger le fichier. + + ", + "Oui", + "Identifiant de l'exercice", + ], + ]} + /> + arrhes exports xbrl-income-statement --year year_xyz + + {'URL=$(arrhes exports xbrl-income-statement --year year_xyz)\ncurl -o compte-resultat.xml "$URL"'} + + + + ) +} diff --git a/packages/website/src/features/docs/cli/commands/FichiersCommandsCliDocPage.tsx b/packages/website/src/features/docs/cli/commands/FichiersCommandsCliDocPage.tsx new file mode 100644 index 00000000..d36ad7de --- /dev/null +++ b/packages/website/src/features/docs/cli/commands/FichiersCommandsCliDocPage.tsx @@ -0,0 +1,262 @@ +import { DocCode } from "../../../../components/document/DocCode.js" +import { DocCodeBlock } from "../../../../components/document/DocCodeBlock.js" +import { DocHeader } from "../../../../components/document/DocHeader.js" +import { DocParagraph } from "../../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../../components/document/DocRoot.js" +import { DocSection } from "../../../../components/document/DocSection.js" +import { DocTable } from "../../../../components/document/DocTable.js" +import { DocTip } from "../../../../components/document/DocTip.js" + +export function FichiersCommandsCliDocPage() { + return ( + + + + + {"arrhes files list --year "}, + "Liste les fichiers d'un exercice", + ], + [ + {"arrhes files get --year "}, + "Détails d'un fichier", + ], + [ + + {"arrhes files create --year --name --reference --hash "} + , + "Enregistre un fichier", + ], + [ + {"arrhes files update --year "}, + "Modifie un fichier", + ], + [ + {"arrhes files delete --year "}, + "Supprime un fichier", + ], + [ + {"arrhes files download-url --year "}, + "Génère une URL de téléchargement", + ], + ]} + /> + + + + arrhes files list --year year_xyz + + + + arrhes files get file_abc --year year_xyz + + + + Enregistre les métadonnées d'un fichier dans l'exercice. + ", + "Oui", + "Identifiant de l'exercice", + ], + [ + "--name ", + "Oui", + "Nom du fichier (avec extension)", + ], + [ + "--reference ", + "Oui", + "Numéro de référence du fichier", + ], + [ + "--hash ", + "Oui", + "Hash SHA-256 du fichier", + ], + [ + "--folder ", + "Non", + "Identifiant du dossier parent", + ], + ]} + /> + + { + "arrhes files create --year year_xyz \\\n --name facture-2025-001.pdf --reference FA-001 --hash abc123..." + } + + + Pour uploader physiquement un fichier, utilisez les routes API de génération d'URL signée + disponibles dans la documentation API. + + + + + ", + "Non", + "Nouveau nom du fichier", + ], + [ + "--reference ", + "Non", + "Nouvelle référence", + ], + [ + "--date ", + "Non", + "Date du fichier (YYYY-MM-DD)", + ], + [ + "--folder ", + "Non", + "Nouveau dossier parent", + ], + ]} + /> + arrhes files update file_abc --year year_xyz --name facture-corrigee.pdf + + + + arrhes files delete file_abc --year year_xyz + + + + + Génère une URL signée temporaire pour télécharger le fichier. L'URL est affichée dans la sortie + standard. + + arrhes files download-url file_abc --year year_xyz + + {'URL=$(arrhes files download-url file_abc --year year_xyz)\ncurl -o facture.pdf "$URL"'} + + + + + + Les dossiers sont gérés via le sous-groupe arrhes files folders. + + {"arrhes files folders list --year "}, + "Liste les dossiers d'un exercice", + ], + [ + {"arrhes files folders get --year "}, + "Détails d'un dossier", + ], + [ + {"arrhes files folders create --year --name "}, + "Crée un dossier", + ], + [ + {"arrhes files folders update --year "}, + "Modifie un dossier", + ], + [ + {"arrhes files folders delete --year "}, + "Supprime un dossier", + ], + ]} + /> + + + + arrhes files folders list --year year_xyz + + + + arrhes files folders get folder_abc --year year_xyz + + + + ", + "Oui", + "Identifiant de l'exercice", + ], + [ + "--name ", + "Oui", + "Nom du dossier", + ], + [ + "--parent ", + "Non", + "Identifiant du dossier parent", + ], + ]} + /> + arrhes files folders create --year year_xyz --name "Factures fournisseurs" + + arrhes files folders create --year year_xyz --name "Mars 2025" --parent folder_abc + + + + + ", + "Non", + "Nouveau nom", + ], + [ + "--parent ", + "Non", + "Nouveau dossier parent", + ], + ]} + /> + + arrhes files folders update folder_abc --year year_xyz --name "Factures achats" + + + + + Supprime un dossier vide. + arrhes files folders delete folder_abc --year year_xyz + + + ) +} diff --git a/packages/website/src/features/docs/cli/commands/IncomeStatementsCommandsCliDocPage.tsx b/packages/website/src/features/docs/cli/commands/IncomeStatementsCommandsCliDocPage.tsx new file mode 100644 index 00000000..360653a5 --- /dev/null +++ b/packages/website/src/features/docs/cli/commands/IncomeStatementsCommandsCliDocPage.tsx @@ -0,0 +1,193 @@ +import { DocCode } from "../../../../components/document/DocCode.js" +import { DocCodeBlock } from "../../../../components/document/DocCodeBlock.js" +import { DocHeader } from "../../../../components/document/DocHeader.js" +import { DocParagraph } from "../../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../../components/document/DocRoot.js" +import { DocSection } from "../../../../components/document/DocSection.js" +import { DocTable } from "../../../../components/document/DocTable.js" + +export function IncomeStatementsCommandsCliDocPage() { + return ( + + + + + {"arrhes income-statements list "}, + "Liste les nœuds du compte de résultat", + ], + [ + {"arrhes income-statements get "}, + "Détails d'un nœud", + ], + [ + {"arrhes income-statements create "}, + "Crée un nœud", + ], + [ + {"arrhes income-statements update "}, + "Modifie un nœud", + ], + [ + {"arrhes income-statements delete "}, + "Supprime un nœud", + ], + [ + {"arrhes income-statements computations list "}, + "Liste les calculs", + ], + [ + + {"arrhes income-statements computations get "} + , + "Détails d'un calcul", + ], + [ + {"arrhes income-statements computations create "}, + "Crée un calcul", + ], + [ + + {"arrhes income-statements computations update "} + , + "Modifie un calcul", + ], + [ + + {"arrhes income-statements computations delete "} + , + "Supprime un calcul", + ], + ]} + /> + + + + arrhes income-statements list year_xyz + + + + arrhes income-statements get year_xyz is_abc + + + + + Crée un nœud de compte de résultat. Le parent est facultatif (nœud racine si absent). + + ", + "Oui", + "Libellé du nœud", + ], + [ + "--parent ", + "Non", + "ID du nœud parent", + ], + ]} + /> + arrhes income-statements create year_xyz --label "Produits d'exploitation" + + arrhes income-statements create year_xyz --parent is_root --label "Chiffre d'affaires" + + + + + ", + "Non", + "Libellé du nœud", + ], + [ + "--parent ", + "Non", + "ID du nœud parent", + ], + ]} + /> + + arrhes income-statements update year_xyz is_abc --label "Charges d'exploitation" + + + + + arrhes income-statements delete year_xyz is_abc + + + + arrhes income-statements computations list year_xyz + + + + arrhes income-statements computations get year_xyz cmp_abc + + + + ", + "Oui", + "Libellé du calcul", + ], + ]} + /> + + arrhes income-statements computations create year_xyz --label "Résultat d'exploitation" + + + + + ", + "Non", + "Libellé du calcul", + ], + ]} + /> + + arrhes income-statements computations update year_xyz cmp_abc --label "Résultat net" + + + + + arrhes income-statements computations delete year_xyz cmp_abc + + + ) +} diff --git a/packages/website/src/features/docs/cli/commands/JournauxCommandsCliDocPage.tsx b/packages/website/src/features/docs/cli/commands/JournauxCommandsCliDocPage.tsx new file mode 100644 index 00000000..bb654571 --- /dev/null +++ b/packages/website/src/features/docs/cli/commands/JournauxCommandsCliDocPage.tsx @@ -0,0 +1,120 @@ +import { DocCode } from "../../../../components/document/DocCode.js" +import { DocCodeBlock } from "../../../../components/document/DocCodeBlock.js" +import { DocHeader } from "../../../../components/document/DocHeader.js" +import { DocParagraph } from "../../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../../components/document/DocRoot.js" +import { DocSection } from "../../../../components/document/DocSection.js" +import { DocTable } from "../../../../components/document/DocTable.js" + +export function JournauxCommandsCliDocPage() { + return ( + + + + + {"arrhes journals list --year "}, + "Liste les journaux d'un exercice", + ], + [ + {"arrhes journals get --year "}, + "Détails d'un journal", + ], + [ + + {"arrhes journals create --year --code --label "} + , + "Crée un journal", + ], + [ + {"arrhes journals update --year "}, + "Modifie un journal", + ], + [ + {"arrhes journals delete --year "}, + "Supprime un journal", + ], + ]} + /> + + + + arrhes journals list --year year_xyz + {"arrhes journals list --year year_xyz | jq '.[].code'"} + + + + arrhes journals get jrn_abc --year year_xyz + + + + ", + "Oui", + "Identifiant de l'exercice", + ], + [ + "--code ", + "Oui", + "Code du journal (ex : ACH, VTE, BQ)", + ], + [ + "--label ", + "Oui", + "Libellé du journal", + ], + ]} + /> + arrhes journals create --year year_xyz --code ACH --label "Achats" + + + + ", + "Non", + "Nouveau code", + ], + [ + "--label ", + "Non", + "Nouveau libellé", + ], + ]} + /> + + arrhes journals update jrn_abc --year year_xyz --label "Achats fournisseurs" + + + + + + Supprime un journal. Cette action échoue si des écritures y sont encore rattachées. + + arrhes journals delete jrn_abc --year year_xyz + + + ) +} diff --git a/packages/website/src/features/docs/cli/commands/LibellesCommandsCliDocPage.tsx b/packages/website/src/features/docs/cli/commands/LibellesCommandsCliDocPage.tsx new file mode 100644 index 00000000..4661b87b --- /dev/null +++ b/packages/website/src/features/docs/cli/commands/LibellesCommandsCliDocPage.tsx @@ -0,0 +1,93 @@ +import { DocCode } from "../../../../components/document/DocCode.js" +import { DocCodeBlock } from "../../../../components/document/DocCodeBlock.js" +import { DocHeader } from "../../../../components/document/DocHeader.js" +import { DocParagraph } from "../../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../../components/document/DocRoot.js" +import { DocSection } from "../../../../components/document/DocSection.js" +import { DocTable } from "../../../../components/document/DocTable.js" + +export function LibellesCommandsCliDocPage() { + return ( + + + + + {"arrhes tags list --year "}, + "Liste les libellés d'un exercice", + ], + [ + {"arrhes tags get --year "}, + "Détails d'un libellé", + ], + [ + {"arrhes tags create --year --label "}, + "Crée un libellé", + ], + [ + {"arrhes tags update --year "}, + "Modifie un libellé", + ], + [ + {"arrhes tags delete --year "}, + "Supprime un libellé", + ], + ]} + /> + + + + arrhes tags list --year year_xyz + + + + arrhes tags get tag_abc --year year_xyz + + + + ", + "Oui", + "Identifiant de l'exercice", + ], + [ + "--label ", + "Oui", + "Libellé du tag", + ], + ]} + /> + arrhes tags create --year year_xyz --label "Investissement" + + + + + arrhes tags update tag_abc --year year_xyz --label "Investissement matériel" + + + + + + Supprime un libellé. Les écritures associées conservent leurs autres libellés. + + arrhes tags delete tag_abc --year year_xyz + + + ) +} diff --git a/packages/website/src/features/docs/cli/commands/MembresCommandsCliDocPage.tsx b/packages/website/src/features/docs/cli/commands/MembresCommandsCliDocPage.tsx new file mode 100644 index 00000000..134d9a4c --- /dev/null +++ b/packages/website/src/features/docs/cli/commands/MembresCommandsCliDocPage.tsx @@ -0,0 +1,101 @@ +import { DocCode } from "../../../../components/document/DocCode.js" +import { DocCodeBlock } from "../../../../components/document/DocCodeBlock.js" +import { DocHeader } from "../../../../components/document/DocHeader.js" +import { DocParagraph } from "../../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../../components/document/DocRoot.js" +import { DocSection } from "../../../../components/document/DocSection.js" +import { DocTable } from "../../../../components/document/DocTable.js" +import { DocTip } from "../../../../components/document/DocTip.js" + +export function MembresCommandsCliDocPage() { + return ( + + + + + arrhes members list, + "Liste les membres de l'organisation", + ], + [ + {"arrhes members get "}, + "Détails d'un membre", + ], + [ + {"arrhes members invite --email "}, + "Invite un utilisateur", + ], + [ + {"arrhes members update "}, + "Modifie les droits d'un membre", + ], + [ + {"arrhes members remove "}, + "Retire un membre de l'organisation", + ], + ]} + /> + + + + arrhes members list + + + + arrhes members get usr_abc + + + + ", + "Oui", + "Adresse email de l'utilisateur à inviter", + ], + [ + "--admin", + "Non", + "Accorder les droits administrateur", + ], + ]} + /> + arrhes members invite --email colleague@example.com + arrhes members invite --email admin@example.com --admin + + + + + Modifie les droits d'un membre. Utilisez --admin pour accorder les droits + administrateur ou --no-admin pour les révoquer. + + arrhes members update usr_abc --admin + arrhes members update usr_abc --no-admin + + + + + Retire un membre de l'organisation. L'utilisateur perd immédiatement l'accès. + + arrhes members remove usr_abc + + Cette action est irréversible. L'utilisateur devra être réinvité pour retrouver l'accès. + + + + ) +} diff --git a/packages/website/src/features/docs/cli/commands/OrgCommandsCliDocPage.tsx b/packages/website/src/features/docs/cli/commands/OrgCommandsCliDocPage.tsx new file mode 100644 index 00000000..ac191fcc --- /dev/null +++ b/packages/website/src/features/docs/cli/commands/OrgCommandsCliDocPage.tsx @@ -0,0 +1,92 @@ +import { DocCode } from "../../../../components/document/DocCode.js" +import { DocCodeBlock } from "../../../../components/document/DocCodeBlock.js" +import { DocHeader } from "../../../../components/document/DocHeader.js" +import { DocParagraph } from "../../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../../components/document/DocRoot.js" +import { DocSection } from "../../../../components/document/DocSection.js" +import { DocTable } from "../../../../components/document/DocTable.js" +import { DocTip } from "../../../../components/document/DocTip.js" + +export function OrgCommandsCliDocPage() { + return ( + + + + + arrhes org get, + "Affiche les détails de l'organisation", + ], + [ + arrhes org update, + "Modifie les informations de l'organisation", + ], + [ + arrhes org delete, + "Supprime l'organisation et toutes ses données", + ], + ]} + /> + + + + + Retourne les informations de l'organisation active (nom, email, SIREN, etc.). + + arrhes org get + + + + + Modifie les informations de l'organisation. Toutes les options sont facultatives. + + ", + "Non", + "Nom de l'organisation", + ], + [ + "--email ", + "Non", + "Email de contact de l'organisation", + ], + [ + "--siren ", + "Non", + "Numéro SIREN de l'organisation", + ], + ]} + /> + arrhes org update --name "Ma Société" + arrhes org update --email contact@example.com --siren 123456789 + + + + + Supprime l'organisation active et toutes ses données de façon irréversible. Nécessite le rôle + administrateur. + + arrhes org delete + + La suppression est irréversible et supprime tous les exercices, écritures et fichiers associés. + + + + ) +} diff --git a/packages/website/src/features/docs/components/DataError.tsx b/packages/website/src/features/docs/components/DataError.tsx index 4ed20f08..805b704b 100644 --- a/packages/website/src/features/docs/components/DataError.tsx +++ b/packages/website/src/features/docs/components/DataError.tsx @@ -1,5 +1,5 @@ import { LinkContent } from "@arrhes/ui" -import { DocTip } from "../../../components/document/docTip.js" +import { DocTip } from "../../../components/document/DocTip.js" export function DataError() { return ( diff --git a/packages/website/src/features/docs/dashboard/BillingDashboardDocPage.tsx b/packages/website/src/features/docs/dashboard/BillingDashboardDocPage.tsx index 5e430999..41a4118b 100644 --- a/packages/website/src/features/docs/dashboard/BillingDashboardDocPage.tsx +++ b/packages/website/src/features/docs/dashboard/BillingDashboardDocPage.tsx @@ -1,9 +1,9 @@ -import { DocHeader } from "../../../components/document/docHeader.js" -import { DocLink } from "../../../components/document/docLink.js" -import { DocList } from "../../../components/document/docList.js" -import { DocParagraph } from "../../../components/document/docParagraph.js" -import { DocRoot } from "../../../components/document/docRoot.js" -import { DocSection } from "../../../components/document/docSection.js" +import { DocHeader } from "../../../components/document/DocHeader.js" +import { DocLink } from "../../../components/document/DocLink.js" +import { DocList } from "../../../components/document/DocList.js" +import { DocParagraph } from "../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../components/document/DocRoot.js" +import { DocSection } from "../../../components/document/DocSection.js" export function BillingDashboardDocPage() { return ( diff --git a/packages/website/src/features/docs/dashboard/EntriesDashboardDocPage.tsx b/packages/website/src/features/docs/dashboard/EntriesDashboardDocPage.tsx new file mode 100644 index 00000000..1669cada --- /dev/null +++ b/packages/website/src/features/docs/dashboard/EntriesDashboardDocPage.tsx @@ -0,0 +1,171 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import { DocExample } from "../../../components/document/DocExample.js" +import { DocHeader } from "../../../components/document/DocHeader.js" +import { DocLink } from "../../../components/document/DocLink.js" +import { DocList } from "../../../components/document/DocList.js" +import { DocParagraph } from "../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../components/document/DocRoot.js" +import { DocSection } from "../../../components/document/DocSection.js" +import { DocTable } from "../../../components/document/DocTable.js" + +export function EntriesDashboardDocPage() { + return ( + + + + + + Pour saisir une écriture, accédez à votre exercice comptable puis cliquez sur l'onglet Écritures. + Vous verrez la liste des écritures existantes et un bouton pour en ajouter une nouvelle. + + + + + Chaque écriture comptable nécessite les informations suivantes : + + + + + + + + + + Chaque ligne d'écriture correspond à un compte mouvementé. Vous devez indiquer : + + + + Rappel : comme expliqué dans le cours sur les{" "} + écritures comptables, le total des + débits doit toujours être égal au total des crédits. + + + +

+ Vous payez une facture de 500 euros par virement bancaire : +

+ +

+ Explication : la dette fournisseur diminue (débit d'un compte de passif), la banque diminue + (crédit d'un compte d'actif). +

+
+
+ + + + Chaque écriture devrait être justifiée par une pièce (facture, relevé bancaire, ticket...). Arrhes + vous permet de joindre des fichiers numériques à vos écritures. + + + + + + + + + + Vous pouvez modifier ou supprimer une écriture tant que l'exercice n'est pas clôturé. Pour cela, + ouvrez l'écriture et utilisez les boutons correspondants. + + + Attention : en comptabilité, il est généralement préférable de passer une écriture + de correction plutôt que de supprimer une écriture erronée. Cela permet de garder une trace de + l'erreur et de sa correction. + + + + + + La liste des écritures peut être filtrée et triée selon plusieurs critères : + + + Utilisez les filtres en haut de la liste pour affiner votre recherche. + + + + + +
+ ) +} diff --git a/packages/website/src/features/docs/dashboard/FilesDashboardDocPage.tsx b/packages/website/src/features/docs/dashboard/FilesDashboardDocPage.tsx new file mode 100644 index 00000000..3f4da31c --- /dev/null +++ b/packages/website/src/features/docs/dashboard/FilesDashboardDocPage.tsx @@ -0,0 +1,94 @@ +import { DocExample } from "../../../components/document/DocExample.js" +import { DocHeader } from "../../../components/document/DocHeader.js" +import { DocLink } from "../../../components/document/DocLink.js" +import { DocList } from "../../../components/document/DocList.js" +import { DocParagraph } from "../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../components/document/DocRoot.js" +import { DocSection } from "../../../components/document/DocSection.js" +import { DocTip } from "../../../components/document/DocTip.js" + +export function FilesDashboardDocPage() { + return ( + + + + + + Chaque écriture comptable doit être + justifiée par une pièce (facture, relevé bancaire, ticket de caisse...). Arrhes vous permet de + stocker ces documents numériquement, directement associés à vos écritures, pour un archivage + sécurisé et une consultation rapide. + + + La numérisation de vos justificatifs facilite les contrôles, les échanges avec votre + expert-comptable et la recherche de documents en cas de besoin. + + + + + Arrhes accepte les formats de fichiers suivants pour les pièces jointes : + + + + + + Les pièces jointes sont associées directement aux écritures comptables. Vous pouvez les ajouter lors + de la saisie d'une écriture ou + ultérieurement. + + + + + + + + + Depuis une écriture, vous pouvez visualiser, télécharger ou supprimer les pièces jointes associées. + Les fichiers PDF et images peuvent être prévisualisés directement dans l'application. + + + + + + + + + + La conservation des pièces justificatives est une obligation légale. Arrhes vous aide à organiser vos + documents numériquement, mais pensez également à conserver vos originaux papier conformément à la + réglementation. + + + ) +} diff --git a/packages/website/src/features/docs/dashboard/GettingStartedDashboardDocPage.tsx b/packages/website/src/features/docs/dashboard/GettingStartedDashboardDocPage.tsx new file mode 100644 index 00000000..b87eb30d --- /dev/null +++ b/packages/website/src/features/docs/dashboard/GettingStartedDashboardDocPage.tsx @@ -0,0 +1,184 @@ +import { DocExample } from "../../../components/document/DocExample.js" +import { DocHeader } from "../../../components/document/DocHeader.js" +import { DocLink } from "../../../components/document/DocLink.js" +import { DocList } from "../../../components/document/DocList.js" +import { DocParagraph } from "../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../components/document/DocRoot.js" +import { DocSection } from "../../../components/document/DocSection.js" +import { DocTip } from "../../../components/document/DocTip.js" + +export function GettingStartedDashboardDocPage() { + return ( + + + + + + Pour commencer à utiliser Arrhes, vous devez d'abord créer un compte utilisateur. Rendez-vous sur la + page d'inscription et renseignez votre adresse email et un mot de passe. + + + Un email de confirmation vous sera envoyé. Cliquez sur le lien pour activer votre compte. + + + + + + Une fois connecté, vous arrivez sur la page d'accueil. Si c'est votre première connexion, vous serez + invité à créer une organisation. + + + + + + Vous pouvez gérer plusieurs organisations depuis le même compte. Pratique si vous gérez une + entreprise et une association par exemple ! + + + + + + Après avoir créé votre organisation, vous devez définir un exercice comptable. L'exercice correspond + généralement à l'année civile (du 1er janvier au 31 décembre), mais vous pouvez choisir d'autres + dates. + + + + + + Le logiciel créera automatiquement un plan comptable adapté à votre type d'organisation. Vous + pourrez le personnaliser par la suite si nécessaire. + + + + + + Arrhes fonctionne comme un éditeur de code ou un navigateur : chaque vue s'ouvre dans un{" "} + onglet. Vous pouvez ainsi garder plusieurs contextes ouverts simultanément sans + perdre votre place. + + + + + + + + + + + + + + + + Vous pouvez naviguer dans l'historique d'un onglet avec les raccourcis clavier{" "} + Alt+Flèche gauche et Alt+Flèche droite, ou via le menu contextuel. + + + + + + La palette de commandes est le moyen le plus rapide d'ouvrir n'importe quelle vue : organisations, + exercices, écritures, documents, paramètres… + + + + Appuyez sur Ctrl+K (ou ⌘+K sur Mac). + , + "Ou cliquez sur le bouton « Rechercher… » dans l'en-tête.", + "Ou cliquez sur le bouton « + » à droite de la barre d'onglets.", + ]} + /> + + + + + + + + + Arrhes permet d'afficher deux panneaux côte à côte pour comparer ou travailler sur deux vues en même + temps, par exemple consulter le journal pendant la saisie d'une écriture. + + + + + + + + + + + + Avant de commencer la saisie, assurez-vous de comprendre les bases de la comptabilité. Si vous + n'avez jamais fait de comptabilité, nous vous recommandons de lire notre{" "} + cours d'introduction. + + Les concepts clés à maîtriser sont : + + + + ) +} diff --git a/packages/website/src/features/docs/dashboard/OrganizationsDashboardDocPage.tsx b/packages/website/src/features/docs/dashboard/OrganizationsDashboardDocPage.tsx new file mode 100644 index 00000000..c21b910a --- /dev/null +++ b/packages/website/src/features/docs/dashboard/OrganizationsDashboardDocPage.tsx @@ -0,0 +1,89 @@ +import { DocDefinition } from "../../../components/document/DocDefinition.js" +import { DocExample } from "../../../components/document/DocExample.js" +import { DocHeader } from "../../../components/document/DocHeader.js" +import { DocLink } from "../../../components/document/DocLink.js" +import { DocList } from "../../../components/document/DocList.js" +import { DocParagraph } from "../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../components/document/DocRoot.js" +import { DocSection } from "../../../components/document/DocSection.js" + +export function OrganizationsDashboardDocPage() { + return ( + + + + + + Arrhes supporte deux types d'organisations, chacun avec un plan comptable adapté : + + + Plan comptable général (PCG) adapté aux sociétés commerciales. Inclut les comptes de TVA, de capital + social, etc. + + + Plan comptable des associations. Inclut les comptes spécifiques comme les cotisations (756), les + subventions (74), les fonds associatifs... + + + Le type d'organisation est défini à la création et ne peut pas être modifié ensuite. Si vous avez + fait une erreur, vous devrez créer une nouvelle organisation. + + + + + Chaque organisation possède des paramètres que vous pouvez modifier : + + + + + + + + + Vous pouvez inviter d'autres personnes à accéder à votre organisation. Chaque membre peut avoir des + droits différents. + + + Accès complet : peut modifier les paramètres, inviter des membres, supprimer l'organisation. + + + Accès limité : peut saisir des écritures{" "} + et consulter les documents de synthèse, + mais ne peut pas modifier les paramètres. + + + + + La personne recevra un email avec un lien pour rejoindre l'organisation. Si la personne ne + possède pas de compte associé à cette adresse email, elle devra créer un compte. + + + + + ) +} diff --git a/packages/website/src/features/docs/dashboard/ReportsDashboardDocPage.tsx b/packages/website/src/features/docs/dashboard/ReportsDashboardDocPage.tsx new file mode 100644 index 00000000..4c620ddb --- /dev/null +++ b/packages/website/src/features/docs/dashboard/ReportsDashboardDocPage.tsx @@ -0,0 +1,217 @@ +import { DocExample } from "../../../components/document/DocExample.js" +import { DocHeader } from "../../../components/document/DocHeader.js" +import { DocLink } from "../../../components/document/DocLink.js" +import { DocList } from "../../../components/document/DocList.js" +import { DocParagraph } from "../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../components/document/DocRoot.js" +import { DocSection } from "../../../components/document/DocSection.js" +import { DocTable } from "../../../components/document/DocTable.js" + +export function ReportsDashboardDocPage() { + return ( + + + + + + Arrhes vous permet de générer les principaux{" "} + documents comptables nécessaires à la + gestion de votre entreprise et à vos obligations légales. + + + + + + + Pour générer un rapport, accédez à votre exercice comptable puis cliquez sur l'onglet Rapports. + Sélectionnez ensuite le type de document souhaité. + + + + + + + + + Le grand livre présente le détail de tous les mouvements pour chaque compte. C'est le document de + référence pour vérifier l'historique d'un compte. + + + + + + + La balance présente la situation de chaque compte avec le total des débits, le total des crédits et + le solde. Elle permet de vérifier l'équilibre comptable. + + + + + + Vérification : le total des soldes débiteurs doit toujours être égal au total des + soldes créditeurs. + + + + + + Le bilan présente la situation patrimoniale de l'entreprise à une date donnée. Il est structuré en + deux parties équilibrées : + + + + Le bilan est généralement produit à la clôture de l'exercice pour les comptes annuels, mais peut + aussi être généré à tout moment pour un suivi intermédiaire. + + + + + + Le compte de résultat présente les produits et les charges de l'exercice, et calcule le résultat + (bénéfice ou perte). + + + + + + + + + Tous les rapports peuvent être exportés dans plusieurs formats pour être archivés ou transmis à + votre expert-comptable. + + + + + + + + + + La plupart des rapports peuvent être filtrés par période pour analyser une portion spécifique de + l'exercice (mois, trimestre...). + + + + + + + + + ) +} diff --git a/packages/website/src/features/docs/dashboard/RootDashboardDocPage.tsx b/packages/website/src/features/docs/dashboard/RootDashboardDocPage.tsx new file mode 100644 index 00000000..8c59440e --- /dev/null +++ b/packages/website/src/features/docs/dashboard/RootDashboardDocPage.tsx @@ -0,0 +1,76 @@ +import { ButtonOutlineContent, LinkContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconChevronRight } from "@tabler/icons-react" +import { DocHeader } from "../../../components/document/DocHeader.tsx" +import { DocLink } from "../../../components/document/DocLink.tsx" +import { DocRoot } from "../../../components/document/DocRoot.tsx" +import { DocTip } from "../../../components/document/DocTip.tsx" +import { LinkButton } from "../../../components/LinkButton.tsx" + +export function RootDashboardDocPage() { + return ( + + + + {/* About section */} +
+

+ Commencez par le guide de démarrage pour configurer votre première organisation et comprendre les + bases du logiciel. +

+
+ + } + /> + +
+
+ + + Vous avez trouvé un bug ? Vous souhaitez ajouter une fonctionnalité ?{" "} + + Ouvrez un ticket sur GitHub + {" "} + pour que nous puissions discuter de ça rapidement. + + + {/* Tip box */} + + Si vous n'avez jamais fait de comptabilité, nous vous recommandons de commencer par le{" "} + cours de comptabilité avant de consulter le guide + d'utilisation. Cela vous permettra de mieux comprendre les concepts utilisés dans le logiciel. + +
+ ) +} diff --git a/packages/website/src/features/docs/dashboard/UpdatesDashboardDocPage.tsx b/packages/website/src/features/docs/dashboard/UpdatesDashboardDocPage.tsx index cb42dcd3..3b751079 100644 --- a/packages/website/src/features/docs/dashboard/UpdatesDashboardDocPage.tsx +++ b/packages/website/src/features/docs/dashboard/UpdatesDashboardDocPage.tsx @@ -1,8 +1,8 @@ import { LinkContent } from "@arrhes/ui" -import { DocHeader } from "../../../components/document/docHeader.js" -import { DocParagraph } from "../../../components/document/docParagraph.js" -import { DocRoot } from "../../../components/document/docRoot.js" -import { DocSection } from "../../../components/document/docSection.js" +import { DocHeader } from "../../../components/document/DocHeader.js" +import { DocParagraph } from "../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../components/document/DocRoot.js" +import { DocSection } from "../../../components/document/DocSection.js" export function UpdatesDashboardDocPage() { return ( diff --git a/packages/website/src/features/docs/dashboard/YearsDashboardDocPage.tsx b/packages/website/src/features/docs/dashboard/YearsDashboardDocPage.tsx new file mode 100644 index 00000000..ee416399 --- /dev/null +++ b/packages/website/src/features/docs/dashboard/YearsDashboardDocPage.tsx @@ -0,0 +1,69 @@ +import { DocExample } from "../../../components/document/DocExample.js" +import { DocHeader } from "../../../components/document/DocHeader.js" +import { DocLink } from "../../../components/document/DocLink.js" +import { DocList } from "../../../components/document/DocList.js" +import { DocParagraph } from "../../../components/document/DocParagraph.js" +import { DocRoot } from "../../../components/document/DocRoot.js" +import { DocSection } from "../../../components/document/DocSection.js" + +export function YearsDashboardDocPage() { + return ( + + + + + + Chaque organisation contient un ou plusieurs exercices comptables. Un exercice représente une + période (généralement 12 mois) pendant laquelle vous enregistrez vos opérations. + + + Pour comprendre ce qu'est un exercice comptable, consultez la page sur les{" "} + principes fondamentaux. + + + + + + + Le plan comptable est la liste des comptes disponibles pour saisir vos écritures. Arrhes crée + automatiquement un plan comptable standard, mais vous pouvez le personnaliser. + + + + + Pour comprendre la numérotation des comptes, consultez la page sur les{" "} + comptes comptables. + + + + + + + Les journaux permettent de classer vos écritures par type d'opération. Des journaux par défaut sont + créés automatiquement (Achats, Ventes, Banque, Caisse, Opérations diverses). + + + Vous pouvez créer des journaux supplémentaires si nécessaire (ex : un journal par compte bancaire si + vous en avez plusieurs). + + + + ) +} diff --git a/packages/website/src/features/docs/dashboard/entriesDashboardDocPage.tsx b/packages/website/src/features/docs/dashboard/entriesDashboardDocPage.tsx deleted file mode 100644 index 2805eb09..00000000 --- a/packages/website/src/features/docs/dashboard/entriesDashboardDocPage.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" -import { DocExample } from "../../../components/document/docExample.js" -import { DocHeader } from "../../../components/document/docHeader.js" -import { DocLink } from "../../../components/document/docLink.js" -import { DocList } from "../../../components/document/docList.js" -import { DocNextPage } from "../../../components/document/docNextPage.js" -import { DocParagraph } from "../../../components/document/docParagraph.js" -import { DocRoot } from "../../../components/document/docRoot.js" -import { DocSection } from "../../../components/document/docSection.js" -import { DocTable } from "../../../components/document/docTable.js" - -export function EntriesDashboardDocPage() { - return ( - - - - - - Pour saisir une écriture, accédez à votre exercice comptable puis cliquez sur l'onglet Écritures. - Vous verrez la liste des écritures existantes et un bouton pour en ajouter une nouvelle. - - - - - Chaque écriture comptable nécessite les informations suivantes : - - - - - - - - - - Chaque ligne d'écriture correspond à un compte mouvementé. Vous devez indiquer : - - - - Rappel : comme expliqué dans le cours sur les{" "} - écritures comptables, le total des - débits doit toujours être égal au total des crédits. - - - -

- Vous payez une facture de 500 euros par virement bancaire : -

- -

- Explication : la dette fournisseur diminue (débit d'un compte de passif), la banque diminue - (crédit d'un compte d'actif). -

-
-
- - - - Chaque écriture devrait être justifiée par une pièce (facture, relevé bancaire, ticket...). Arrhes - vous permet de joindre des fichiers numériques à vos écritures. - - - - - - - - - - Vous pouvez modifier ou supprimer une écriture tant que l'exercice n'est pas clôturé. Pour cela, - ouvrez l'écriture et utilisez les boutons correspondants. - - - Attention : en comptabilité, il est généralement préférable de passer une écriture - de correction plutôt que de supprimer une écriture erronée. Cela permet de garder une trace de - l'erreur et de sa correction. - - - - - - La liste des écritures peut être filtrée et triée selon plusieurs critères : - - - Utilisez les filtres en haut de la liste pour affiner votre recherche. - - - - - - - -
- ) -} diff --git a/packages/website/src/features/docs/dashboard/filesDashboardDocPage.tsx b/packages/website/src/features/docs/dashboard/filesDashboardDocPage.tsx deleted file mode 100644 index d972f59e..00000000 --- a/packages/website/src/features/docs/dashboard/filesDashboardDocPage.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { DocExample } from "../../../components/document/docExample.js" -import { DocHeader } from "../../../components/document/docHeader.js" -import { DocLink } from "../../../components/document/docLink.js" -import { DocList } from "../../../components/document/docList.js" -import { DocNextPage } from "../../../components/document/docNextPage.js" -import { DocParagraph } from "../../../components/document/docParagraph.js" -import { DocRoot } from "../../../components/document/docRoot.js" -import { DocSection } from "../../../components/document/docSection.js" -import { DocTip } from "../../../components/document/docTip.js" - -export function FilesDashboardDocPage() { - return ( - - - - - - Chaque écriture comptable doit être - justifiée par une pièce (facture, relevé bancaire, ticket de caisse...). Arrhes vous permet de - stocker ces documents numériquement, directement associés à vos écritures, pour un archivage - sécurisé et une consultation rapide. - - - La numérisation de vos justificatifs facilite les contrôles, les échanges avec votre - expert-comptable et la recherche de documents en cas de besoin. - - - - - Arrhes accepte les formats de fichiers suivants pour les pièces jointes : - - - - - - Les pièces jointes sont associées directement aux écritures comptables. Vous pouvez les ajouter lors - de la saisie d'une écriture ou - ultérieurement. - - - - - - - - - Depuis une écriture, vous pouvez visualiser, télécharger ou supprimer les pièces jointes associées. - Les fichiers PDF et images peuvent être prévisualisés directement dans l'application. - - - - - - - - - - La conservation des pièces justificatives est une obligation légale. Arrhes vous aide à organiser vos - documents numériquement, mais pensez également à conserver vos originaux papier conformément à la - réglementation. - - - - - ) -} diff --git a/packages/website/src/features/docs/dashboard/gettingStartedDashboardDocPage.tsx b/packages/website/src/features/docs/dashboard/gettingStartedDashboardDocPage.tsx deleted file mode 100644 index 5c579755..00000000 --- a/packages/website/src/features/docs/dashboard/gettingStartedDashboardDocPage.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { DocExample } from "../../../components/document/docExample.js" -import { DocHeader } from "../../../components/document/docHeader.js" -import { DocLink } from "../../../components/document/docLink.js" -import { DocList } from "../../../components/document/docList.js" -import { DocNextPage } from "../../../components/document/docNextPage.js" -import { DocParagraph } from "../../../components/document/docParagraph.js" -import { DocRoot } from "../../../components/document/docRoot.js" -import { DocSection } from "../../../components/document/docSection.js" - -export function GettingStartedDashboardDocPage() { - return ( - - - - - - Pour commencer à utiliser Arrhes, vous devez d'abord créer un compte utilisateur. Rendez-vous sur la - page d'inscription et renseignez votre adresse email et un mot de passe. - - - Un email de confirmation vous sera envoyé. Cliquez sur le lien pour activer votre compte. - - - - - - Une fois connecté, vous arrivez sur la page d'accueil. Si c'est votre première connexion, vous serez - invité à créer une organisation. - - - - - - Vous pouvez gérer plusieurs organisations depuis le même compte. Pratique si vous gérez une - entreprise et une association par exemple ! - - - - - - Après avoir créé votre organisation, vous devez définir un exercice comptable. L'exercice correspond - généralement à l'année civile (du 1er janvier au 31 décembre), mais vous pouvez choisir d'autres - dates. - - - - - - Le logiciel créera automatiquement un plan comptable adapté à votre type d'organisation. Vous - pourrez le personnaliser par la suite si nécessaire. - - - - - L'interface d'Arrhes est organisée autour de plusieurs sections : - - - La navigation se fait via le menu upérieur. Chaque section est accessible en un clic depuis - n'importe quelle page. - - - - - - Avant de commencer la saisie, assurez-vous de comprendre les bases de la comptabilité. Si vous - n'avez jamais fait de comptabilité, nous vous recommandons de lire notre{" "} - cours d'introduction. - - Les concepts clés à maîtriser sont : - - - - - - ) -} diff --git a/packages/website/src/features/docs/dashboard/organizationsDashboardDocPage.tsx b/packages/website/src/features/docs/dashboard/organizationsDashboardDocPage.tsx deleted file mode 100644 index f6d3e398..00000000 --- a/packages/website/src/features/docs/dashboard/organizationsDashboardDocPage.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { DocDefinition } from "../../../components/document/docDefinition.js" -import { DocExample } from "../../../components/document/docExample.js" -import { DocHeader } from "../../../components/document/docHeader.js" -import { DocLink } from "../../../components/document/docLink.js" -import { DocList } from "../../../components/document/docList.js" -import { DocNextPage } from "../../../components/document/docNextPage.js" -import { DocParagraph } from "../../../components/document/docParagraph.js" -import { DocRoot } from "../../../components/document/docRoot.js" -import { DocSection } from "../../../components/document/docSection.js" - -export function OrganizationsDashboardDocPage() { - return ( - - - - - - Arrhes supporte deux types d'organisations, chacun avec un plan comptable adapté : - - - Plan comptable général (PCG) adapté aux sociétés commerciales. Inclut les comptes de TVA, de capital - social, etc. - - - Plan comptable des associations. Inclut les comptes spécifiques comme les cotisations (756), les - subventions (74), les fonds associatifs... - - - Le type d'organisation est défini à la création et ne peut pas être modifié ensuite. Si vous avez - fait une erreur, vous devrez créer une nouvelle organisation. - - - - - Chaque organisation possède des paramètres que vous pouvez modifier : - - - - - - - - - Vous pouvez inviter d'autres personnes à accéder à votre organisation. Chaque membre peut avoir des - droits différents. - - - Accès complet : peut modifier les paramètres, inviter des membres, supprimer l'organisation. - - - Accès limité : peut saisir des écritures{" "} - et consulter les documents de synthèse, - mais ne peut pas modifier les paramètres. - - - - - La personne recevra un email avec un lien pour rejoindre l'organisation. Si la personne ne - possède pas de compte associé à cette adresse email, elle devra créer un compte. - - - - - - - ) -} diff --git a/packages/website/src/features/docs/dashboard/reportsDashboardDocPage.tsx b/packages/website/src/features/docs/dashboard/reportsDashboardDocPage.tsx deleted file mode 100644 index f17d0549..00000000 --- a/packages/website/src/features/docs/dashboard/reportsDashboardDocPage.tsx +++ /dev/null @@ -1,217 +0,0 @@ -import { DocExample } from "../../../components/document/docExample.js" -import { DocHeader } from "../../../components/document/docHeader.js" -import { DocLink } from "../../../components/document/docLink.js" -import { DocList } from "../../../components/document/docList.js" -import { DocParagraph } from "../../../components/document/docParagraph.js" -import { DocRoot } from "../../../components/document/docRoot.js" -import { DocSection } from "../../../components/document/docSection.js" -import { DocTable } from "../../../components/document/docTable.js" - -export function ReportsDashboardDocPage() { - return ( - - - - - - Arrhes vous permet de générer les principaux{" "} - documents comptables nécessaires à la - gestion de votre entreprise et à vos obligations légales. - - - - - - - Pour générer un rapport, accédez à votre exercice comptable puis cliquez sur l'onglet Rapports. - Sélectionnez ensuite le type de document souhaité. - - - - - - - - - Le grand livre présente le détail de tous les mouvements pour chaque compte. C'est le document de - référence pour vérifier l'historique d'un compte. - - - - - - - La balance présente la situation de chaque compte avec le total des débits, le total des crédits et - le solde. Elle permet de vérifier l'équilibre comptable. - - - - - - Vérification : le total des soldes débiteurs doit toujours être égal au total des - soldes créditeurs. - - - - - - Le bilan présente la situation patrimoniale de l'entreprise à une date donnée. Il est structuré en - deux parties équilibrées : - - - - Le bilan est généralement produit à la clôture de l'exercice pour les comptes annuels, mais peut - aussi être généré à tout moment pour un suivi intermédiaire. - - - - - - Le compte de résultat présente les produits et les charges de l'exercice, et calcule le résultat - (bénéfice ou perte). - - - - - - - - - Tous les rapports peuvent être exportés dans plusieurs formats pour être archivés ou transmis à - votre expert-comptable. - - - - - - - - - - La plupart des rapports peuvent être filtrés par période pour analyser une portion spécifique de - l'exercice (mois, trimestre...). - - - - - - - - - ) -} diff --git a/packages/website/src/features/docs/dashboard/rootDashboardDocPage.tsx b/packages/website/src/features/docs/dashboard/rootDashboardDocPage.tsx deleted file mode 100644 index 8b356079..00000000 --- a/packages/website/src/features/docs/dashboard/rootDashboardDocPage.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { ButtonOutlineContent, LinkContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconChevronRight } from "@tabler/icons-react" -import { DocHeader } from "../../../components/document/docHeader.tsx" -import { DocLink } from "../../../components/document/docLink.tsx" -import { DocRoot } from "../../../components/document/docRoot.tsx" -import { DocTip } from "../../../components/document/docTip.tsx" -import { LinkButton } from "../../../components/linkButton.tsx" - -export function RootDashboardDocPage() { - return ( - - - - {/* About section */} -
-

- Commencez par le guide de démarrage pour configurer votre première organisation et comprendre les - bases du logiciel. -

-
- - } - /> - -
-
- - - Vous avez trouvé un bug ? Vous souhaitez ajouter une fonctionnalité ?{" "} - - Ouvrez un ticket sur GitHub - {" "} - pour que nous puissions discuter de ça rapidement. - - - {/* Tip box */} - - Si vous n'avez jamais fait de comptabilité, nous vous recommandons de commencer par le{" "} - cours de comptabilité avant de consulter le guide - d'utilisation. Cela vous permettra de mieux comprendre les concepts utilisés dans le logiciel. - -
- ) -} diff --git a/packages/website/src/features/docs/dashboard/yearsDashboardDocPage.tsx b/packages/website/src/features/docs/dashboard/yearsDashboardDocPage.tsx deleted file mode 100644 index dba1838d..00000000 --- a/packages/website/src/features/docs/dashboard/yearsDashboardDocPage.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { DocExample } from "../../../components/document/docExample.js" -import { DocHeader } from "../../../components/document/docHeader.js" -import { DocLink } from "../../../components/document/docLink.js" -import { DocList } from "../../../components/document/docList.js" -import { DocNextPage } from "../../../components/document/docNextPage.js" -import { DocParagraph } from "../../../components/document/docParagraph.js" -import { DocRoot } from "../../../components/document/docRoot.js" -import { DocSection } from "../../../components/document/docSection.js" - -export function YearsDashboardDocPage() { - return ( - - - - - - Chaque organisation contient un ou plusieurs exercices comptables. Un exercice représente une - période (généralement 12 mois) pendant laquelle vous enregistrez vos opérations. - - - Pour comprendre ce qu'est un exercice comptable, consultez la page sur les{" "} - principes fondamentaux. - - - - - - - Le plan comptable est la liste des comptes disponibles pour saisir vos écritures. Arrhes crée - automatiquement un plan comptable standard, mais vous pouvez le personnaliser. - - - - - Pour comprendre la numérotation des comptes, consultez la page sur les{" "} - comptes comptables. - - - - - - - Les journaux permettent de classer vos écritures par type d'opération. Des journaux par défaut sont - créés automatiquement (Achats, Ventes, Banque, Caisse, Opérations diverses). - - - Vous pouvez créer des journaux supplémentaires si nécessaire (ex : un journal par compte bancaire si - vous en avez plusieurs). - - - - - - ) -} diff --git a/packages/website/src/features/docs/docSections.tsx b/packages/website/src/features/docs/docSections.tsx index 0c4703bc..187fd252 100644 --- a/packages/website/src/features/docs/docSections.tsx +++ b/packages/website/src/features/docs/docSections.tsx @@ -9,8 +9,9 @@ import { IconReport, IconRobot, IconStack, + IconTerminal, } from "@tabler/icons-react" -import type { DocSection } from "./sectionTab.tsx" +import type { DocSection } from "./SectionTab.tsx" // Documentation sections configuration export const docSections: Record = { @@ -32,6 +33,10 @@ export const docSections: Record = { path: "/documentation/fonctionnalités", label: "Fonctionnalités", }, + { + path: "/documentation/architecture", + label: "Architecture", + }, { path: "/documentation/philosophie", label: "Philosophie", @@ -257,14 +262,20 @@ export const docSections: Record = { path: "/documentation/api", icon: , navigation: { - api: { - title: "API", - icon: , + home: { + title: undefined, + icon: undefined, items: [ { path: "/documentation/api", label: "Présentation", }, + ], + }, + basicsGroup: { + title: "Généralités", + icon: , + items: [ { path: "/documentation/api/introduction", label: "Introduction", @@ -273,17 +284,159 @@ export const docSections: Record = { path: "/documentation/api/authentification", label: "Authentification", }, + ], + }, + organisationGroup: { + title: "Organisation", + icon: , + items: [ { path: "/documentation/api/organisation", label: "Organisation", }, { - path: "/documentation/api/exercice", - label: "Exercice", + path: "/documentation/api/membres", + label: "Membres", + }, + { + path: "/documentation/api/clés-api", + label: "Clés API", }, { path: "/documentation/api/stockage", - label: "Fichiers et documents", + label: "Stockage", + }, + ], + }, + exerciceGroup: { + title: "Exercice", + icon: , + items: [ + { + path: "/documentation/api/exercice", + label: "Exercices", + }, + { + path: "/documentation/api/comptes", + label: "Comptes", + }, + { + path: "/documentation/api/journaux", + label: "Journaux", + }, + { + path: "/documentation/api/bilans", + label: "Bilans", + }, + { + path: "/documentation/api/comptes-de-résultat", + label: "Comptes de résultat", + }, + { + path: "/documentation/api/libellés", + label: "Libellés", + }, + { + path: "/documentation/api/écritures", + label: "Écritures", + }, + { + path: "/documentation/api/exports", + label: "Exports", + }, + ], + }, + }, + }, + cli: { + id: "cli", + label: "CLI", + path: "/documentation/cli", + icon: , + navigation: { + home: { + items: [ + { + path: "/documentation/cli", + label: "Présentation", + }, + ], + }, + guide: { + title: "Guide", + icon: , + items: [ + { + path: "/documentation/cli/installation", + label: "Installation", + }, + { + path: "/documentation/cli/demarrer", + label: "Démarrer", + }, + { + path: "/documentation/cli/authentification", + label: "Authentification", + }, + ], + }, + organisationGroup: { + title: "Organisation", + icon: , + items: [ + { + path: "/documentation/cli/commandes/organisation", + label: "Organisation", + }, + { + path: "/documentation/cli/commandes/membres", + label: "Membres", + }, + { + path: "/documentation/cli/commandes/cles-api", + label: "Clés API", + }, + { + path: "/documentation/cli/commandes/stockage", + label: "Stockage", + }, + ], + }, + exerciceGroup: { + title: "Exercice", + icon: , + items: [ + { + path: "/documentation/cli/commandes/exercices", + label: "Exercices", + }, + { + path: "/documentation/cli/commandes/journaux", + label: "Journaux", + }, + { + path: "/documentation/cli/commandes/comptes", + label: "Comptes", + }, + { + path: "/documentation/cli/commandes/libelles", + label: "Libellés", + }, + { + path: "/documentation/cli/commandes/ecritures", + label: "Écritures", + }, + { + path: "/documentation/cli/commandes/exports", + label: "Exports", + }, + { + path: "/documentation/cli/commandes/bilans", + label: "Bilans", + }, + { + path: "/documentation/cli/commandes/comptes-de-resultat", + label: "Comptes de résultat", }, ], }, diff --git a/packages/website/src/features/docs/docsLayout.tsx b/packages/website/src/features/docs/docsLayout.tsx deleted file mode 100644 index 844a1fb9..00000000 --- a/packages/website/src/features/docs/docsLayout.tsx +++ /dev/null @@ -1,303 +0,0 @@ -import { Button, ButtonGhostContent, ButtonPlainContent, Logo } from "@arrhes/ui" -import { css, cx } from "@arrhes/ui/utilities/cn.js" -import { IconBook2, IconBrandGithub, IconMenu } from "@tabler/icons-react" -import { Outlet, useRouterState } from "@tanstack/react-router" -import { useState } from "react" -import { LinkButton } from "../../components/linkButton.js" -import { docSections } from "./docSections.js" -import { DocsSearch } from "./docsSearch.js" -import { SectionTab } from "./sectionTab.js" -import { SidebarNavigation } from "./sidebarNavigation.js" - -type DocSectionId = keyof typeof docSections - -function getCurrentSection(pathname: string): DocSectionId { - if (pathname.startsWith("/documentation/comptabilité")) return "comptabilite" - if (pathname.startsWith("/documentation/dashboard")) return "dashboard" - if (pathname.startsWith("/documentation/api")) return "api" - return "general" -} - -export function DocsLayout() { - const pathname = useRouterState({ - select: (s) => s.location.pathname, - }) - const [isMenuOpen, setIsMenuOpen] = useState(false) - - const currentSectionId = getCurrentSection(pathname) - const currentSection = docSections[currentSectionId] - - return ( -
- {/* Header */} -
- {/* Top bar */} -
-
-
- - } - text="Arrhes" - /> - - - - / - - - - } - text="Documentation" - /> - -
- - - - -
-
- - {/* Section tabs */} -
-
- {Object.values(docSections).map((section) => ( - - ))} -
-
-
- -
-
- {/* Sidebar */} - - - {/* Main content */} -
-
- - {isMenuOpen === false ? null : ( - - )} -
-
- -
-
-
-
-
- ) -} diff --git a/packages/website/src/features/docs/general/LegalGeneralDocPage.tsx b/packages/website/src/features/docs/general/LegalGeneralDocPage.tsx new file mode 100644 index 00000000..dc38d8c4 --- /dev/null +++ b/packages/website/src/features/docs/general/LegalGeneralDocPage.tsx @@ -0,0 +1,135 @@ +import { LinkContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { DocLastUpdate } from "../../../components/document/DocLastUpdate.js" +import { DocRoot } from "../../../components/document/DocRoot.js" +import { DocTextSection } from "../../../components/document/DocTextSection.js" +import { DocTip } from "../../../components/document/DocTip.js" + +export function LegalGeneralDocPage() { + return ( + + {/* Page header */} +
+

+ Mentions légales +

+

+ Informations légales relatives à l'utilisation du site et du service Arrhes. +

+
+ + {/* Content */} +
+ +

+ Le site arrhes.com est édité par Barbote SAS, société par actions simplifiée au capital de + 1000.00 euros, immatriculée au Registre du Commerce et des Sociétés de Paris sous le numéro 908 + 719 503. +

+

+ Siège social : 93 rue Sedaine, 75011 Paris +
+ Numéro de TVA intracommunautaire : FR02 908 719 503 +
+ Directeur de la publication : Emile Sabatier +
+ Contact :{" "} + + contact@arrhes.com + +

+
+ + +

+ Le site est hébergé par la société :
+ OVH +
+ RCS Lille Métropole, 424 761 419 00045 +
2 rue Kellermann - 59100 Roubaix - France +

+
+ + +

+ L'ensemble du contenu de ce site (textes, images, vidéos, logos, icônes, etc.) est protégé par + les lois relatives à la propriété intellectuelle. +

+

+ Le code source d'Arrhes est distribué sous licence{" "} + + AGPL-3.0 + + . Consultez notre dépôt GitHub pour plus d'informations. +

+
+ + +

+ Les informations relatives au traitement de vos données personnelles sont détaillées dans notre{" "} + + Politique de confidentialité + + . +

+
+ + +

+ Ce site utilise des cookies strictement nécessaires au fonctionnement du service. Aucun cookie + publicitaire ou de traçage n'est utilisé. +

+
+ + +

+ Arrhes s'efforce de fournir des informations exactes et à jour sur ce site. Toutefois, nous ne + pouvons garantir l'exactitude, la complétude ou l'actualité des informations diffusées. +

+

+ L'utilisateur est seul responsable de l'utilisation qu'il fait des informations et + fonctionnalités disponibles sur ce site. +

+
+ + Oui, pour notre comptabilité, nous utilisons Arrhes. ;) + + +
+
+ ) +} diff --git a/packages/website/src/features/docs/general/PrivacyGeneralDocPage.tsx b/packages/website/src/features/docs/general/PrivacyGeneralDocPage.tsx new file mode 100644 index 00000000..480732ab --- /dev/null +++ b/packages/website/src/features/docs/general/PrivacyGeneralDocPage.tsx @@ -0,0 +1,192 @@ +import { LinkContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { DocLastUpdate } from "../../../components/document/DocLastUpdate.js" +import { DocRoot } from "../../../components/document/DocRoot.js" +import { DocTextSection } from "../../../components/document/DocTextSection.js" +import { LinkButton } from "../../../components/LinkButton.js" + +export function PrivacyGeneralDocPage() { + return ( + + {/* Page header */} +
+

+ Politique de confidentialité +

+

+ Comment nous collectons, utilisons et protégeons vos données personnelles. +

+
+ + {/* Content */} +
+ +

+ Le responsable du traitement des données personnelles est la même société que celle qui + mentionnée dans les{" "} + + mentions légales + + . +

+

+ Pour toute question relative à vos données personnelles, vous pouvez nous contacter à l'adresse + :{" "} + + support@arrhes.com + +

+
+ + +

Nous collectons les données suivantes :

+
    +
  • + Données d'identification : nom, prénom, adresse email +
  • +
  • + Données de connexion : adresse IP, logs de connexion +
  • +
  • + Données comptables : écritures, comptes, documents que vous saisissez +
  • +
  • + Données d'usage : fonctionnalités utilisées, préférences +
  • +
+
+ + +

Vos données sont utilisées pour :

+
    +
  • Fournir et améliorer le service
  • +
  • Gérer votre compte utilisateur
  • +
  • Assurer la sécurité du service
  • +
  • Vous contacter pour des informations relatives au service
  • +
  • Respecter nos obligations légales
  • +
+
+ + +

Le traitement de vos données repose sur les bases légales suivantes :

+
    +
  • + Exécution du contrat : pour fournir le service +
  • +
  • + Intérêt légitime : pour améliorer nos services et assurer la sécurité +
  • +
  • + Consentement : pour les communications (optionnel) +
  • +
  • + Obligation légale : pour la conservation des données liées au paiement +
  • +
+
+ + +

Vos données sont conservées pendant la durée de votre utilisation du service, puis :

+
    +
  • Données de compte : supprimées à la clôture du compte
  • +
  • Données de paiement : conservées 10 ans conformément à la loi
  • +
  • Logs de connexion : conservés 1 an
  • +
+
+ + +

Vos données peuvent être partagées avec :

+
    +
  • Nos sous-traitants techniques (hébergement, email)
  • +
  • Les autorités en cas d'obligation légale
  • +
+

Nous ne fournissons jamais vos données, à quelque titre que ce soit, à des tiers.

+
+ + +

+ Vos données sont hébergées en France et ne font l'objet d'aucun transfert en dehors de l'Union + Européenne. +

+
+ + +

+ Nous mettons en oeuvre des mesures techniques et organisationnelles pour protéger vos données : +

+
    +
  • Chiffrement des données en transit et au repos
  • + {/*
  • Authentification sécurisée avec option 2FA
  • */} +
  • Sauvegardes régulières
  • +
  • Accès limité au personnel autorisé
  • +
+
+ + +

Conformément au RGPD, vous disposez des droits suivants :

+
    +
  • + Droit d'accès : obtenir une copie de vos données +
  • +
  • + Droit de rectification : corriger vos données +
  • +
  • + Droit à l'effacement : supprimer vos données +
  • +
  • + Droit à la portabilité : exporter vos données +
  • +
  • + Droit d'opposition : vous opposer à certains traitements +
  • +
  • + Droit de limitation : limiter le traitement +
  • +
+

+ Pour exercer ces droits, contactez-nous à{" "} + + support@arrhes.com + + . +

+
+ + +

+ Si vous estimez que le traitement de vos données ne respecte pas la réglementation, nous pouvons + en discuter ensemble. Dans le cas échéant, vous pouvez introduire une réclamation auprès de la + CNIL (Commission Nationale de l'Informatique et des Libertés). +

+
+ + +
+
+ ) +} diff --git a/packages/website/src/features/docs/general/RootGeneralDocPage.tsx b/packages/website/src/features/docs/general/RootGeneralDocPage.tsx new file mode 100644 index 00000000..089eac0b --- /dev/null +++ b/packages/website/src/features/docs/general/RootGeneralDocPage.tsx @@ -0,0 +1,56 @@ +import { ButtonOutlineContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconChevronRight } from "@tabler/icons-react" +import { DocHeader } from "../../../components/document/DocHeader.js" +import { DocRoot } from "../../../components/document/DocRoot.js" +import { LinkButton } from "../../../components/LinkButton.js" + +export function RootGeneralDocPage() { + return ( + + + + {/* About section */} +
+

+ Arrhes est un logiciel de comptabilité open source conçu pour tous. Accessible, moderne et + respectueux des normes comptables françaises, il vous permet de gérer vos écritures comptables, vos + documents, générer vos documents fiscaux et collaborer avec votre équipe. +

+
+ + } + /> + +
+
+
+ ) +} diff --git a/packages/website/src/features/docs/general/SupportGeneralDocPage.tsx b/packages/website/src/features/docs/general/SupportGeneralDocPage.tsx new file mode 100644 index 00000000..c4ce6c2b --- /dev/null +++ b/packages/website/src/features/docs/general/SupportGeneralDocPage.tsx @@ -0,0 +1,404 @@ +import { ButtonOutlineContent, LinkContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconBrandGithub, IconBrandLinkedin, IconExternalLink, IconMail } from "@tabler/icons-react" +import { DocRoot } from "../../../components/document/DocRoot.js" +import { DocTip } from "../../../components/document/DocTip.js" +import { LinkButton } from "../../../components/LinkButton.js" + +export function SupportGeneralDocPage() { + return ( + + {/* Page header */} +
+

+ Support +

+

+ Besoin d'aide ? Nous sommes là pour vous accompagner. +

+
+ + {/* Contact cards */} +
+ {/* Email support */} +
+
+
+ +
+

+ Email +

+
+

+ Contactez notre équipe par email pour toute question ou demande d'assistance. +

+ + support@arrhes.com + +
+ + {/* GitHub */} +
+
+
+ +
+

+ GitHub +

+
+

+ Signalez un bug, proposez une amélioration ou consultez le code source. +

+ + } + text="Voir sur GitHub" + /> + +
+ + {/* LinkedIn */} +
+
+
+ +
+

+ LinkedIn +

+
+

+ Suivez-nous sur LinkedIn pour les dernières nouvelles. +

+ + } + text="Voir sur LinkedIn" + /> + +
+
+ + {/* Response time info */} + + Nous nous efforçons de répondre à toutes les demandes dans un délai raisonnable. + + + {/* Documentation section */} +
+

+ Ressources utiles +

+ +
+ +
+

+ Cours de comptabilité +

+

+ Les bases de la comptabilité expliquées simplement +

+
+
+ + +
+

+ Guide d'utilisation du dashboard +

+

+ Apprenez à utiliser l'interface web efficacement +

+
+
+ + +
+

+ Guide d'utilisation de l'API +

+

+ Apprenez à utiliser l'API +

+
+
+
+
+
+ ) +} diff --git a/packages/website/src/features/docs/general/TermsGeneralDocPage.tsx b/packages/website/src/features/docs/general/TermsGeneralDocPage.tsx new file mode 100644 index 00000000..2f51cf5b --- /dev/null +++ b/packages/website/src/features/docs/general/TermsGeneralDocPage.tsx @@ -0,0 +1,160 @@ +import { LinkContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { DocLastUpdate } from "../../../components/document/DocLastUpdate.js" +import { DocRoot } from "../../../components/document/DocRoot.js" +import { DocTextSection } from "../../../components/document/DocTextSection.js" + +export function TermsGeneralDocPage() { + return ( + + {/* Page header */} +
+

+ Conditions Générales d'Utilisation +

+

+ Conditions régissant l'utilisation du service Arrhes. +

+
+ + {/* Content */} +
+ +

+ Les présentes Conditions Générales d'Utilisation (CGU) ont pour objet de définir les modalités + et conditions d'utilisation du service Arrhes, accessible à l'adresse arrhes.com. +

+

En utilisant le service, l'utilisateur accepte sans réserve les présentes CGU.

+
+ + +

+ Arrhes est un logiciel de comptabilité en ligne permettant aux entreprises et associations de + gérer leur comptabilité, saisir des écritures, générer des documents comptables et collaborer en + équipe. +

+

+ Le service est proposé en version basique (gratuite) avec les fonctionnalités de base, et en + version avancée (payante) avec des fonctionnalités étendues. +

+
+ + +

+ L'utilisation du service nécessite la création d'un compte utilisateur. L'utilisateur s'engage à + fournir des informations exactes et à jour lors de son inscription. +

+

+ L'utilisateur est responsable de la confidentialité de ses identifiants de connexion et de + toutes les activités effectuées depuis son compte. +

+
+ + +

L'utilisateur s'engage à :

+
    +
  • Utiliser le service conformément à sa destination
  • +
  • Ne pas utiliser le service à des fins illégales ou frauduleuses
  • +
  • Ne pas tenter de compromettre la sécurité du service
  • +
  • Respecter les droits de propriété intellectuelle
  • +
  • Ne pas collecter de données d'autres utilisateurs sans autorisation
  • +
+
+ + +

+ L'utilisateur est seul responsable des données qu'il saisit dans le service. Arrhes ne peut être + tenu responsable des erreurs comptables résultant d'une mauvaise utilisation du service. +

+

+ L'utilisateur est encouragé à vérifier ses données et à consulter un professionnel comptable en + cas de doute. +

+
+ + +

+ Le code source d'Arrhes est distribué sous licence{" "} + + AGPL-3.0 + + . Les conditions spécifiques de cette licence sont disponibles sur notre dépôt GitHub. +

+

Les marques, logos et contenus graphiques restent la propriété de l'entreprise.

+
+ + +

+ Le traitement des données personnelles est décrit dans notre{" "} + + Politique de confidentialité + + . +

+
+ + +

+ L'utilisateur peut supprimer son compte à tout moment depuis les paramètres de son profil. La + suppression entraîne l'effacement de toutes les données associées au compte, sauf celle + nécessaires, i.e. les données légales. +

+

+ Arrhes se réserve le droit de suspendre ou supprimer un compte en cas de violation des présentes + CGU. +

+
+ + +

+ Arrhes se réserve le droit de modifier les présentes CGU à tout moment. Les utilisateurs seront + informés de toute modification substantielle par email ou notification dans l'application. +

+
+ + +

+ Les présentes CGU sont soumises au droit français. En cas de litige, les tribunaux de Paris + seront seuls compétents. +

+
+ + +
+
+ ) +} diff --git a/packages/website/src/features/docs/general/WhitepaperGeneralDocPage.tsx b/packages/website/src/features/docs/general/WhitepaperGeneralDocPage.tsx new file mode 100644 index 00000000..0ed7172c --- /dev/null +++ b/packages/website/src/features/docs/general/WhitepaperGeneralDocPage.tsx @@ -0,0 +1,114 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import { DocLastUpdate } from "../../../components/document/DocLastUpdate.js" +import { DocRoot } from "../../../components/document/DocRoot.js" +import { DocTextSection } from "../../../components/document/DocTextSection.js" + +export function WhitepaperGeneralDocPage() { + return ( + + {/* Page header */} +
+

+ Notre philosophie +

+

+ Nous souhaitons une transparence totale avec notre communauté. C'est pourquoi nous avons rédigé ces + quelques lignes. +

+
+ + {/* Content */} +
+ +

+ Nous voulons que la comptabilité devienne accessible, compréhensible et même agréable. Nous + construisons Arrhes pour tout le monde : autant ceux qui veulent faire leur comptabilité + simplement que les experts du domaine. +

+

+ À terme, nous voulons qu'Arrhes devienne la référence open source de la comptabilité en France - + un outil moderne, transparent et communautaire, soutenu par un modèle économique sain. Un outil + dont la communauté est fière, parce qu'elle contribue à le rendre meilleur chaque jour. +

+

+ L'intelligence artificielle est au coeur de cette vision. Pas comme un gadget marketing, mais + comme un véritable assistant qui peut vous accompagner, suivre des instructions, détecter les + anomalies, et vous aider à prendre des décisions. L'IA doit rendre la comptabilité plus + accessible. +

+
+ + +

+ Arrhes est un logiciel open source parce que nous croyons que la comptabilité ne devrait pas + être une boîte noire. Quand il s'agit de vos finances, vous devriez pouvoir vérifier exactement + ce que fait votre outil, comment il calcule vos soldes, et où sont stockées vos données. Mieux + comprendre sa comptabilité, c'est mieux comprendre son activité. +

+

+ Mais au-delà de la transparence, l'open source est un choix communautaire. Un logiciel de + comptabilité utilisé par des associations, des indépendants et des entreprises a tout intérêt à + être construit avec eux. Les retours, les contributions et les idées de la communauté rendent le + produit meilleur pour tout le monde. Chaque utilisateur peut signaler un bug, proposer une + amélioration ou adapter l'outil à ses besoins. +

+

+ Nous pensons aussi qu'à l'ère de l'intelligence artificielle, l'open source a plus de sens que + jamais. Les modèles IA sont entraînés sur du code et savent le restituer. Nous pensons que la + valeur ajoutée ne réside plus dans le code, mais plutôt dans l'architecture pensée par et pour + les utilisateurs. +

+
+ + +

+ Notre approche est simple : le coeur du logiciel est gratuit, et nous faisons payer uniquement + ce qui nous coûte cher à fournir. +

+

+ Toute la comptabilité - saisie des écritures, plan comptable, bilan, compte de résultat, gestion + des pièces jointes - est accessible gratuitement. Nous ne croyons pas aux limitations + artificielles qui frustrent les utilisateurs pour les pousser à payer. +

+

+ Ce qui est payant, ce sont les fonctionnalités qui ont un coût réel pour nous : le stockage, + l'assistant IA (qui consomme des ressources de calcul), l'hébergement cloud géré (serveurs, + sauvegardes, maintenance), et les intégrations avancées. En résumé, vous payez pour + l'infrastructure, pas pour le logiciel lui-même. +

+

+ Ce modèle nous semble juste. Les revenus générés financent l'ensemble du projet : le + développement des fonctionnalités, la maintenance, la documentation et le support communautaire. + Chaque licence soutient l'outil pour tout le monde. +

+
+ + +
+
+ ) +} diff --git a/packages/website/src/features/docs/general/architecture/ArchitectureDiagram.tsx b/packages/website/src/features/docs/general/architecture/ArchitectureDiagram.tsx new file mode 100644 index 00000000..0b5220e8 --- /dev/null +++ b/packages/website/src/features/docs/general/architecture/ArchitectureDiagram.tsx @@ -0,0 +1,279 @@ +import { css } from "@arrhes/ui/utilities/cn.js" + +export function ArchitectureDiagram() { + return ( +
+ + + + + + + + {/* ── Layout constants + Total drawable width: x=15..605 (590px) + 1/3 unit = 190px, gap = 10px + Row 1: Utilisateurs(x=15,w=190,cx=110) | Développeurs(x=215,w=390,cx=410) + Row 2: Dashboard(x=15,w=190,cx=110) | [gap 210px] | CLI(x=415,w=190,cx=510) + Row 3: API (x=15, w=590, h=36) + Row 4: Database (x=15, w=590, h=36) + ── */} + + {/* ── Connector lines (drawn first, behind boxes) ───────── */} + + {/* Utilisateurs → Dashboard */} + + {/* Développeurs → API (straight down through the gap in row 2) */} + + {/* Développeurs → CLI */} + + {/* Dashboard → API */} + + {/* CLI → API */} + + {/* API → Database */} + + + {/* ── Row 1 : 1/3 Utilisateurs + 2/3 Développeurs ──────── */} + + {/* Utilisateurs — 1/3 (x=15, w=190, cx=110) */} + + + Utilisateurs + + + Utilisation simple + + + {/* Développeurs — 2/3 (x=215, w=390, cx=410) */} + + + Développeurs + + + Intégrations · Automatisations · IA + + + {/* ── Row 2 : 1/3 Dashboard + gap + 1/3 CLI ────────────── */} + + {/* Dashboard — 1/3 (x=15, w=190, cx=110) */} + + + Dashboard + + + {/* CLI — 1/3 (x=415, w=190, cx=510) */} + + + CLI + + + {/* ── Row 3 : API full width (h=36) ────────────────────── */} + + + API + + + {/* ── Row 4 : Database full width (h=36, same as API) ───── */} + + + Database + + +
+ ) +} diff --git a/packages/website/src/features/docs/general/architecture/ArchitectureGeneralDocPage.tsx b/packages/website/src/features/docs/general/architecture/ArchitectureGeneralDocPage.tsx new file mode 100644 index 00000000..aa301db9 --- /dev/null +++ b/packages/website/src/features/docs/general/architecture/ArchitectureGeneralDocPage.tsx @@ -0,0 +1,108 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import { DocRoot } from "../../../../components/document/DocRoot.js" +import { DocTextSection } from "../../../../components/document/DocTextSection.js" +import { ArchitectureDiagram } from "./ArchitectureDiagram.js" + +export function ArchitectureGeneralDocPage() { + return ( + + {/* Page header */} +
+

+ Architecture +

+

+ Vue d'ensemble de l'architecture technique d'Arrhes et de ses différents services. +

+
+ + {/* Infogram */} + + + {/* Sections */} + +

+ Arrhes est organisé en plusieurs services distincts qui travaillent ensemble. Chaque service a un + rôle précis : certains sont destinés aux utilisateurs finaux, d'autres aux développeurs qui + souhaitent intégrer Arrhes dans leurs outils. +

+

+ Tous les services passent par l'API, qui est le cœur du système. C'est elle qui + gère les données, les autorisations et la logique métier. Le reste — dashboard, CLI, agent IA — + n'est que la façon dont chacun choisit d'y accéder. +

+
+ + +

+ Le dashboard est l'interface web d'Arrhes. Il permet de gérer ses organisations, saisir ses + écritures comptables, consulter ses documents de synthèse, stocker ses pièces justificatives et + discuter avec l'assistant IA. +

+

+ Pour qui ? Les utilisateurs qui souhaitent gérer leur comptabilité au quotidien + depuis un navigateur, sans installation ni configuration. +

+
+ + +

+ Le CLI est une interface en ligne de commande qui donne accès aux mêmes fonctionnalités que le + dashboard : organisations, exercices, écritures, exports et documents comptables. Il s'utilise + directement depuis un terminal, sur Linux ou macOS. +

+

+ Pour qui ? Les développeurs et équipes techniques qui souhaitent automatiser des + tâches, intégrer Arrhes dans des scripts ou des pipelines CI/CD. +

+
+ + +

+ L'API est le service central d'Arrhes. Elle reçoit toutes les requêtes — qu'elles viennent du + dashboard, du CLI ou d'une intégration tierce — et est responsable de l'authentification, des + données comptables, du stockage des fichiers et de l'assistant IA. +

+

+ Pour qui ? Les développeurs qui souhaitent accéder directement aux données d'Arrhes + depuis leur propre application, via des clés API. +

+
+ + +

+ La base de données stocke l'ensemble des données d'Arrhes : utilisateurs, organisations, exercices, + écritures comptables, fichiers et sessions. Elle est le seul endroit où les données sont persistées + et n'est jamais accessible directement depuis l'extérieur — uniquement via l'API. +

+

+ Le stockage des fichiers (pièces justificatives, documents exportés) est géré séparément, dans un + espace dédié compatible avec le protocole S3. Les emails transactionnels — comme les liens de + connexion — sont envoyés via un service SMTP. +

+

+ Pour qui ? Ce service est entièrement géré par Arrhes. Les utilisateurs et + développeurs n'ont pas à interagir avec lui directement. +

+
+
+ ) +} diff --git a/packages/website/src/features/docs/general/features/featureCard.tsx b/packages/website/src/features/docs/general/features/FeatureCard.tsx similarity index 100% rename from packages/website/src/features/docs/general/features/featureCard.tsx rename to packages/website/src/features/docs/general/features/FeatureCard.tsx diff --git a/packages/website/src/features/docs/general/features/FeaturesGeneralDocPage.tsx b/packages/website/src/features/docs/general/features/FeaturesGeneralDocPage.tsx new file mode 100644 index 00000000..5e6be66b --- /dev/null +++ b/packages/website/src/features/docs/general/features/FeaturesGeneralDocPage.tsx @@ -0,0 +1,168 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconBook, IconCode, IconFileText, IconLock, IconScan, IconSparkles, IconUsers } from "@tabler/icons-react" +import { DocRoot } from "../../../../components/document/DocRoot.js" +import { FeatureCard } from "./FeatureCard.js" + +export function FeaturesGeneralDocPage() { + return ( + + {/* Page header */} +
+

+ Fonctionnalités +

+

+ Découvrez toutes les fonctionnalités d'Arrhes pour gérer votre comptabilité de manière simple et + efficace. +

+
+ + {/* Features grid */} +
+ } + title="Saisie des écritures" + description="Enregistrez vos écritures comptables avec une interface intuitive." + features={[ + "Journal chronologique", + // "Import CSV/OFX", + // "Modèles d'écriture", + "Lettrage automatique", + ]} + /> + } + title="Espace de stockage" + description="Organisez vos documents et accédez-y rapidement." + features={[ + "Pièces justificatives (factures, relevés bancaires, etc.)", + "Visualisation des documents", + "Volume offert de 1Go par organisation", + ]} + /> + } + title="Documents comptables" + description="Générez tous vos documents comptables de synthèse conformes aux normes françaises." + features={[ + "Grand livre, Balance générale, Bilan comptable, Compte de résultat", + "Export du Fichier des Écritures Comptable (FEC)", + "Mise à jour automatique à chaque nouvelle écriture", + "Export PDF", + ]} + /> + {/* } + title="Analyses et rapports" + description="Visualisez vos données financières avec des tableaux de bord interactifs." + features={[ + "Évolution du chiffre d'affaires", + "Répartition des charges", + "Comparaisons N/N-1", + "Export PDF/Excel" + ]} + /> */} + } + title="Multiple organisations" + description="Gérez plusieurs structures avec un seul compte et collaborez en équipe." + features={[ + "Organisations illimitées", + "Gestion des membres", + // "Rôles et permissions", + "Exercices illimités", + ]} + /> + {/* } + title="Intelligence artificielle" + description="Bénéficiez d'un assistant IA pour automatiser vos tâches comptables." + features={[ + "Catégorisation automatique", + "Suggestions d'écritures", + "Détection d'anomalies", + "Questions en langage naturel" + ]} + /> */} + {/* } + title="API REST" + description="Intégrez Arrhes à vos outils grâce à notre API complète." + features={[ + "Authentification OAuth2", + "Webhooks", + "Documentation complète", + "SDK JavaScript" + ]} + /> */} + } + title="API REST" + description="Intégrez Arrhes à vos outils grâce à notre API complète." + features={[ + "Authentification par clé API", + "Accès complet aux écritures et documents", + "Documentation intégrée", + ]} + /> + } + title="Assistant IA" + description="Posez vos questions comptables en langage naturel et obtenez des réponses contextuelles." + features={[ + "1 million de tokens inclus", + "Accès aux écritures et documents de l'organisation", + "Questions en langage naturel", + ]} + /> + } + title="Reconnaissance optique (OCR)" + description="Extrayez le texte de vos documents pour les analyser avec l'assistant IA." + features={[ + "100 pages incluses", + "Formats PDF, JPEG, PNG", + "Intégration directe avec l'assistant IA", + ]} + /> + } + title="Sécurité" + description="Vos données sont protégées avec les meilleures pratiques de sécurité." + features={[ + "Chiffrement bout-en-bout", + "Sauvegardes automatiques", + "Hébergement en France", + ]} + /> +
+
+ ) +} diff --git a/packages/website/src/features/docs/general/features/featuresGeneralDocPage.tsx b/packages/website/src/features/docs/general/features/featuresGeneralDocPage.tsx deleted file mode 100644 index 8bee0815..00000000 --- a/packages/website/src/features/docs/general/features/featuresGeneralDocPage.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconBook, IconCode, IconFileText, IconLock, IconScan, IconSparkles, IconUsers } from "@tabler/icons-react" -import { DocRoot } from "../../../../components/document/docRoot.js" -import { FeatureCard } from "./featureCard.js" - -export function FeaturesGeneralDocPage() { - return ( - - {/* Page header */} -
-

- Fonctionnalités -

-

- Découvrez toutes les fonctionnalités d'Arrhes pour gérer votre comptabilité de manière simple et - efficace. -

-
- - {/* Features grid */} -
- } - title="Saisie des écritures" - description="Enregistrez vos écritures comptables avec une interface intuitive." - features={[ - "Journal chronologique", - // "Import CSV/OFX", - // "Modèles d'écriture", - "Lettrage automatique", - ]} - /> - } - title="Espace de stockage" - description="Organisez vos documents et accédez-y rapidement." - features={[ - "Pièces justificatives (factures, relevés bancaires, etc.)", - "Visualisation des documents", - "Volume offert de 1Go par organisation", - ]} - /> - } - title="Documents comptables" - description="Générez tous vos documents comptables de synthèse conformes aux normes françaises." - features={[ - "Grand livre, Balance générale, Bilan comptable, Compte de résultat", - "Export du Fichier des Écritures Comptable (FEC)", - "Mise à jour automatique à chaque nouvelle écriture", - "Export PDF", - ]} - /> - {/* } - title="Analyses et rapports" - description="Visualisez vos données financières avec des tableaux de bord interactifs." - features={[ - "Évolution du chiffre d'affaires", - "Répartition des charges", - "Comparaisons N/N-1", - "Export PDF/Excel" - ]} - /> */} - } - title="Multiple organisations" - description="Gérez plusieurs structures avec un seul compte et collaborez en équipe." - features={[ - "Organisations illimitées", - "Gestion des membres", - // "Rôles et permissions", - "Exercices illimités", - ]} - /> - {/* } - title="Intelligence artificielle" - description="Bénéficiez d'un assistant IA pour automatiser vos tâches comptables." - features={[ - "Catégorisation automatique", - "Suggestions d'écritures", - "Détection d'anomalies", - "Questions en langage naturel" - ]} - /> */} - {/* } - title="API REST" - description="Intégrez Arrhes à vos outils grâce à notre API complète." - features={[ - "Authentification OAuth2", - "Webhooks", - "Documentation complète", - "SDK JavaScript" - ]} - /> */} - } - title="API REST" - description="Intégrez Arrhes à vos outils grâce à notre API complète." - features={[ - "Authentification par clé API", - "Accès complet aux écritures et documents", - "Documentation intégrée", - ]} - /> - } - title="Assistant IA" - description="Posez vos questions comptables en langage naturel et obtenez des réponses contextuelles." - features={[ - "1 million de tokens inclus", - "Accès aux écritures et documents de l'organisation", - "Questions en langage naturel", - ]} - /> - } - title="Reconnaissance optique (OCR)" - description="Extrayez le texte de vos documents pour les analyser avec l'assistant IA." - features={[ - "100 pages incluses", - "Formats PDF, JPEG, PNG", - "Intégration directe avec l'assistant IA", - ]} - /> - } - title="Sécurité" - description="Vos données sont protégées avec les meilleures pratiques de sécurité." - features={[ - "Chiffrement bout-en-bout", - "Sauvegardes automatiques", - "Hébergement en France", - ]} - /> -
-
- ) -} diff --git a/packages/website/src/features/docs/general/legalGeneralDocPage.tsx b/packages/website/src/features/docs/general/legalGeneralDocPage.tsx deleted file mode 100644 index ff85926c..00000000 --- a/packages/website/src/features/docs/general/legalGeneralDocPage.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { LinkContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { DocLastUpdate } from "../../../components/document/docLastUpdate.js" -import { DocRoot } from "../../../components/document/docRoot.js" -import { DocTextSection } from "../../../components/document/docTextSection.js" -import { DocTip } from "../../../components/document/docTip.js" - -export function LegalGeneralDocPage() { - return ( - - {/* Page header */} -
-

- Mentions légales -

-

- Informations légales relatives à l'utilisation du site et du service Arrhes. -

-
- - {/* Content */} -
- -

- Le site arrhes.com est édité par Barbote SAS, société par actions simplifiée au capital de - 1000.00 euros, immatriculée au Registre du Commerce et des Sociétés de Paris sous le numéro 908 - 719 503. -

-

- Siège social : 93 rue Sedaine, 75011 Paris -
- Numéro de TVA intracommunautaire : FR02 908 719 503 -
- Directeur de la publication : Emile Sabatier -
- Contact :{" "} - - contact@arrhes.com - -

-
- - -

- Le site est hébergé par la société :
- OVH -
- RCS Lille Métropole, 424 761 419 00045 -
2 rue Kellermann - 59100 Roubaix - France -

-
- - -

- L'ensemble du contenu de ce site (textes, images, vidéos, logos, icônes, etc.) est protégé par - les lois relatives à la propriété intellectuelle. -

-

- Le code source d'Arrhes est distribué sous licence{" "} - - AGPL-3.0 - - . Consultez notre dépôt GitHub pour plus d'informations. -

-
- - -

- Les informations relatives au traitement de vos données personnelles sont détaillées dans notre{" "} - - Politique de confidentialité - - . -

-
- - -

- Ce site utilise des cookies strictement nécessaires au fonctionnement du service. Aucun cookie - publicitaire ou de traçage n'est utilisé. -

-
- - -

- Arrhes s'efforce de fournir des informations exactes et à jour sur ce site. Toutefois, nous ne - pouvons garantir l'exactitude, la complétude ou l'actualité des informations diffusées. -

-

- L'utilisateur est seul responsable de l'utilisation qu'il fait des informations et - fonctionnalités disponibles sur ce site. -

-
- - Oui, pour notre comptabilité, nous utilisons Arrhes. ;) - - -
-
- ) -} diff --git a/packages/website/src/features/docs/general/pricing/faqItem.tsx b/packages/website/src/features/docs/general/pricing/FaqItem.tsx similarity index 100% rename from packages/website/src/features/docs/general/pricing/faqItem.tsx rename to packages/website/src/features/docs/general/pricing/FaqItem.tsx diff --git a/packages/website/src/features/docs/general/pricing/PricingGeneralDocPage.tsx b/packages/website/src/features/docs/general/pricing/PricingGeneralDocPage.tsx new file mode 100644 index 00000000..12f1eb02 --- /dev/null +++ b/packages/website/src/features/docs/general/pricing/PricingGeneralDocPage.tsx @@ -0,0 +1,89 @@ +import { css } from "@arrhes/ui/utilities/cn.js" +import { DocRoot } from "../../../../components/document/DocRoot.js" +import { Pricing } from "../../../home/Pricing.js" +import { FaqItem } from "./FaqItem.js" + +export function PricingGeneralDocPage() { + return ( + + {/* Page header */} +
+

+ Tarifs +

+

+ Un logiciel de comptabilité conçu pour être transparent et accessible à tous. +

+
+ + {/* Pricing cards */} + + + {/* FAQ */} +
+

+ Questions fréquentes +

+
+ + + + +
+
+
+ ) +} diff --git a/packages/website/src/features/docs/general/pricing/pricingGeneralDocPage.tsx b/packages/website/src/features/docs/general/pricing/pricingGeneralDocPage.tsx deleted file mode 100644 index c490bb81..00000000 --- a/packages/website/src/features/docs/general/pricing/pricingGeneralDocPage.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" -import { DocRoot } from "../../../../components/document/docRoot.js" -import { Pricing } from "../../../home/pricing.js" -import { FaqItem } from "./faqItem.js" - -export function PricingGeneralDocPage() { - return ( - - {/* Page header */} -
-

- Tarifs -

-

- Un logiciel de comptabilité conçu pour être transparent et accessible à tous. -

-
- - {/* Pricing cards */} - - - {/* FAQ */} -
-

- Questions fréquentes -

-
- - - - -
-
-
- ) -} diff --git a/packages/website/src/features/docs/general/privacyGeneralDocPage.tsx b/packages/website/src/features/docs/general/privacyGeneralDocPage.tsx deleted file mode 100644 index 433aee88..00000000 --- a/packages/website/src/features/docs/general/privacyGeneralDocPage.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import { LinkContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { DocLastUpdate } from "../../../components/document/docLastUpdate.js" -import { DocRoot } from "../../../components/document/docRoot.js" -import { DocTextSection } from "../../../components/document/docTextSection.js" -import { LinkButton } from "../../../components/linkButton.js" - -export function PrivacyGeneralDocPage() { - return ( - - {/* Page header */} -
-

- Politique de confidentialité -

-

- Comment nous collectons, utilisons et protégeons vos données personnelles. -

-
- - {/* Content */} -
- -

- Le responsable du traitement des données personnelles est la même société que celle qui - mentionnée dans les{" "} - - mentions légales - - . -

-

- Pour toute question relative à vos données personnelles, vous pouvez nous contacter à l'adresse - :{" "} - - support@arrhes.com - -

-
- - -

Nous collectons les données suivantes :

-
    -
  • - Données d'identification : nom, prénom, adresse email -
  • -
  • - Données de connexion : adresse IP, logs de connexion -
  • -
  • - Données comptables : écritures, comptes, documents que vous saisissez -
  • -
  • - Données d'usage : fonctionnalités utilisées, préférences -
  • -
-
- - -

Vos données sont utilisées pour :

-
    -
  • Fournir et améliorer le service
  • -
  • Gérer votre compte utilisateur
  • -
  • Assurer la sécurité du service
  • -
  • Vous contacter pour des informations relatives au service
  • -
  • Respecter nos obligations légales
  • -
-
- - -

Le traitement de vos données repose sur les bases légales suivantes :

-
    -
  • - Exécution du contrat : pour fournir le service -
  • -
  • - Intérêt légitime : pour améliorer nos services et assurer la sécurité -
  • -
  • - Consentement : pour les communications (optionnel) -
  • -
  • - Obligation légale : pour la conservation des données liées au paiement -
  • -
-
- - -

Vos données sont conservées pendant la durée de votre utilisation du service, puis :

-
    -
  • Données de compte : supprimées à la clôture du compte
  • -
  • Données de paiement : conservées 10 ans conformément à la loi
  • -
  • Logs de connexion : conservés 1 an
  • -
-
- - -

Vos données peuvent être partagées avec :

-
    -
  • Nos sous-traitants techniques (hébergement, email)
  • -
  • Les autorités en cas d'obligation légale
  • -
-

Nous ne fournissons jamais vos données, à quelque titre que ce soit, à des tiers.

-
- - -

- Vos données sont hébergées en France et ne font l'objet d'aucun transfert en dehors de l'Union - Européenne. -

-
- - -

- Nous mettons en oeuvre des mesures techniques et organisationnelles pour protéger vos données : -

-
    -
  • Chiffrement des données en transit et au repos
  • - {/*
  • Authentification sécurisée avec option 2FA
  • */} -
  • Sauvegardes régulières
  • -
  • Accès limité au personnel autorisé
  • -
-
- - -

Conformément au RGPD, vous disposez des droits suivants :

-
    -
  • - Droit d'accès : obtenir une copie de vos données -
  • -
  • - Droit de rectification : corriger vos données -
  • -
  • - Droit à l'effacement : supprimer vos données -
  • -
  • - Droit à la portabilité : exporter vos données -
  • -
  • - Droit d'opposition : vous opposer à certains traitements -
  • -
  • - Droit de limitation : limiter le traitement -
  • -
-

- Pour exercer ces droits, contactez-nous à{" "} - - support@arrhes.com - - . -

-
- - -

- Si vous estimez que le traitement de vos données ne respecte pas la réglementation, nous pouvons - en discuter ensemble. Dans le cas échéant, vous pouvez introduire une réclamation auprès de la - CNIL (Commission Nationale de l'Informatique et des Libertés). -

-
- - -
-
- ) -} diff --git a/packages/website/src/features/docs/general/rootGeneralDocPage.tsx b/packages/website/src/features/docs/general/rootGeneralDocPage.tsx deleted file mode 100644 index e8f03ee3..00000000 --- a/packages/website/src/features/docs/general/rootGeneralDocPage.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { ButtonOutlineContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconChevronRight } from "@tabler/icons-react" -import { DocHeader } from "../../../components/document/docHeader.js" -import { DocRoot } from "../../../components/document/docRoot.js" -import { LinkButton } from "../../../components/linkButton.js" - -export function RootGeneralDocPage() { - return ( - - - - {/* About section */} -
-

- Arrhes est un logiciel de comptabilité open source conçu pour tous. Accessible, moderne et - respectueux des normes comptables françaises, il vous permet de gérer vos écritures comptables, vos - documents, générer vos documents fiscaux et collaborer avec votre équipe. -

-
- - } - /> - -
-
-
- ) -} diff --git a/packages/website/src/features/docs/general/supportGeneralDocPage.tsx b/packages/website/src/features/docs/general/supportGeneralDocPage.tsx deleted file mode 100644 index b8d5ab13..00000000 --- a/packages/website/src/features/docs/general/supportGeneralDocPage.tsx +++ /dev/null @@ -1,404 +0,0 @@ -import { ButtonOutlineContent, LinkContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconBrandGithub, IconBrandLinkedin, IconExternalLink, IconMail } from "@tabler/icons-react" -import { DocRoot } from "../../../components/document/docRoot.js" -import { DocTip } from "../../../components/document/docTip.js" -import { LinkButton } from "../../../components/linkButton.js" - -export function SupportGeneralDocPage() { - return ( - - {/* Page header */} -
-

- Support -

-

- Besoin d'aide ? Nous sommes là pour vous accompagner. -

-
- - {/* Contact cards */} -
- {/* Email support */} -
-
-
- -
-

- Email -

-
-

- Contactez notre équipe par email pour toute question ou demande d'assistance. -

- - support@arrhes.com - -
- - {/* GitHub */} -
-
-
- -
-

- GitHub -

-
-

- Signalez un bug, proposez une amélioration ou consultez le code source. -

- - } - text="Voir sur GitHub" - /> - -
- - {/* LinkedIn */} -
-
-
- -
-

- LinkedIn -

-
-

- Suivez-nous sur LinkedIn pour les dernières nouvelles. -

- - } - text="Voir sur LinkedIn" - /> - -
-
- - {/* Response time info */} - - Nous nous efforçons de répondre à toutes les demandes dans un délai raisonnable. - - - {/* Documentation section */} -
-

- Ressources utiles -

- -
- -
-

- Cours de comptabilité -

-

- Les bases de la comptabilité expliquées simplement -

-
-
- - -
-

- Guide d'utilisation du dashboard -

-

- Apprenez à utiliser l'interface web efficacement -

-
-
- - -
-

- Guide d'utilisation de l'API -

-

- Apprenez à utiliser l'API -

-
-
-
-
-
- ) -} diff --git a/packages/website/src/features/docs/general/termsGeneralDocPage.tsx b/packages/website/src/features/docs/general/termsGeneralDocPage.tsx deleted file mode 100644 index 2ee83879..00000000 --- a/packages/website/src/features/docs/general/termsGeneralDocPage.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { LinkContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { DocLastUpdate } from "../../../components/document/docLastUpdate.js" -import { DocRoot } from "../../../components/document/docRoot.js" -import { DocTextSection } from "../../../components/document/docTextSection.js" - -export function TermsGeneralDocPage() { - return ( - - {/* Page header */} -
-

- Conditions Générales d'Utilisation -

-

- Conditions régissant l'utilisation du service Arrhes. -

-
- - {/* Content */} -
- -

- Les présentes Conditions Générales d'Utilisation (CGU) ont pour objet de définir les modalités - et conditions d'utilisation du service Arrhes, accessible à l'adresse arrhes.com. -

-

En utilisant le service, l'utilisateur accepte sans réserve les présentes CGU.

-
- - -

- Arrhes est un logiciel de comptabilité en ligne permettant aux entreprises et associations de - gérer leur comptabilité, saisir des écritures, générer des documents comptables et collaborer en - équipe. -

-

- Le service est proposé en version basique (gratuite) avec les fonctionnalités de base, et en - version avancée (payante) avec des fonctionnalités étendues. -

-
- - -

- L'utilisation du service nécessite la création d'un compte utilisateur. L'utilisateur s'engage à - fournir des informations exactes et à jour lors de son inscription. -

-

- L'utilisateur est responsable de la confidentialité de ses identifiants de connexion et de - toutes les activités effectuées depuis son compte. -

-
- - -

L'utilisateur s'engage à :

-
    -
  • Utiliser le service conformément à sa destination
  • -
  • Ne pas utiliser le service à des fins illégales ou frauduleuses
  • -
  • Ne pas tenter de compromettre la sécurité du service
  • -
  • Respecter les droits de propriété intellectuelle
  • -
  • Ne pas collecter de données d'autres utilisateurs sans autorisation
  • -
-
- - -

- L'utilisateur est seul responsable des données qu'il saisit dans le service. Arrhes ne peut être - tenu responsable des erreurs comptables résultant d'une mauvaise utilisation du service. -

-

- L'utilisateur est encouragé à vérifier ses données et à consulter un professionnel comptable en - cas de doute. -

-
- - -

- Le code source d'Arrhes est distribué sous licence{" "} - - AGPL-3.0 - - . Les conditions spécifiques de cette licence sont disponibles sur notre dépôt GitHub. -

-

Les marques, logos et contenus graphiques restent la propriété de l'entreprise.

-
- - -

- Le traitement des données personnelles est décrit dans notre{" "} - - Politique de confidentialité - - . -

-
- - -

- L'utilisateur peut supprimer son compte à tout moment depuis les paramètres de son profil. La - suppression entraîne l'effacement de toutes les données associées au compte, sauf celle - nécessaires, i.e. les données légales. -

-

- Arrhes se réserve le droit de suspendre ou supprimer un compte en cas de violation des présentes - CGU. -

-
- - -

- Arrhes se réserve le droit de modifier les présentes CGU à tout moment. Les utilisateurs seront - informés de toute modification substantielle par email ou notification dans l'application. -

-
- - -

- Les présentes CGU sont soumises au droit français. En cas de litige, les tribunaux de Paris - seront seuls compétents. -

-
- - -
-
- ) -} diff --git a/packages/website/src/features/docs/general/whitepaperGeneralDocPage.tsx b/packages/website/src/features/docs/general/whitepaperGeneralDocPage.tsx deleted file mode 100644 index c8fef895..00000000 --- a/packages/website/src/features/docs/general/whitepaperGeneralDocPage.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { css } from "@arrhes/ui/utilities/cn.js" -import { DocLastUpdate } from "../../../components/document/docLastUpdate.js" -import { DocRoot } from "../../../components/document/docRoot.js" -import { DocTextSection } from "../../../components/document/docTextSection.js" - -export function WhitepaperGeneralDocPage() { - return ( - - {/* Page header */} -
-

- Notre philosophie -

-

- Nous souhaitons une transparence totale avec notre communauté. C'est pourquoi nous avons rédigé ces - quelques lignes. -

-
- - {/* Content */} -
- -

- Nous voulons que la comptabilité devienne accessible, compréhensible et même agréable. Nous - construisons Arrhes pour tout le monde : autant ceux qui veulent faire leur comptabilité - simplement que les experts du domaine. -

-

- À terme, nous voulons qu'Arrhes devienne la référence open source de la comptabilité en France - - un outil moderne, transparent et communautaire, soutenu par un modèle économique sain. Un outil - dont la communauté est fière, parce qu'elle contribue à le rendre meilleur chaque jour. -

-

- L'intelligence artificielle est au coeur de cette vision. Pas comme un gadget marketing, mais - comme un véritable assistant qui peut vous accompagner, suivre des instructions, détecter les - anomalies, et vous aider à prendre des décisions. L'IA doit rendre la comptabilité plus - accessible. -

-
- - -

- Arrhes est un logiciel open source parce que nous croyons que la comptabilité ne devrait pas - être une boîte noire. Quand il s'agit de vos finances, vous devriez pouvoir vérifier exactement - ce que fait votre outil, comment il calcule vos soldes, et où sont stockées vos données. Mieux - comprendre sa comptabilité, c'est mieux comprendre son activité. -

-

- Mais au-delà de la transparence, l'open source est un choix communautaire. Un logiciel de - comptabilité utilisé par des associations, des indépendants et des entreprises a tout intérêt à - être construit avec eux. Les retours, les contributions et les idées de la communauté rendent le - produit meilleur pour tout le monde. Chaque utilisateur peut signaler un bug, proposer une - amélioration ou adapter l'outil à ses besoins. -

-

- Nous pensons aussi qu'à l'ère de l'intelligence artificielle, l'open source a plus de sens que - jamais. Les modèles IA sont entraînés sur du code et savent le restituer. Nous pensons que la - valeur ajoutée ne réside plus dans le code, mais plutôt dans l'architecture pensée par et pour - les utilisateurs. -

-
- - -

- Notre approche est simple : le coeur du logiciel est gratuit, et nous faisons payer uniquement - ce qui nous coûte cher à fournir. -

-

- Toute la comptabilité - saisie des écritures, plan comptable, bilan, compte de résultat, gestion - des pièces jointes - est accessible gratuitement. Nous ne croyons pas aux limitations - artificielles qui frustrent les utilisateurs pour les pousser à payer. -

-

- Ce qui est payant, ce sont les fonctionnalités qui ont un coût réel pour nous : le stockage, - l'assistant IA (qui consomme des ressources de calcul), l'hébergement cloud géré (serveurs, - sauvegardes, maintenance), et les intégrations avancées. En résumé, vous payez pour - l'infrastructure, pas pour le logiciel lui-même. -

-

- Ce modèle nous semble juste. Les revenus générés financent l'ensemble du projet : le - développement des fonctionnalités, la maintenance, la documentation et le support communautaire. - Chaque licence soutient l'outil pour tout le monde. -

-
- - -
-
- ) -} diff --git a/packages/website/src/features/docs/sectionTab.tsx b/packages/website/src/features/docs/sectionTab.tsx deleted file mode 100644 index e70ab3bc..00000000 --- a/packages/website/src/features/docs/sectionTab.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { ButtonGhostContent } from "@arrhes/ui" -import type { Icon, IconProps } from "@tabler/icons-react" -import type { ReactElement } from "react" -import { LinkButton } from "../../components/linkButton.tsx" - -export interface DocSection { - id: string - label: string - path: string - icon: ReactElement> - navigation: Record< - string, - { - title?: string - icon?: ReactElement> - items: { - path: string - hash?: string - label: string - chipText?: string - chipColor?: "neutral" | "information" | "error" | "warning" | "success" - }[] - } - > -} - -export function SectionTab(props: { section: DocSection; isActive: boolean }) { - return ( - - - - ) -} diff --git a/packages/website/src/features/docs/sidebarNavigation.tsx b/packages/website/src/features/docs/sidebarNavigation.tsx deleted file mode 100644 index 40210762..00000000 --- a/packages/website/src/features/docs/sidebarNavigation.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { ButtonGhostContent, Chip, type ChipColors } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import type { Icon, IconProps } from "@tabler/icons-react" -import { cloneElement, type ReactElement } from "react" -import { LinkButton } from "../../components/linkButton.tsx" - -export interface NavigationSection { - title?: string - icon?: ReactElement> - items: { - path: string - hash?: string - label: string - chipText?: string - chipColor?: ChipColors - }[] -} - -export function SidebarNavigation(props: { - navigation: Record - pathname: string - onClick?: () => void -}) { - return ( - - ) -} diff --git a/packages/website/src/features/error/errorPage.tsx b/packages/website/src/features/error/ErrorPage.tsx similarity index 100% rename from packages/website/src/features/error/errorPage.tsx rename to packages/website/src/features/error/ErrorPage.tsx diff --git a/packages/website/src/features/facturation/ublInvoiceViewer.tsx b/packages/website/src/features/facturation/UblInvoiceViewer.tsx similarity index 100% rename from packages/website/src/features/facturation/ublInvoiceViewer.tsx rename to packages/website/src/features/facturation/UblInvoiceViewer.tsx diff --git a/packages/website/src/features/home/DocumentationBanner.tsx b/packages/website/src/features/home/DocumentationBanner.tsx new file mode 100644 index 00000000..0301d562 --- /dev/null +++ b/packages/website/src/features/home/DocumentationBanner.tsx @@ -0,0 +1,53 @@ +import { ButtonPlainContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconExternalLink } from "@tabler/icons-react" +import { Box } from "../../components/layouts/Box.js" + +export function DocumentationBanner() { + return ( + +
+

+ Documentation +

+

+ Collaborative et ouverte +

+
+ + } + text="Consulter la documentation" + /> + +
+ ) +} diff --git a/packages/website/src/features/home/featureItem.tsx b/packages/website/src/features/home/FeatureItem.tsx similarity index 100% rename from packages/website/src/features/home/featureItem.tsx rename to packages/website/src/features/home/FeatureItem.tsx diff --git a/packages/website/src/features/home/heroIllustration.tsx b/packages/website/src/features/home/HeroIllustration.tsx similarity index 100% rename from packages/website/src/features/home/heroIllustration.tsx rename to packages/website/src/features/home/HeroIllustration.tsx diff --git a/packages/website/src/features/home/HomePage.tsx b/packages/website/src/features/home/HomePage.tsx new file mode 100644 index 00000000..bb5061ec --- /dev/null +++ b/packages/website/src/features/home/HomePage.tsx @@ -0,0 +1,371 @@ +import { Badge, ButtonGhostContent, ButtonOutlineContent, ButtonPlainContent, Logo } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconBook2, IconBrandGithub, IconBrandLinkedin, IconGavel, IconUser } from "@tabler/icons-react" +import { LinkButton } from "../../components/LinkButton.js" +import { HeroIllustration } from "./HeroIllustration.js" +import { Pricing } from "./Pricing.js" + +export function HomePage() { + return ( +
+ {/* Header */} +
+
+
+ + } + text="Arrhes" + /> + +
+ +
+
+ + {/* Hero Section */} +
+
+ {/* Content */} +
+ Open source + +

+ Le logiciel de comptabilité moderne et intuitif, +
+ pour{" "} + + ceux qui veulent reprendre la main sur leur comptabilité + {" "} + . +

+ +
+ + } + text="Créer un compte" + /> + + + } + text="En savoir plus" + /> + +
+
+ + {/* Illustration */} +
+ +
+
+
+ + {/* Pricing Section */} +
+
+

+ Pourquoi choisir Arrhes ? +

+

+ Un logiciel de comptabilité conçu pour être simple et transparent, tout en respectant les + exigences de la comptabilité française. +
+ Il s'adresse à tous : indépendants, entreprises, associations, experts-comptables... +

+ + {/* Pricing columns */} + +
+
+ + {/* Documentation CTA */} +
+
+

+ Nouveau en comptabilité ? +

+

+ Notre documentation inclut un cours sur les bases de la comptabilité française. +
+ Apprenez les concepts essentiels : partie double, plan comptable, écritures, journaux... +

+ + } + text="Découvrir le cours" + /> + +
+
+ + {/* Footer */} +
+
+ {/* + Arrhes + {/* [2024-{(new Date()).getFullYear()}] + */} + {/* */} + + } + text="Mentions légales" + /> + + +
+
+
+ ) +} diff --git a/packages/website/src/features/home/OrganizationsBanner.tsx b/packages/website/src/features/home/OrganizationsBanner.tsx new file mode 100644 index 00000000..ccccd7ea --- /dev/null +++ b/packages/website/src/features/home/OrganizationsBanner.tsx @@ -0,0 +1,51 @@ +import { ButtonPlainContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconBuilding } from "@tabler/icons-react" +import { LinkButton } from "../../components/LinkButton.js" +import { Box } from "../../components/layouts/Box.js" + +export function OrganizationsBanner() { + return ( + +
+

+ Mes organisations +

+

+ Que vous avez créées ou auxquelles vous avez été invité +

+
+ + } + text="Voir mes organisations" + /> + +
+ ) +} diff --git a/packages/website/src/features/home/Pricing.tsx b/packages/website/src/features/home/Pricing.tsx new file mode 100644 index 00000000..c2722694 --- /dev/null +++ b/packages/website/src/features/home/Pricing.tsx @@ -0,0 +1,236 @@ +import { ButtonOutlineContent } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { + IconAppWindow, + IconBuildings, + IconCalculator, + IconCloudUpload, + IconCode, + IconDownload, + IconFileText, + IconLifebuoy, + IconReload, + IconSparkles, + IconUserPlus, + IconUsers, +} from "@tabler/icons-react" +import { LinkButton } from "../../components/LinkButton.js" +import { FeatureItem } from "./FeatureItem.tsx" + +export function Pricing() { + return ( +
+ {/* Free Plan */} +
+
+
+

+ Services inclus +

+
+

+ Toutes les fonctionnalités essentielles pour gérer votre comptabilité. Gratuitement. +

+
+ +
+ } + text="Accès illimité à l'application web" + /> + } + text="Multiple organisations" + /> + } + text="Membres illimités" + /> + } + text="Stockage de documents (jusqu'à 1Go)" + /> + } + text="Génération des documents de synthèse pour la liasse fiscale" + /> + } + text="Export du Fichier des Écritures Comptable (FEC)" + /> + } + text="Accès complet à l'API" + /> + } + text="Assistant IA : 1 million de tokens offerts" + /> + } + text="OCR : 100 pages offertes" + /> + } + text="Comptabilité analytique" + isDev + /> +
+ + + } + text="Créer un compte" + className={css({ + width: "100%", + })} + /> + +
+ + {/* Paid services */} +
+
+
+

+ Services optionnels +

+
+

+ Certaines fonctionnalités ont un coût structurel que nous devons répercuter. Sans engagement. +

+
+ +
+ } + text="Licence" + highlighted + description={[ + "Montant mensuel libre, pour supporter le projet et les frais des serveurs. Vous bénéficiez d'un support privilégié si vous prenez une licence payante.", + ]} + /> + } + text="Stockage" + highlighted + description={[ + "0,10€ (HT) / Go / mois (au-dela du Go inclus)", + ]} + /> + } + text="Tokens IA" + description={[ + "1,00€ (HT) / million de tokens", + ]} + highlighted + /> + } + text="OCR" + description={[ + "0,01€ (HT) / page", + ]} + highlighted + /> +
+
+
+ ) +} diff --git a/packages/website/src/features/home/documentationBanner.tsx b/packages/website/src/features/home/documentationBanner.tsx deleted file mode 100644 index c4bd4695..00000000 --- a/packages/website/src/features/home/documentationBanner.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { ButtonPlainContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconExternalLink } from "@tabler/icons-react" -import { Box } from "../../components/layouts/box.js" - -export function DocumentationBanner() { - return ( - -
-

- Documentation -

-

- Collaborative et ouverte -

-
- - } - text="Consulter la documentation" - /> - -
- ) -} diff --git a/packages/website/src/features/home/homePage.tsx b/packages/website/src/features/home/homePage.tsx deleted file mode 100644 index f15df78a..00000000 --- a/packages/website/src/features/home/homePage.tsx +++ /dev/null @@ -1,371 +0,0 @@ -import { Badge, ButtonGhostContent, ButtonOutlineContent, ButtonPlainContent, Logo } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconBook2, IconBrandGithub, IconBrandLinkedin, IconGavel, IconUser } from "@tabler/icons-react" -import { LinkButton } from "../../components/linkButton.js" -import { HeroIllustration } from "./heroIllustration.js" -import { Pricing } from "./pricing.js" - -export function HomePage() { - return ( -
- {/* Header */} -
-
-
- - } - text="Arrhes" - /> - -
- -
-
- - {/* Hero Section */} -
-
- {/* Content */} -
- Open source - -

- Le logiciel de comptabilité moderne et intuitif, -
- pour{" "} - - ceux qui veulent reprendre la main sur leur comptabilité - {" "} - . -

- -
- - } - text="Créer un compte" - /> - - - } - text="En savoir plus" - /> - -
-
- - {/* Illustration */} -
- -
-
-
- - {/* Pricing Section */} -
-
-

- Pourquoi choisir Arrhes ? -

-

- Un logiciel de comptabilité conçu pour être simple et transparent, tout en respectant les - exigences de la comptabilité française. -
- Il s'adresse à tous : indépendants, entreprises, associations, experts-comptables... -

- - {/* Pricing columns */} - -
-
- - {/* Documentation CTA */} -
-
-

- Nouveau en comptabilité ? -

-

- Notre documentation inclut un cours sur les bases de la comptabilité française. -
- Apprenez les concepts essentiels : partie double, plan comptable, écritures, journaux... -

- - } - text="Découvrir le cours" - /> - -
-
- - {/* Footer */} -
-
- {/* - Arrhes - {/* [2024-{(new Date()).getFullYear()}] - */} - {/* */} - - } - text="Mentions légales" - /> - - -
-
-
- ) -} diff --git a/packages/website/src/features/home/organizationsBanner.tsx b/packages/website/src/features/home/organizationsBanner.tsx deleted file mode 100644 index 39308278..00000000 --- a/packages/website/src/features/home/organizationsBanner.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { ButtonPlainContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconBuilding } from "@tabler/icons-react" -import { Box } from "../../components/layouts/box.js" -import { LinkButton } from "../../components/linkButton.js" - -export function OrganizationsBanner() { - return ( - -
-

- Mes organisations -

-

- Que vous avez créées ou auxquelles vous avez été invité -

-
- - } - text="Voir mes organisations" - /> - -
- ) -} diff --git a/packages/website/src/features/home/pricing.tsx b/packages/website/src/features/home/pricing.tsx deleted file mode 100644 index 773e14d4..00000000 --- a/packages/website/src/features/home/pricing.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import { ButtonOutlineContent } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { - IconAppWindow, - IconBuildings, - IconCalculator, - IconCloudUpload, - IconCode, - IconDownload, - IconFileText, - IconLifebuoy, - IconReload, - IconSparkles, - IconUserPlus, - IconUsers, -} from "@tabler/icons-react" -import { LinkButton } from "../../components/linkButton.js" -import { FeatureItem } from "./featureItem.tsx" - -export function Pricing() { - return ( -
- {/* Free Plan */} -
-
-
-

- Services inclus -

-
-

- Toutes les fonctionnalités essentielles pour gérer votre comptabilité. Gratuitement. -

-
- -
- } - text="Accès illimité à l'application web" - /> - } - text="Multiple organisations" - /> - } - text="Membres illimités" - /> - } - text="Stockage de documents (jusqu'à 1Go)" - /> - } - text="Génération des documents de synthèse pour la liasse fiscale" - /> - } - text="Export du Fichier des Écritures Comptable (FEC)" - /> - } - text="Accès complet à l'API" - /> - } - text="Assistant IA : 1 million de tokens offerts" - /> - } - text="OCR : 100 pages offertes" - /> - } - text="Comptabilité analytique" - isDev - /> -
- - - } - text="Créer un compte" - className={css({ - width: "100%", - })} - /> - -
- - {/* Paid services */} -
-
-
-

- Services optionnels -

-
-

- Certaines fonctionnalités ont un coût structurel que nous devons répercuter. Sans engagement. -

-
- -
- } - text="Licence" - highlighted - description={[ - "Montant mensuel libre, pour supporter le projet et les frais des serveurs. Vous bénéficiez d'un support privilégié si vous prenez une licence payante.", - ]} - /> - } - text="Stockage" - highlighted - description={[ - "0,10€ (HT) / Go / mois (au-dela du Go inclus)", - ]} - /> - } - text="Tokens IA" - description={[ - "1,00€ (HT) / million de tokens", - ]} - highlighted - /> - } - text="OCR" - description={[ - "0,01€ (HT) / page", - ]} - highlighted - /> -
-
-
- ) -} diff --git a/packages/website/src/features/notFound/NotFoundPage.tsx b/packages/website/src/features/notFound/NotFoundPage.tsx new file mode 100644 index 00000000..f281bea8 --- /dev/null +++ b/packages/website/src/features/notFound/NotFoundPage.tsx @@ -0,0 +1,99 @@ +import { ButtonOutlineContent } from "@arrhes/ui/components/buttons/ButtonOutlineContent.js" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconBook2, IconHome } from "@tabler/icons-react" +import { LinkButton } from "../../components/LinkButton.js" + +export function NotFoundPage() { + return ( +
+
+
+ + 404 + +
+

+ Page introuvable +

+

+ La page que vous recherchez n'existe pas ou a été déplacée. +

+
+
+ + } + /> + + + } + /> + +
+
+
+
+ ) +} diff --git a/packages/website/src/features/notFound/notFoundPage.tsx b/packages/website/src/features/notFound/notFoundPage.tsx deleted file mode 100644 index 07b5393e..00000000 --- a/packages/website/src/features/notFound/notFoundPage.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { ButtonOutlineContent } from "@arrhes/ui/components/buttons/buttonOutlineContent.js" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconBook2, IconHome } from "@tabler/icons-react" -import { LinkButton } from "../../components/linkButton.js" - -export function NotFoundPage() { - return ( -
-
-
- - 404 - -
-

- Page introuvable -

-

- La page que vous recherchez n'existe pas ou a été déplacée. -

-
-
- - } - /> - - - } - /> - -
-
-
-
- ) -} diff --git a/packages/website/src/features/public/invoiceXml/publicInvoiceXmlPage.tsx b/packages/website/src/features/public/invoiceXml/PublicInvoiceXmlPage.tsx similarity index 100% rename from packages/website/src/features/public/invoiceXml/publicInvoiceXmlPage.tsx rename to packages/website/src/features/public/invoiceXml/PublicInvoiceXmlPage.tsx diff --git a/packages/website/src/features/signIn/ResetPasswordPage.tsx b/packages/website/src/features/signIn/ResetPasswordPage.tsx new file mode 100644 index 00000000..e64276de --- /dev/null +++ b/packages/website/src/features/signIn/ResetPasswordPage.tsx @@ -0,0 +1,197 @@ +import { resetPasswordRouteDefinition } from "@arrhes/application-metadata/routes" +import { ButtonGhostContent, InputText, Logo, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconArrowLeft, IconBook2, IconMail } from "@tabler/icons-react" +import { FormControl } from "../../components/forms/FormControl.js" +import { FormError } from "../../components/forms/FormError.js" +import { FormField } from "../../components/forms/FormField.js" +import { FormItem } from "../../components/forms/FormItem.js" +import { FormLabel } from "../../components/forms/FormLabel.js" +import { FormRoot } from "../../components/forms/FormRoot.js" +import { LinkButton } from "../../components/LinkButton.js" +import { getResponseBodyFromAPI } from "../../utilities/getResponseBodyFromAPI.js" + +export function ResetPasswordPage() { + return ( +
+
+
+
+ + } + text="Dashboard" + /> + + + } + className={css({ + width: "100%", + justifyContent: "center", + })} + /> + +
+ + + } + text="Retour à la connexion" + /> + + +
+

+ Réinitialiser le mot de passe +

+

+ Saisissez votre email pour recevoir un nouveau mot de passe temporaire. +

+
+ + , + text: "Recevoir un nouveau mot de passe", + className: css({ + width: "100%", + justifyContent: "center", + }), + }} + submitOnPressEnterKey={true} + onSubmit={async (data) => { + const response = await getResponseBodyFromAPI({ + routeDefinition: resetPasswordRouteDefinition, + body: data, + }) + + if (response.ok === false) { + toast({ + title: response.error?.cause ?? "Réinitialisation impossible", + variant: "error", + }) + return false + } + + toast({ + title: "Un nouveau mot de passe vous a été envoyé par email", + description: "Pensez à le modifier dès votre prochaine connexion.", + variant: "success", + }) + return true + }} + onCancel={undefined} + onSuccess={() => { + window.location.assign("/connexion") + }} + > + {(form) => ( + ( + + + + + + + + )} + /> + )} + + +

+ Après connexion, modifiez ce mot de passe temporaire depuis votre profil pour sécuriser votre + compte. +

+
+
+
+ ) +} diff --git a/packages/website/src/features/signIn/SignInPage.tsx b/packages/website/src/features/signIn/SignInPage.tsx new file mode 100644 index 00000000..31522f3b --- /dev/null +++ b/packages/website/src/features/signIn/SignInPage.tsx @@ -0,0 +1,245 @@ +import { signInRouteDefinition } from "@arrhes/application-metadata/routes" +import { ButtonGhostContent, ButtonOutlineContent, InputPassword, InputText, Logo, Separator, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconBook2, IconLogin2, IconUserPlus } from "@tabler/icons-react" +import { Fragment } from "react/jsx-runtime" +import { FormControl } from "../../components/forms/FormControl.js" +import { FormError } from "../../components/forms/FormError.js" +import { FormField } from "../../components/forms/FormField.js" +import { FormItem } from "../../components/forms/FormItem.js" +import { FormLabel } from "../../components/forms/FormLabel.js" +import { FormRoot } from "../../components/forms/FormRoot.js" +import { LinkButton } from "../../components/LinkButton.js" +import { setCookie } from "../../utilities/cookies/setCookie.js" +import { getResponseBodyFromAPI } from "../../utilities/getResponseBodyFromAPI.js" +import { cookiePrefix } from "../../utilities/variables.js" + +export function SignInPage() { + return ( +
+ {/* Main content */} +
+
+
+ + } + text="Dashboard" + /> + + + } + className={css({ + width: "100%", + justifyContent: "center", + })} + /> + +
+ +
+

+ Connexion +

+

+ Connectez-vous à votre compte +

+
+ + , + text: "Se connecter", + className: css({ + width: "100%", + justifyContent: "center", + }), + }} + submitOnPressEnterKey={true} + onSubmit={async (data) => { + const response = await getResponseBodyFromAPI({ + routeDefinition: signInRouteDefinition, + body: data, + }) + if (response.ok === false) { + toast({ + title: "Connexion impossible", + variant: "error", + }) + return false + } + + toast({ + title: "Connexion réussie", + variant: "success", + }) + return true + }} + onCancel={undefined} + onSuccess={() => { + // Mirror backend auth cookie to avoid a transient undefined state during immediate redirect. + setCookie(`${cookiePrefix}_is_auth`, "true") + window.location.assign("/dashboard") + }} + > + {(form) => ( + + ( + + + + + + + + )} + /> + ( + + + + + + + + )} + /> + + )} + + + + + + + + +
+ + } + text="Créer un compte" + className={css({ + width: "100%", + justifyContent: "center", + })} + /> + +
+
+
+
+ ) +} diff --git a/packages/website/src/features/signIn/resetPasswordPage.tsx b/packages/website/src/features/signIn/resetPasswordPage.tsx deleted file mode 100644 index de40d9b8..00000000 --- a/packages/website/src/features/signIn/resetPasswordPage.tsx +++ /dev/null @@ -1,197 +0,0 @@ -import { resetPasswordRouteDefinition } from "@arrhes/application-metadata/routes" -import { ButtonGhostContent, InputText, Logo, toast } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconArrowLeft, IconBook2, IconMail } from "@tabler/icons-react" -import { FormControl } from "../../components/forms/formControl.js" -import { FormError } from "../../components/forms/formError.js" -import { FormField } from "../../components/forms/formField.js" -import { FormItem } from "../../components/forms/formItem.js" -import { FormLabel } from "../../components/forms/formLabel.js" -import { FormRoot } from "../../components/forms/formRoot.js" -import { LinkButton } from "../../components/linkButton.js" -import { getResponseBodyFromAPI } from "../../utilities/getResponseBodyFromAPI.js" - -export function ResetPasswordPage() { - return ( -
-
-
-
- - } - text="Dashboard" - /> - - - } - className={css({ - width: "100%", - justifyContent: "center", - })} - /> - -
- - - } - text="Retour à la connexion" - /> - - -
-

- Réinitialiser le mot de passe -

-

- Saisissez votre email pour recevoir un nouveau mot de passe temporaire. -

-
- - , - text: "Recevoir un nouveau mot de passe", - className: css({ - width: "100%", - justifyContent: "center", - }), - }} - submitOnPressEnterKey={true} - onSubmit={async (data) => { - const response = await getResponseBodyFromAPI({ - routeDefinition: resetPasswordRouteDefinition, - body: data, - }) - - if (response.ok === false) { - toast({ - title: response.error?.cause ?? "Réinitialisation impossible", - variant: "error", - }) - return false - } - - toast({ - title: "Un nouveau mot de passe vous a été envoyé par email", - description: "Pensez à le modifier dès votre prochaine connexion.", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={() => { - window.location.assign("/connexion") - }} - > - {(form) => ( - ( - - - - - - - - )} - /> - )} - - -

- Après connexion, modifiez ce mot de passe temporaire depuis votre profil pour sécuriser votre - compte. -

-
-
-
- ) -} diff --git a/packages/website/src/features/signIn/signInPage.tsx b/packages/website/src/features/signIn/signInPage.tsx deleted file mode 100644 index 626fea5b..00000000 --- a/packages/website/src/features/signIn/signInPage.tsx +++ /dev/null @@ -1,245 +0,0 @@ -import { signInRouteDefinition } from "@arrhes/application-metadata/routes" -import { ButtonGhostContent, ButtonOutlineContent, InputPassword, InputText, Logo, Separator, toast } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconBook2, IconLogin2, IconUserPlus } from "@tabler/icons-react" -import { Fragment } from "react/jsx-runtime" -import { FormControl } from "../../components/forms/formControl.js" -import { FormError } from "../../components/forms/formError.js" -import { FormField } from "../../components/forms/formField.js" -import { FormItem } from "../../components/forms/formItem.js" -import { FormLabel } from "../../components/forms/formLabel.js" -import { FormRoot } from "../../components/forms/formRoot.js" -import { LinkButton } from "../../components/linkButton.js" -import { setCookie } from "../../utilities/cookies/setCookie.js" -import { getResponseBodyFromAPI } from "../../utilities/getResponseBodyFromAPI.js" -import { cookiePrefix } from "../../utilities/variables.js" - -export function SignInPage() { - return ( -
- {/* Main content */} -
-
-
- - } - text="Dashboard" - /> - - - } - className={css({ - width: "100%", - justifyContent: "center", - })} - /> - -
- -
-

- Connexion -

-

- Connectez-vous à votre compte -

-
- - , - text: "Se connecter", - className: css({ - width: "100%", - justifyContent: "center", - }), - }} - submitOnPressEnterKey={true} - onSubmit={async (data) => { - const response = await getResponseBodyFromAPI({ - routeDefinition: signInRouteDefinition, - body: data, - }) - if (response.ok === false) { - toast({ - title: "Connexion impossible", - variant: "error", - }) - return false - } - - toast({ - title: "Connexion réussie", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={() => { - // Mirror backend auth cookie to avoid a transient undefined state during immediate redirect. - setCookie(`${cookiePrefix}_is_auth`, "true") - window.location.assign("/dashboard") - }} - > - {(form) => ( - - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - - )} - - - - - - - - -
- - } - text="Créer un compte" - className={css({ - width: "100%", - justifyContent: "center", - })} - /> - -
-
-
-
- ) -} diff --git a/packages/website/src/features/signUp/SignUpPage.tsx b/packages/website/src/features/signUp/SignUpPage.tsx new file mode 100644 index 00000000..8ce03347 --- /dev/null +++ b/packages/website/src/features/signUp/SignUpPage.tsx @@ -0,0 +1,260 @@ +import { signUpRouteDefinition } from "@arrhes/application-metadata/routes" +import { ButtonGhostContent, ButtonOutlineContent, InputPassword, InputText, Logo, Separator, toast } from "@arrhes/ui" +import { css } from "@arrhes/ui/utilities/cn.js" +import { IconBook2, IconLogin2, IconUserPlus } from "@tabler/icons-react" +import { Fragment } from "react/jsx-runtime" +import { FormControl } from "../../components/forms/FormControl.js" +import { FormError } from "../../components/forms/FormError.js" +import { FormField } from "../../components/forms/FormField.js" +import { FormItem } from "../../components/forms/FormItem.js" +import { FormLabel } from "../../components/forms/FormLabel.js" +import { FormRoot } from "../../components/forms/FormRoot.js" +import { LinkButton } from "../../components/LinkButton.js" +import { setCookie } from "../../utilities/cookies/setCookie.js" +import { getResponseBodyFromAPI } from "../../utilities/getResponseBodyFromAPI.js" +import { cookiePrefix } from "../../utilities/variables.js" + +export function SignUpPage() { + return ( +
+ {/* Main content */} +
+
+
+ + } + text="Dashboard" + /> + + + } + className={css({ + width: "100%", + justifyContent: "center", + })} + /> + +
+ +
+

+ Inscription +

+

+ Créez votre compte gratuitement +

+
+ + , + text: "Créer un compte", + className: css({ + width: "100%", + justifyContent: "center", + }), + }} + submitOnPressEnterKey={true} + onSubmit={async (data) => { + if (data.password !== data.passwordCheck) { + toast({ + title: "Les mots de passe ne correspondent pas", + variant: "error", + }) + return false + } + + const response = await getResponseBodyFromAPI({ + routeDefinition: signUpRouteDefinition, + body: data, + }) + if (!response.ok) { + toast({ + title: "Inscription impossible", + variant: "error", + }) + return false + } + + toast({ + title: "Inscription réussie", + variant: "success", + }) + return true + }} + onCancel={undefined} + onSuccess={() => { + // Mirror backend auth cookie to avoid a transient undefined state during immediate redirect. + setCookie(`${cookiePrefix}_is_auth`, "true") + window.location.assign("/dashboard") + }} + > + {(form) => ( + + ( + + + + + + + + )} + /> + ( + + + + + + + + )} + /> + ( + + + + + + + + )} + /> + + )} + + + + +
+ + } + text="Se connecter" + className={css({ + width: "100%", + justifyContent: "center", + })} + /> + +
+
+
+
+ ) +} diff --git a/packages/website/src/features/signUp/signUpPage.tsx b/packages/website/src/features/signUp/signUpPage.tsx deleted file mode 100644 index 19195609..00000000 --- a/packages/website/src/features/signUp/signUpPage.tsx +++ /dev/null @@ -1,260 +0,0 @@ -import { signUpRouteDefinition } from "@arrhes/application-metadata/routes" -import { ButtonGhostContent, ButtonOutlineContent, InputPassword, InputText, Logo, Separator, toast } from "@arrhes/ui" -import { css } from "@arrhes/ui/utilities/cn.js" -import { IconBook2, IconLogin2, IconUserPlus } from "@tabler/icons-react" -import { Fragment } from "react/jsx-runtime" -import { FormControl } from "../../components/forms/formControl.js" -import { FormError } from "../../components/forms/formError.js" -import { FormField } from "../../components/forms/formField.js" -import { FormItem } from "../../components/forms/formItem.js" -import { FormLabel } from "../../components/forms/formLabel.js" -import { FormRoot } from "../../components/forms/formRoot.js" -import { LinkButton } from "../../components/linkButton.js" -import { setCookie } from "../../utilities/cookies/setCookie.js" -import { getResponseBodyFromAPI } from "../../utilities/getResponseBodyFromAPI.js" -import { cookiePrefix } from "../../utilities/variables.js" - -export function SignUpPage() { - return ( -
- {/* Main content */} -
-
-
- - } - text="Dashboard" - /> - - - } - className={css({ - width: "100%", - justifyContent: "center", - })} - /> - -
- -
-

- Inscription -

-

- Créez votre compte gratuitement -

-
- - , - text: "Créer un compte", - className: css({ - width: "100%", - justifyContent: "center", - }), - }} - submitOnPressEnterKey={true} - onSubmit={async (data) => { - if (data.password !== data.passwordCheck) { - toast({ - title: "Les mots de passe ne correspondent pas", - variant: "error", - }) - return false - } - - const response = await getResponseBodyFromAPI({ - routeDefinition: signUpRouteDefinition, - body: data, - }) - if (!response.ok) { - toast({ - title: "Inscription impossible", - variant: "error", - }) - return false - } - - toast({ - title: "Inscription réussie", - variant: "success", - }) - return true - }} - onCancel={undefined} - onSuccess={() => { - // Mirror backend auth cookie to avoid a transient undefined state during immediate redirect. - setCookie(`${cookiePrefix}_is_auth`, "true") - window.location.assign("/dashboard") - }} - > - {(form) => ( - - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - - )} - - - - -
- - } - text="Se connecter" - className={css({ - width: "100%", - justifyContent: "center", - })} - /> - -
-
-
-
- ) -} diff --git a/packages/website/src/root.tsx b/packages/website/src/root.tsx index c6007d0b..6de177b4 100644 --- a/packages/website/src/root.tsx +++ b/packages/website/src/root.tsx @@ -1,15 +1,11 @@ import { StrictMode } from "react" import { createRoot } from "react-dom/client" import "./assets/css/root.css" -import { RootProvider } from "./contexts/rootProvider.js" +import { RootProvider } from "./contexts/RootProvider.js" -const rootElement = document.getElementById("root")! -if (!rootElement.innerHTML) { - localStorage.theme = "light" - const root = createRoot(rootElement) - root.render( - - - , - ) -} +localStorage.theme = "light" +createRoot(document.getElementById("root")!).render( + + + , +) diff --git a/packages/website/src/routes/applicationTree.ts b/packages/website/src/routes/applicationTree.ts index 705c144b..63f4c1c5 100644 --- a/packages/website/src/routes/applicationTree.ts +++ b/packages/website/src/routes/applicationTree.ts @@ -1,6 +1,7 @@ import type { AnyRoute } from "@tanstack/react-router" import { catchRoute } from "./catchRoute.js" -import { dashboardTree } from "./root/dashboard/dashboardTree.js" +import { cliRoute } from "./root/cliRoute.js" +import { fullDashboardTree } from "./root/dashboard/fullDashboardTree.js" import { docsTree } from "./root/docs/docsTree.js" import { homeLayoutRoute } from "./root/home/homeLayoutRoute.js" import { homeRootRoute } from "./root/home/homeRootRoute.js" @@ -15,10 +16,11 @@ export const applicationTree: AnyRoute = rootLayoutRoute.addChildren([ ]), docsTree, + cliRoute, signUpRoute, signInRoute, resetPasswordRoute, - dashboardTree, + fullDashboardTree, catchRoute, ]) diff --git a/packages/website/src/routes/catchRoute.tsx b/packages/website/src/routes/catchRoute.tsx index dd421fac..6c95f28f 100644 --- a/packages/website/src/routes/catchRoute.tsx +++ b/packages/website/src/routes/catchRoute.tsx @@ -9,5 +9,5 @@ export const catchRoute = createRoute({ description: "La page que vous recherchez n'existe pas ou a été déplacée.", robots: "noindex", }), - component: lazyRouteComponent(() => import("../features/notFound/notFoundPage.js"), "NotFoundPage"), + component: lazyRouteComponent(() => import("../features/notFound/NotFoundPage.js"), "NotFoundPage"), }) diff --git a/packages/website/src/routes/root/cliRoute.tsx b/packages/website/src/routes/root/cliRoute.tsx new file mode 100644 index 00000000..7af43ec3 --- /dev/null +++ b/packages/website/src/routes/root/cliRoute.tsx @@ -0,0 +1,12 @@ +import { createRoute, redirect } from "@tanstack/react-router" +import { rootLayoutRoute } from "../rootLayoutRoute.js" + +export const cliRoute = createRoute({ + getParentRoute: () => rootLayoutRoute, + path: "/cli", + beforeLoad: () => { + throw redirect({ + href: "https://github.com/arrhes/application/releases/latest", + }) + }, +}) diff --git a/packages/website/src/routes/root/dashboard/admin/adminLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/admin/adminLayoutRoute.tsx deleted file mode 100644 index b4c40924..00000000 --- a/packages/website/src/routes/root/dashboard/admin/adminLayoutRoute.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { createRoute, lazyRouteComponent, redirect } from "@tanstack/react-router" -import { dashboardLayoutRoute } from "../dashboardLayoutRoute.js" - -export const adminLayoutRoute = createRoute({ - getParentRoute: () => dashboardLayoutRoute, - path: "/admin", - beforeLoad: async ({ context }) => { - const userSession = await context.userSession - - if (userSession?.user.isSuperAdmin !== true) { - throw redirect({ - to: "/dashboard", - }) - } - - return { - title: "Admin", - } - }, - component: lazyRouteComponent( - () => import("../../../../features/dashboard/admin/adminPanelLayout.js"), - "AdminPanelLayout", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/admin/adminRoute.tsx b/packages/website/src/routes/root/dashboard/admin/adminRoute.tsx deleted file mode 100644 index 09200477..00000000 --- a/packages/website/src/routes/root/dashboard/admin/adminRoute.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { createRoute, redirect } from "@tanstack/react-router" -import { adminLayoutRoute } from "./adminLayoutRoute.js" - -export const adminRoute = createRoute({ - getParentRoute: () => adminLayoutRoute, - path: "/", - beforeLoad: () => { - throw redirect({ - to: "/dashboard/admin/tickets", - }) - }, -}) diff --git a/packages/website/src/routes/root/dashboard/admin/tickets/$idTicket/$idTicketLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/admin/tickets/$idTicket/$idTicketLayoutRoute.tsx deleted file mode 100644 index c4898cf3..00000000 --- a/packages/website/src/routes/root/dashboard/admin/tickets/$idTicket/$idTicketLayoutRoute.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { CircularLoader } from "@arrhes/ui" -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { adminTicketsLayoutRoute } from "../ticketsLayoutRoute.js" - -export const $idTicketLayoutRoute = createRoute({ - getParentRoute: () => adminTicketsLayoutRoute, - path: "/$idTicket", - pendingComponent: () => , - beforeLoad: () => ({ - title: "Ticket", - }), - component: lazyRouteComponent( - () => import("../../../../../../features/dashboard/admin/tickets/$idTicket/ticketLayout.js"), - "TicketLayout", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/admin/tickets/$idTicket/$idTicketRoute.tsx b/packages/website/src/routes/root/dashboard/admin/tickets/$idTicket/$idTicketRoute.tsx deleted file mode 100644 index 2e39cff3..00000000 --- a/packages/website/src/routes/root/dashboard/admin/tickets/$idTicket/$idTicketRoute.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { $idTicketLayoutRoute } from "./$idTicketLayoutRoute.js" - -export const $idTicketRoute = createRoute({ - getParentRoute: () => $idTicketLayoutRoute, - path: "/", - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent( - () => import("../../../../../../features/dashboard/admin/tickets/$idTicket/ticketPage.js"), - "TicketPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/admin/tickets/ticketsLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/admin/tickets/ticketsLayoutRoute.tsx deleted file mode 100644 index 5680c075..00000000 --- a/packages/website/src/routes/root/dashboard/admin/tickets/ticketsLayoutRoute.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { createRoute, Outlet } from "@tanstack/react-router" -import { adminLayoutRoute } from "../adminLayoutRoute.js" - -export const adminTicketsLayoutRoute = createRoute({ - getParentRoute: () => adminLayoutRoute, - path: "/tickets", - component: () => , -}) diff --git a/packages/website/src/routes/root/dashboard/admin/tickets/ticketsRoute.tsx b/packages/website/src/routes/root/dashboard/admin/tickets/ticketsRoute.tsx deleted file mode 100644 index 4cb438d5..00000000 --- a/packages/website/src/routes/root/dashboard/admin/tickets/ticketsRoute.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { CircularLoader } from "@arrhes/ui" -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { adminTicketsLayoutRoute } from "./ticketsLayoutRoute.js" - -export const adminTicketsRoute = createRoute({ - getParentRoute: () => adminTicketsLayoutRoute, - path: "/", - pendingComponent: () => , - beforeLoad: () => ({ - title: "Tickets", - }), - component: lazyRouteComponent( - () => import("../../../../../features/dashboard/admin/tickets/ticketsPage.js"), - "TicketsPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/agent/agentLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/agent/agentLayoutRoute.tsx deleted file mode 100644 index ed1a9aae..00000000 --- a/packages/website/src/routes/root/dashboard/agent/agentLayoutRoute.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { organizationLayoutRoute } from "../organizations/$idOrganization/organizationLayoutRoute.js" - -export const agentLayoutRoute = createRoute({ - getParentRoute: () => organizationLayoutRoute, - path: "/agent", - beforeLoad: () => ({ - title: "Assistant", - }), - component: lazyRouteComponent( - () => import("../../../../features/dashboard/$idOrganization/agent/agentLayout.js"), - "AgentLayout", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/agent/agentRoute.tsx b/packages/website/src/routes/root/dashboard/agent/agentRoute.tsx deleted file mode 100644 index 069bf17b..00000000 --- a/packages/website/src/routes/root/dashboard/agent/agentRoute.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { agentLayoutRoute } from "./agentLayoutRoute.js" - -export const agentRoute = createRoute({ - getParentRoute: () => agentLayoutRoute, - path: "/", - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent( - () => import("../../../../features/dashboard/$idOrganization/agent/agentPage.js"), - "AgentPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/agent/agentSessionRoute.tsx b/packages/website/src/routes/root/dashboard/agent/agentSessionRoute.tsx deleted file mode 100644 index 8fdc75e0..00000000 --- a/packages/website/src/routes/root/dashboard/agent/agentSessionRoute.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { createRoute, useParams } from "@tanstack/react-router" -import { lazy, Suspense } from "react" -import { agentSessionsLayoutRoute } from "./agentSessionsLayoutRoute.js" - -const AgentSessionPage = lazy(() => - import("../../../../features/dashboard/$idOrganization/agent/agentSessionPage.js").then((m) => ({ - default: m.AgentSessionPage, - })), -) - -// Wrapper that keys AgentSessionPage on idAgentSession so the component -// fully remounts when the user switches sessions — resetting all state/refs. -function AgentSessionPageKeyed() { - const { idAgentSession } = useParams({ - from: agentSessionRoute.id, - }) - return ( - - - - ) -} - -export const agentSessionRoute = createRoute({ - getParentRoute: () => agentSessionsLayoutRoute, - path: "/$idAgentSession", - beforeLoad: () => ({ - title: undefined, - }), - component: AgentSessionPageKeyed, -}) diff --git a/packages/website/src/routes/root/dashboard/agent/agentSessionsLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/agent/agentSessionsLayoutRoute.tsx deleted file mode 100644 index 2a9fab9a..00000000 --- a/packages/website/src/routes/root/dashboard/agent/agentSessionsLayoutRoute.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { createRoute } from "@tanstack/react-router" -import { agentLayoutRoute } from "./agentLayoutRoute.js" - -export const agentSessionsLayoutRoute = createRoute({ - getParentRoute: () => agentLayoutRoute, - path: "/sessions", -}) diff --git a/packages/website/src/routes/root/dashboard/dashboardCatchRoute.tsx b/packages/website/src/routes/root/dashboard/dashboardCatchRoute.tsx index 737ea11e..490ad689 100644 --- a/packages/website/src/routes/root/dashboard/dashboardCatchRoute.tsx +++ b/packages/website/src/routes/root/dashboard/dashboardCatchRoute.tsx @@ -8,7 +8,7 @@ export const dashboardCatchRoute = createRoute({ title: "Page introuvable", }), component: lazyRouteComponent( - () => import("../../../features/dashboard/dashboardNotFoundPage.js"), + () => import("../../../features/dashboard/DashboardNotFoundPage.js"), "DashboardNotFoundPage", ), }) diff --git a/packages/website/src/routes/root/dashboard/dashboardLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/dashboardLayoutRoute.tsx index 170caf31..16c3f6dd 100644 --- a/packages/website/src/routes/root/dashboard/dashboardLayoutRoute.tsx +++ b/packages/website/src/routes/root/dashboard/dashboardLayoutRoute.tsx @@ -21,5 +21,8 @@ export const dashboardLayoutRoute = createRoute({ robots: "noindex, nofollow", } }, - component: lazyRouteComponent(() => import("../../../features/dashboard/dashboardLayout.js"), "DashboardLayout"), + component: lazyRouteComponent( + () => import("../../../features/dashboard/dashboardLayout/DashboardLayout.js"), + "DashboardLayout", + ), }) diff --git a/packages/website/src/routes/root/dashboard/dashboardRootRoute.tsx b/packages/website/src/routes/root/dashboard/dashboardRootRoute.tsx index d5d9f11c..b1abb5b6 100644 --- a/packages/website/src/routes/root/dashboard/dashboardRootRoute.tsx +++ b/packages/website/src/routes/root/dashboard/dashboardRootRoute.tsx @@ -1,24 +1,9 @@ -import { createRoute, redirect } from "@tanstack/react-router" -import { getCookie } from "../../../utilities/cookies/getCookie.js" -import { cookiePrefix } from "../../../utilities/variables.js" +import { createRoute } from "@tanstack/react-router" +import { DashboardDefaultPage } from "../../../features/dashboard/DashboardDefaultPage.js" import { dashboardLayoutRoute } from "./dashboardLayoutRoute.js" export const dashboardRootRoute = createRoute({ getParentRoute: () => dashboardLayoutRoute, path: "/", - beforeLoad: () => { - const storedOrganizationId = getCookie(`${cookiePrefix}_id_organization`) - if (storedOrganizationId) { - throw redirect({ - to: "/dashboard/organisations/$idOrganization", - params: { - idOrganization: storedOrganizationId, - }, - }) - } - throw redirect({ - to: "/dashboard/organisations", - }) - }, - component: () => {}, + component: DashboardDefaultPage, }) diff --git a/packages/website/src/routes/root/dashboard/dashboardTabHistoryRoute.tsx b/packages/website/src/routes/root/dashboard/dashboardTabHistoryRoute.tsx new file mode 100644 index 00000000..7b423ab8 --- /dev/null +++ b/packages/website/src/routes/root/dashboard/dashboardTabHistoryRoute.tsx @@ -0,0 +1,9 @@ +import { createRoute } from "@tanstack/react-router" +import { dashboardLayoutRoute } from "./dashboardLayoutRoute.tsx" + +// Matches /dashboard/:tabId/:historyIndex — pushed by the tab system on each +// in-tab navigation step so browser back/forward traverses tab history. +export const dashboardTabHistoryRoute = createRoute({ + getParentRoute: () => dashboardLayoutRoute, + path: "/$tabId/$historyIndex", +}) diff --git a/packages/website/src/routes/root/dashboard/dashboardTabRoute.tsx b/packages/website/src/routes/root/dashboard/dashboardTabRoute.tsx new file mode 100644 index 00000000..de0ccfec --- /dev/null +++ b/packages/website/src/routes/root/dashboard/dashboardTabRoute.tsx @@ -0,0 +1,9 @@ +import { createRoute } from "@tanstack/react-router" +import { dashboardLayoutRoute } from "./dashboardLayoutRoute.tsx" + +// This route exists so that /dashboard/:tabId URLs are valid in the outer router. +// The DashboardLayout (parent) handles all rendering — no component needed here. +export const dashboardTabRoute = createRoute({ + getParentRoute: () => dashboardLayoutRoute, + path: "/$tabId", +}) diff --git a/packages/website/src/routes/root/dashboard/dashboardTree.ts b/packages/website/src/routes/root/dashboard/dashboardTree.ts deleted file mode 100644 index bfbc5d91..00000000 --- a/packages/website/src/routes/root/dashboard/dashboardTree.ts +++ /dev/null @@ -1,263 +0,0 @@ -import type { AnyRoute } from "@tanstack/react-router" -import { adminLayoutRoute } from "./admin/adminLayoutRoute.tsx" -import { adminRoute } from "./admin/adminRoute.tsx" -import { $idTicketLayoutRoute } from "./admin/tickets/$idTicket/$idTicketLayoutRoute.tsx" -import { $idTicketRoute } from "./admin/tickets/$idTicket/$idTicketRoute.tsx" -import { adminTicketsLayoutRoute } from "./admin/tickets/ticketsLayoutRoute.tsx" -import { adminTicketsRoute } from "./admin/tickets/ticketsRoute.tsx" -import { agentLayoutRoute } from "./agent/agentLayoutRoute.tsx" -import { agentRoute } from "./agent/agentRoute.tsx" -import { agentSessionRoute } from "./agent/agentSessionRoute.tsx" -import { agentSessionsLayoutRoute } from "./agent/agentSessionsLayoutRoute.tsx" -import { dashboardCatchRoute } from "./dashboardCatchRoute.tsx" -import { dashboardLayoutRoute } from "./dashboardLayoutRoute.tsx" -import { dashboardRootRoute } from "./dashboardRootRoute.tsx" -import { organizationApiKeysRoute } from "./organizations/$idOrganization/organizationApi/organizationApiKeysRoute.tsx" -import { organizationApiLayoutRoute } from "./organizations/$idOrganization/organizationApi/organizationApiLayoutRoute.tsx" -import { organizationApiRoute } from "./organizations/$idOrganization/organizationApi/organizationApiRoute.tsx" -import { organizationBillingHistoryRoute } from "./organizations/$idOrganization/organizationBilling/organizationBillingHistoryRoute.tsx" -import { organizationBillingLayoutRoute } from "./organizations/$idOrganization/organizationBilling/organizationBillingLayoutRoute.tsx" -import { organizationBillingRoute } from "./organizations/$idOrganization/organizationBilling/organizationBillingRoute.tsx" -import { organizationServicesRoute } from "./organizations/$idOrganization/organizationBilling/organizationBillingsRoute.tsx" -import { organizationInvoicesRoute } from "./organizations/$idOrganization/organizationBilling/organizationInvoicesRoute.tsx" -import { organizationInvoiceUblRoute } from "./organizations/$idOrganization/organizationBilling/organizationInvoiceUblRoute.tsx" -import { organizationLayoutRoute } from "./organizations/$idOrganization/organizationLayoutRoute.tsx" -import { organizationPathRoute } from "./organizations/$idOrganization/organizationPathRoute.tsx" -import { organizationRoute } from "./organizations/$idOrganization/organizationRoute.tsx" -import { organizationSecurityRoute } from "./organizations/$idOrganization/organizationSettings/organizationSecurityRoute.tsx" -import { organizationSettingsLayoutRoute } from "./organizations/$idOrganization/organizationSettings/organizationSettingsLayoutRoute.tsx" -import { organizationSettingsRoute } from "./organizations/$idOrganization/organizationSettings/organizationSettingsRoute.tsx" -import { organizationUsersLayoutRoute } from "./organizations/$idOrganization/organizationUsers/organizationUsersLayoutRoute.tsx" -import { organizationUsersRoute } from "./organizations/$idOrganization/organizationUsers/organizationUsersRoute.tsx" -import { entryLineLayoutRoute } from "./organizations/$idOrganization/years/$idYear/entries/$idEntry/$idEntryLine/entryLineLayoutRoute.tsx" -import { entryLineMetadataRoute } from "./organizations/$idOrganization/years/$idYear/entries/$idEntry/$idEntryLine/entryLineMetadataRoute.tsx" -import { entryLineRoute } from "./organizations/$idOrganization/years/$idYear/entries/$idEntry/$idEntryLine/entryLineRoute.tsx" -import { entryCategoriesRoute } from "./organizations/$idOrganization/years/$idYear/entries/$idEntry/entryCategoriesRoute.tsx" -import { entryLayoutRoute } from "./organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLayoutRoute.tsx" -import { entryLinesRoute } from "./organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLinesRoute.tsx" -import { entryMetadataRoute } from "./organizations/$idOrganization/years/$idYear/entries/$idEntry/entryMetadataRoute.tsx" -import { entryRoute } from "./organizations/$idOrganization/years/$idYear/entries/$idEntry/entryRoute.tsx" -import { entriesLayoutRoute } from "./organizations/$idOrganization/years/$idYear/entries/entriesLayoutRoute.tsx" -import { entriesRoute } from "./organizations/$idOrganization/years/$idYear/entries/entriesRoute.tsx" -import { fileLayoutRoute } from "./organizations/$idOrganization/years/$idYear/files/$idFile/fileLayoutRoute.tsx" -import { fileMetadataRoute } from "./organizations/$idOrganization/years/$idYear/files/$idFile/fileMetadataRoute.tsx" -import { fileRoute } from "./organizations/$idOrganization/years/$idYear/files/$idFile/fileRoute.tsx" -import { fileVisualisationRoute } from "./organizations/$idOrganization/years/$idYear/files/$idFile/fileVisualisationRoute.tsx" -import { filesLayoutRoute } from "./organizations/$idOrganization/years/$idYear/files/filesLayoutRoute.tsx" -import { filesRoute } from "./organizations/$idOrganization/years/$idYear/files/filesRoute.tsx" -import { balanceReportRoute } from "./organizations/$idOrganization/years/$idYear/reports/balanceReportRoute.tsx" -import { balanceSheetReportRoute } from "./organizations/$idOrganization/years/$idYear/reports/balanceSheetReportRoute.tsx" -import { incomeStatementReportRoute } from "./organizations/$idOrganization/years/$idYear/reports/incomeStatementReportRoute.tsx" -import { journalReportRoute } from "./organizations/$idOrganization/years/$idYear/reports/journalReportRoute.tsx" -import { ledgerReportRoute } from "./organizations/$idOrganization/years/$idYear/reports/ledgerReportRoute.tsx" -import { reportsLayoutRoute } from "./organizations/$idOrganization/years/$idYear/reports/reportsLayoutRoute.tsx" -import { reportsRoute } from "./organizations/$idOrganization/years/$idYear/reports/reportsRoute.tsx" -import { yearLayoutRoute } from "./organizations/$idOrganization/years/$idYear/yearLayoutRoute.tsx" -import { yearPathRoute } from "./organizations/$idOrganization/years/$idYear/yearPathRoute.tsx" -import { yearRoute } from "./organizations/$idOrganization/years/$idYear/yearRoute.tsx" -import { accountLayoutRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/accountLayoutRoute.tsx" -import { accountMetadataRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/accountMetadataRoute.tsx" -import { accountRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/accountRoute.tsx" -import { accountsLayoutRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/accounts/accountsLayoutRoute.tsx" -import { accountsRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/accounts/accountsRoute.tsx" -import { balanceSheetLayoutRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/balanceSheetLayoutRoute.tsx" -import { balanceSheetMetadataRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/balanceSheetMetadataRoute.tsx" -import { balanceSheetRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/balanceSheetRoute.tsx" -import { actifLayoutRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/actif/actifLayoutRoute.tsx" -import { actifRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/actif/actifRoute.tsx" -import { balanceSheetsLayoutRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/balanceSheetsLayoutRoute.tsx" -import { balanceSheetsRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/balanceSheetsRoute.tsx" -import { passifLayoutRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/passif/passifLayoutRoute.tsx" -import { passifRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/passif/passifRoute.tsx" -import { incomeStatementLayoutRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/incomeStatementLayoutRoute.tsx" -import { incomeStatementMetadataRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/incomeStatementMetadataRoute.tsx" -import { incomeStatementRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/incomeStatementRoute.tsx" -import { computationIncomeStatementLayoutRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/$idComputationIncomeStatement/computationIncomeStatementLayoutRoute.tsx" -import { computationIncomeStatementMetadataRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/$idComputationIncomeStatement/computationIncomeStatementMetadataRoute.tsx" -import { computationIncomeStatementRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/$idComputationIncomeStatement/computationIncomeStatementRoute.tsx" -import { computationLayoutRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationLayoutRoute.tsx" -import { computationMetadataRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationMetadataRoute.tsx" -import { computationPostesRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationPostesRoute.tsx" -import { computationRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationRoute.tsx" -import { computationsLayoutRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/computationsLayoutRoute.tsx" -import { computationsRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/computationsRoute.tsx" -import { incomeStatementsLayoutRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/incomeStatementsLayoutRoute.tsx" -import { incomeStatementsRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/incomeStatementsRoute.tsx" -import { journalLayoutRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/journalLayoutRoute.tsx" -import { journalMetadataRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/journalMetadataRoute.tsx" -import { journalRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/journalRoute.tsx" -import { journalsLayoutRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/journals/journalsLayoutRoute.tsx" -import { journalsRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/journals/journalsRoute.tsx" -import { tagLayoutRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/tagLayoutRoute.tsx" -import { tagMetadataRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/tagMetadataRoute.tsx" -import { tagRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/tagRoute.tsx" -import { tagsLayoutRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/tags/tagsLayoutRoute.tsx" -import { tagsRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/tags/tagsRoute.tsx" -import { yearSettingsLayoutRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/yearSettingsLayoutRoute.tsx" -import { yearSettingsRoute } from "./organizations/$idOrganization/years/$idYear/yearSettings/yearSettingsRoute.tsx" -import { yearsLayoutRoute } from "./organizations/$idOrganization/years/yearsLayoutRoute.tsx" -import { yearsPathRoute } from "./organizations/$idOrganization/years/yearsPathRoute.tsx" -import { yearsRoute } from "./organizations/$idOrganization/years/yearsRoute.tsx" -import { organizationsLayoutRoute } from "./organizations/organizationsLayoutRoute.tsx" -import { organizationsRoute } from "./organizations/organizationsRoute.tsx" -import { userProfileLayoutRoute } from "./profile/userProfileLayoutRoute.tsx" -import { userProfileRoute } from "./profile/userProfileRoute.tsx" -import { settingsLayoutRoute } from "./settings/settingsLayoutRoute.tsx" -import { settingsRoute } from "./settings/settingsRoute.tsx" -import { supportLayoutRoute } from "./support/supportLayoutRoute.tsx" -import { supportRoute } from "./support/supportRoute.tsx" -import { ticketLayoutRoute } from "./support/ticketLayoutRoute.tsx" -import { ticketRoute } from "./support/ticketRoute.tsx" - -export const dashboardTree: AnyRoute = dashboardLayoutRoute.addChildren([ - dashboardRootRoute, - adminLayoutRoute.addChildren([ - adminRoute, - adminTicketsLayoutRoute.addChildren([ - adminTicketsRoute, - $idTicketLayoutRoute.addChildren([ - $idTicketRoute, - ]), - ]), - ]), - organizationsLayoutRoute.addChildren([ - organizationsRoute, - organizationPathRoute.addChildren([ - organizationLayoutRoute.addChildren([ - organizationRoute, - yearsLayoutRoute.addChildren([ - yearsRoute, - ]), - organizationUsersLayoutRoute.addChildren([ - organizationUsersRoute, - ]), - organizationApiLayoutRoute.addChildren([ - organizationApiRoute, - organizationApiKeysRoute, - ]), - organizationBillingLayoutRoute.addChildren([ - organizationBillingRoute, - organizationServicesRoute, - organizationBillingHistoryRoute, - organizationInvoicesRoute, - organizationInvoiceUblRoute, - ]), - organizationSettingsLayoutRoute.addChildren([ - organizationSettingsRoute, - organizationSecurityRoute, - ]), - agentLayoutRoute.addChildren([ - agentRoute, - agentSessionsLayoutRoute.addChildren([ - agentSessionRoute, - ]), - ]), - ]), - yearsPathRoute.addChildren([ - yearPathRoute.addChildren([ - yearLayoutRoute.addChildren([ - yearRoute, - entriesLayoutRoute.addChildren([ - entriesRoute, - entryLayoutRoute.addChildren([ - entryRoute, - entryLinesRoute, - entryCategoriesRoute, - entryMetadataRoute, - ]), - entryLineLayoutRoute.addChildren([ - entryLineRoute, - entryLineMetadataRoute, - ]), - ]), - filesLayoutRoute.addChildren([ - filesRoute, - fileLayoutRoute.addChildren([ - fileRoute, - fileMetadataRoute, - fileVisualisationRoute, - ]), - ]), - reportsLayoutRoute.addChildren([ - reportsRoute, - journalReportRoute, - ledgerReportRoute, - balanceReportRoute, - balanceSheetReportRoute, - incomeStatementReportRoute, - ]), - yearSettingsLayoutRoute.addChildren([ - yearSettingsRoute, - accountsLayoutRoute.addChildren([ - accountsRoute, - accountLayoutRoute.addChildren([ - accountRoute, - accountMetadataRoute, - ]), - ]), - journalsLayoutRoute.addChildren([ - journalsRoute, - journalLayoutRoute.addChildren([ - journalRoute, - journalMetadataRoute, - ]), - ]), - tagsLayoutRoute.addChildren([ - tagsRoute, - tagLayoutRoute.addChildren([ - tagRoute, - tagMetadataRoute, - ]), - ]), - balanceSheetsLayoutRoute.addChildren([ - balanceSheetsRoute, - actifLayoutRoute.addChildren([ - actifRoute, - ]), - passifLayoutRoute.addChildren([ - passifRoute, - ]), - balanceSheetLayoutRoute.addChildren([ - balanceSheetRoute, - balanceSheetMetadataRoute, - ]), - ]), - incomeStatementsLayoutRoute.addChildren([ - incomeStatementsRoute, - incomeStatementLayoutRoute.addChildren([ - incomeStatementRoute, - incomeStatementMetadataRoute, - ]), - computationsLayoutRoute.addChildren([ - computationsRoute, - computationLayoutRoute.addChildren([ - computationRoute, - computationPostesRoute, - computationMetadataRoute, - ]), - computationIncomeStatementLayoutRoute.addChildren([ - computationIncomeStatementRoute, - computationIncomeStatementMetadataRoute, - ]), - ]), - ]), - ]), - ]), - ]), - ]), - ]), - ]), - userProfileLayoutRoute.addChildren([ - userProfileRoute, - ]), - settingsLayoutRoute.addChildren([ - settingsRoute, - ]), - supportLayoutRoute.addChildren([ - supportRoute, - ticketLayoutRoute.addChildren([ - ticketRoute, - ]), - ]), - dashboardCatchRoute, -]) diff --git a/packages/website/src/routes/root/dashboard/fullDashboardTree.ts b/packages/website/src/routes/root/dashboard/fullDashboardTree.ts new file mode 100644 index 00000000..090bf767 --- /dev/null +++ b/packages/website/src/routes/root/dashboard/fullDashboardTree.ts @@ -0,0 +1,15 @@ +import type { AnyRoute } from "@tanstack/react-router" +import { dashboardCatchRoute } from "./dashboardCatchRoute.js" +import { dashboardLayoutRoute } from "./dashboardLayoutRoute.js" +import { dashboardRootRoute } from "./dashboardRootRoute.js" +import { dashboardTabHistoryRoute } from "./dashboardTabHistoryRoute.js" +import { dashboardTabRoute } from "./dashboardTabRoute.js" + +// Only the tab-based URL pattern is needed. The inner per-tab memory routers +// handle all content routing. Classic mode has been removed. +export const fullDashboardTree: AnyRoute = dashboardLayoutRoute.addChildren([ + dashboardRootRoute, + dashboardTabRoute, + dashboardTabHistoryRoute, + dashboardCatchRoute, +]) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationApi/organizationApiKeysRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationApi/organizationApiKeysRoute.tsx deleted file mode 100644 index 48e405e9..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationApi/organizationApiKeysRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { organizationApiLayoutRoute } from "./organizationApiLayoutRoute.js" - -export const organizationApiKeysRoute = createRoute({ - getParentRoute: () => organizationApiLayoutRoute, - path: "/clés", - beforeLoad: () => ({ - title: "Clés", - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../features/dashboard/$idOrganization/organizationApi/keys/organizationApiKeysPage.js" - ), - "OrganizationApiKeysPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationApi/organizationApiLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationApi/organizationApiLayoutRoute.tsx deleted file mode 100644 index 06abae39..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationApi/organizationApiLayoutRoute.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { organizationLayoutRoute } from "../organizationLayoutRoute.tsx" - -export const organizationApiLayoutRoute = createRoute({ - getParentRoute: () => organizationLayoutRoute, - path: "/api", - beforeLoad: () => ({ - title: "API", - }), - component: lazyRouteComponent( - () => import("../../../../../../features/dashboard/$idOrganization/organizationApi/organizationApiLayout.js"), - "OrganizationApiLayout", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationApi/organizationApiRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationApi/organizationApiRoute.tsx deleted file mode 100644 index a50f7e1f..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationApi/organizationApiRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { organizationApiLayoutRoute } from "./organizationApiLayoutRoute.js" - -export const organizationApiRoute = createRoute({ - getParentRoute: () => organizationApiLayoutRoute, - path: "/", - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../features/dashboard/$idOrganization/organizationApi/organizationApiGeneralPage.js" - ), - "OrganizationApiGeneralPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationBilling/organizationBillingHistoryRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationBilling/organizationBillingHistoryRoute.tsx deleted file mode 100644 index fff4763c..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationBilling/organizationBillingHistoryRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { organizationBillingLayoutRoute } from "./organizationBillingLayoutRoute.js" - -export const organizationBillingHistoryRoute = createRoute({ - getParentRoute: () => organizationBillingLayoutRoute, - path: "/historique", - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../features/dashboard/$idOrganization/organizationBilling/organizationBillingHistoryPage.js" - ), - "OrganizationBillingHistoryPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationBilling/organizationBillingLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationBilling/organizationBillingLayoutRoute.tsx deleted file mode 100644 index 785241a3..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationBilling/organizationBillingLayoutRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { organizationLayoutRoute } from "../organizationLayoutRoute.js" - -export const organizationBillingLayoutRoute = createRoute({ - getParentRoute: () => organizationLayoutRoute, - path: "/facturation", - beforeLoad: () => ({ - title: "Facturation", - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../features/dashboard/$idOrganization/organizationBilling/organizationBillingLayout.js" - ), - "OrganizationBillingLayout", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationBilling/organizationBillingRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationBilling/organizationBillingRoute.tsx deleted file mode 100644 index 59cd923b..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationBilling/organizationBillingRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { organizationBillingLayoutRoute } from "./organizationBillingLayoutRoute.js" - -export const organizationBillingRoute = createRoute({ - getParentRoute: () => organizationBillingLayoutRoute, - path: "/", - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../features/dashboard/$idOrganization/organizationBilling/wallet/organizationWalletPage.js" - ), - "OrganizationWalletPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationBilling/organizationBillingsRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationBilling/organizationBillingsRoute.tsx deleted file mode 100644 index b9997597..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationBilling/organizationBillingsRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { organizationBillingLayoutRoute } from "./organizationBillingLayoutRoute.js" - -export const organizationServicesRoute = createRoute({ - getParentRoute: () => organizationBillingLayoutRoute, - path: "/services", - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../features/dashboard/$idOrganization/organizationBilling/organizationServicesPage.js" - ), - "OrganizationServicesPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationBilling/organizationInvoiceUblRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationBilling/organizationInvoiceUblRoute.tsx deleted file mode 100644 index 522f8c81..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationBilling/organizationInvoiceUblRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { organizationBillingLayoutRoute } from "./organizationBillingLayoutRoute.js" - -export const organizationInvoiceUblRoute = createRoute({ - getParentRoute: () => organizationBillingLayoutRoute, - path: "/facture/$idInvoice", - beforeLoad: () => ({ - title: "Facture XML", - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../features/dashboard/$idOrganization/organizationBilling/invoices/organizationInvoicePage.js" - ), - "OrganizationInvoicePage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationBilling/organizationInvoicesRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationBilling/organizationInvoicesRoute.tsx deleted file mode 100644 index f3a35bed..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationBilling/organizationInvoicesRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { organizationBillingLayoutRoute } from "./organizationBillingLayoutRoute.js" - -export const organizationInvoicesRoute = createRoute({ - getParentRoute: () => organizationBillingLayoutRoute, - path: "/factures", - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../features/dashboard/$idOrganization/organizationBilling/invoices/organizationInvoicesPage.js" - ), - "OrganizationInvoicesPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationLayoutRoute.tsx deleted file mode 100644 index f58b22b3..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationLayoutRoute.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { organizationPathRoute } from "./organizationPathRoute.js" - -export const organizationLayoutRoute = createRoute({ - getParentRoute: () => organizationPathRoute, - id: "organizationLayout", - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent( - () => import("../../../../../features/dashboard/$idOrganization/organizationLayout.js"), - "OrganizationLayout", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationPathRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationPathRoute.tsx deleted file mode 100644 index 1b4f50d3..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationPathRoute.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { createRoute } from "@tanstack/react-router" -import { dataClient } from "../../../../../contexts/data/queryClient.js" -import { getCookie } from "../../../../../utilities/cookies/getCookie.js" -import { setCookie } from "../../../../../utilities/cookies/setCookie.js" -import { cookiePrefix } from "../../../../../utilities/variables.js" -import { organizationsLayoutRoute } from "../organizationsLayoutRoute.tsx" - -export const organizationPathRoute = createRoute({ - getParentRoute: () => organizationsLayoutRoute, - path: "/$idOrganization", - beforeLoad: ({ params }) => { - const previousOrganizationId = getCookie(`${cookiePrefix}_id_organization`) - setCookie(`${cookiePrefix}_id_organization`, params.idOrganization) - - if (previousOrganizationId !== params.idOrganization) { - dataClient.removeQueries() - } - - return { - title: "Organisation", - } - }, -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationRoute.tsx deleted file mode 100644 index 00fa7660..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationRoute.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { createRoute, redirect } from "@tanstack/react-router" -import { organizationLayoutRoute } from "./organizationLayoutRoute.tsx" - -export const organizationRoute = createRoute({ - getParentRoute: () => organizationLayoutRoute, - path: "/", - beforeLoad: ({ params }) => { - throw redirect({ - to: "/dashboard/organisations/$idOrganization/exercices", - params: params, - }) - }, -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationSettings/organizationSecurityRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationSettings/organizationSecurityRoute.tsx deleted file mode 100644 index 196f41f3..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationSettings/organizationSecurityRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { organizationSettingsLayoutRoute } from "./organizationSettingsLayoutRoute.js" - -export const organizationSecurityRoute = createRoute({ - getParentRoute: () => organizationSettingsLayoutRoute, - path: "/sécurité", - beforeLoad: () => ({ - title: "Sécurité", - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../features/dashboard/$idOrganization/organizationSettings/organizationSecurityPage.js" - ), - "OrganizationSecurityPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationSettings/organizationSettingsLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationSettings/organizationSettingsLayoutRoute.tsx deleted file mode 100644 index 0b39d8db..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationSettings/organizationSettingsLayoutRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { organizationLayoutRoute } from "../organizationLayoutRoute.js" - -export const organizationSettingsLayoutRoute = createRoute({ - getParentRoute: () => organizationLayoutRoute, - path: "/paramètres", - beforeLoad: () => ({ - title: "Paramètres", - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../features/dashboard/$idOrganization/organizationSettings/organizationSettingsLayout.js" - ), - "OrganizationSettingsLayout", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationSettings/organizationSettingsRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationSettings/organizationSettingsRoute.tsx deleted file mode 100644 index d8490853..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationSettings/organizationSettingsRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { organizationSettingsLayoutRoute } from "./organizationSettingsLayoutRoute.js" - -export const organizationSettingsRoute = createRoute({ - getParentRoute: () => organizationSettingsLayoutRoute, - path: "/", - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../features/dashboard/$idOrganization/organizationSettings/organizationSettingsPage.js" - ), - "OrganizationSettingsPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationUsers/organizationUsersLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationUsers/organizationUsersLayoutRoute.tsx deleted file mode 100644 index e296c808..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationUsers/organizationUsersLayoutRoute.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createRoute, Outlet } from "@tanstack/react-router" -import { organizationLayoutRoute } from "../organizationLayoutRoute.tsx" - -export const organizationUsersLayoutRoute = createRoute({ - getParentRoute: () => organizationLayoutRoute, - path: "/membres", - beforeLoad: () => ({ - title: "Membres", - }), - component: () => , -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationUsers/organizationUsersRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationUsers/organizationUsersRoute.tsx deleted file mode 100644 index 3065da29..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/organizationUsers/organizationUsersRoute.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { organizationUsersLayoutRoute } from "./organizationUsersLayoutRoute.js" - -export const organizationUsersRoute = createRoute({ - getParentRoute: () => organizationUsersLayoutRoute, - path: "/", - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent( - () => import("../../../../../../features/dashboard/$idOrganization/organizationUsers/organizationUsersPage.js"), - "OrganizationUsersPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/$idEntryLine/entryLineLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/$idEntryLine/entryLineLayoutRoute.tsx deleted file mode 100644 index 451fbcdf..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/$idEntryLine/entryLineLayoutRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { entriesLayoutRoute } from "../../entriesLayoutRoute.js" - -export const entryLineLayoutRoute = createRoute({ - getParentRoute: () => entriesLayoutRoute, - path: "/$idEntry/mouvements/$idEntryLine", - beforeLoad: () => ({ - title: "Ligne d'écriture", - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../../../../../features/dashboard/$idYear/entries/$idEntry/$idEntryLine/entryLineLayout.js" - ), - "EntryLineLayout", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/$idEntryLine/entryLineMetadataRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/$idEntryLine/entryLineMetadataRoute.tsx deleted file mode 100644 index 1a419175..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/$idEntryLine/entryLineMetadataRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { entryLineLayoutRoute } from "./entryLineLayoutRoute.js" - -export const entryLineMetadataRoute = createRoute({ - getParentRoute: () => entryLineLayoutRoute, - path: "/métadonnées", - beforeLoad: () => ({ - title: "Métadonnées", - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../../../../../features/dashboard/$idYear/entries/$idEntry/$idEntryLine/entryLineMetadataTab.js" - ), - "EntryLineMetadataTab", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/$idEntryLine/entryLineRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/$idEntryLine/entryLineRoute.tsx deleted file mode 100644 index 9670772b..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/$idEntryLine/entryLineRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { entryLineLayoutRoute } from "./entryLineLayoutRoute.js" - -export const entryLineRoute = createRoute({ - getParentRoute: () => entryLineLayoutRoute, - path: "/", - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../../../../../features/dashboard/$idYear/entries/$idEntry/$idEntryLine/entryLinePage.js" - ), - "EntryLinePage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryCategoriesRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryCategoriesRoute.tsx deleted file mode 100644 index c0812003..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryCategoriesRoute.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { entryLayoutRoute } from "./entryLayoutRoute.js" - -export const entryCategoriesRoute = createRoute({ - getParentRoute: () => entryLayoutRoute, - path: "/catégories", - beforeLoad: () => ({ - title: "Catégories", - }), - component: lazyRouteComponent( - () => import("../../../../../../../../../features/dashboard/$idYear/entries/$idEntry/entryCategoriesTab.js"), - "EntryCategoriesTab", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLayoutRoute.tsx deleted file mode 100644 index 7d61e80b..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLayoutRoute.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { entriesLayoutRoute } from "../entriesLayoutRoute.js" - -export const entryLayoutRoute = createRoute({ - getParentRoute: () => entriesLayoutRoute, - path: "/$idEntry", - beforeLoad: () => ({ - title: "Écriture", - }), - component: lazyRouteComponent( - () => import("../../../../../../../../../features/dashboard/$idYear/entries/$idEntry/entryLayout.js"), - "EntryLayout", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLinesRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLinesRoute.tsx deleted file mode 100644 index 26842d16..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryLinesRoute.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { entryLayoutRoute } from "./entryLayoutRoute.js" - -export const entryLinesRoute = createRoute({ - getParentRoute: () => entryLayoutRoute, - path: "/mouvements", - beforeLoad: () => ({ - title: "Mouvements", - }), - component: lazyRouteComponent( - () => import("../../../../../../../../../features/dashboard/$idYear/entries/$idEntry/entryLinesTab.js"), - "EntryLinesTab", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryMetadataRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryMetadataRoute.tsx deleted file mode 100644 index 1146223c..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryMetadataRoute.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { entryLayoutRoute } from "./entryLayoutRoute.js" - -export const entryMetadataRoute = createRoute({ - getParentRoute: () => entryLayoutRoute, - path: "/métadonnées", - beforeLoad: () => ({ - title: "Métadonnées", - }), - component: lazyRouteComponent( - () => import("../../../../../../../../../features/dashboard/$idYear/entries/$idEntry/entryMetadataTab.js"), - "EntryMetadataTab", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryRoute.tsx deleted file mode 100644 index 5973be7c..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/$idEntry/entryRoute.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { entryLayoutRoute } from "./entryLayoutRoute.js" - -export const entryRoute = createRoute({ - getParentRoute: () => entryLayoutRoute, - path: "/", - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent( - () => import("../../../../../../../../../features/dashboard/$idYear/entries/$idEntry/entryPage.js"), - "EntryPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/entriesLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/entriesLayoutRoute.tsx deleted file mode 100644 index 1e6db60d..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/entriesLayoutRoute.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createRoute, Outlet } from "@tanstack/react-router" -import { yearLayoutRoute } from "../yearLayoutRoute.js" - -export const entriesLayoutRoute = createRoute({ - getParentRoute: () => yearLayoutRoute, - path: "/écritures", - beforeLoad: () => ({ - title: "Écritures", - }), - component: () => , -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/entriesRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/entriesRoute.tsx deleted file mode 100644 index 0baf5388..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/entries/entriesRoute.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { entriesLayoutRoute } from "./entriesLayoutRoute.js" - -export const entriesRoute = createRoute({ - getParentRoute: () => entriesLayoutRoute, - path: "/", - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent( - () => import("../../../../../../../../features/dashboard/$idYear/entries/entriesPage.js"), - "EntriesPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/files/$idFile/fileLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/files/$idFile/fileLayoutRoute.tsx deleted file mode 100644 index 500ae2a4..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/files/$idFile/fileLayoutRoute.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { filesLayoutRoute } from "../filesLayoutRoute.js" - -export const fileLayoutRoute = createRoute({ - getParentRoute: () => filesLayoutRoute, - path: "/$idFile", - beforeLoad: () => ({ - title: "Pièce justificative", - }), - component: lazyRouteComponent( - () => import("../../../../../../../../../features/dashboard/$idYear/files/$idFile/fileLayout.js"), - "FileLayout", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/files/$idFile/fileMetadataRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/files/$idFile/fileMetadataRoute.tsx deleted file mode 100644 index 27bd79c7..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/files/$idFile/fileMetadataRoute.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { fileLayoutRoute } from "./fileLayoutRoute.js" - -export const fileMetadataRoute = createRoute({ - getParentRoute: () => fileLayoutRoute, - path: "/métadonnées", - beforeLoad: () => ({ - title: "Métadonnées", - }), - component: lazyRouteComponent( - () => import("../../../../../../../../../features/dashboard/$idYear/files/$idFile/fileMetadataTab.js"), - "FileMetadataTab", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/files/$idFile/fileRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/files/$idFile/fileRoute.tsx deleted file mode 100644 index 8019018b..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/files/$idFile/fileRoute.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { fileLayoutRoute } from "./fileLayoutRoute.js" - -export const fileRoute = createRoute({ - getParentRoute: () => fileLayoutRoute, - path: "/", - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent( - () => import("../../../../../../../../../features/dashboard/$idYear/files/$idFile/filePage.js"), - "FilePage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/files/$idFile/fileVisualisationRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/files/$idFile/fileVisualisationRoute.tsx deleted file mode 100644 index 326a702e..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/files/$idFile/fileVisualisationRoute.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { fileLayoutRoute } from "./fileLayoutRoute.js" - -export const fileVisualisationRoute = createRoute({ - getParentRoute: () => fileLayoutRoute, - path: "/visualisation", - beforeLoad: () => ({ - title: "Visualisation", - }), - component: lazyRouteComponent( - () => import("../../../../../../../../../features/dashboard/$idYear/files/$idFile/fileVisualisationTab.js"), - "FileVisualisationTab", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/files/filesLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/files/filesLayoutRoute.tsx deleted file mode 100644 index 805fd84a..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/files/filesLayoutRoute.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createRoute, Outlet } from "@tanstack/react-router" -import { yearLayoutRoute } from "../yearLayoutRoute.js" - -export const filesLayoutRoute = createRoute({ - getParentRoute: () => yearLayoutRoute, - path: "/stockage", - beforeLoad: () => ({ - title: "Stockage", - }), - component: () => , -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/files/filesRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/files/filesRoute.tsx deleted file mode 100644 index e73b18bb..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/files/filesRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { filesLayoutRoute } from "./filesLayoutRoute.js" - -export const filesRoute = createRoute({ - getParentRoute: () => filesLayoutRoute, - path: "/", - validateSearch: (search: Record) => ({ - idFolder: typeof search.idFolder === "string" ? search.idFolder : undefined, - }), - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent( - () => import("../../../../../../../../features/dashboard/$idYear/files/filesPage.js"), - "FilesPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/reports/balanceReportRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/reports/balanceReportRoute.tsx deleted file mode 100644 index 3e3ba5f1..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/reports/balanceReportRoute.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { reportsLayoutRoute } from "./reportsLayoutRoute.js" - -export const balanceReportRoute = createRoute({ - getParentRoute: () => reportsLayoutRoute, - path: "/balance", - beforeLoad: () => ({ - title: "Balance", - }), - component: lazyRouteComponent( - () => import("../../../../../../../../features/dashboard/$idYear/reports/balanceReport/balanceReportPage.js"), - "BalanceReportPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/reports/balanceSheetReportRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/reports/balanceSheetReportRoute.tsx deleted file mode 100644 index 792574a3..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/reports/balanceSheetReportRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { reportsLayoutRoute } from "./reportsLayoutRoute.js" - -export const balanceSheetReportRoute = createRoute({ - getParentRoute: () => reportsLayoutRoute, - path: "/bilan", - beforeLoad: () => ({ - title: "Bilan", - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../../../features/dashboard/$idYear/reports/balanceSheetReport/balanceSheetReportPage.js" - ), - "BalanceSheetReportPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/reports/incomeStatementReportRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/reports/incomeStatementReportRoute.tsx deleted file mode 100644 index a376cd47..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/reports/incomeStatementReportRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { reportsLayoutRoute } from "./reportsLayoutRoute.js" - -export const incomeStatementReportRoute = createRoute({ - getParentRoute: () => reportsLayoutRoute, - path: "/compte-de-résultat", - beforeLoad: () => ({ - title: "Compte de résultat", - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../../../features/dashboard/$idYear/reports/incomeStatementReport/incomeStatementReportPage.js" - ), - "IncomeStatementReportPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/reports/journalReportRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/reports/journalReportRoute.tsx deleted file mode 100644 index 049d906e..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/reports/journalReportRoute.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { reportsLayoutRoute } from "./reportsLayoutRoute.js" - -export const journalReportRoute = createRoute({ - getParentRoute: () => reportsLayoutRoute, - path: "/livre-journal", - beforeLoad: () => ({ - title: "Livre-journal", - }), - component: lazyRouteComponent( - () => import("../../../../../../../../features/dashboard/$idYear/reports/journalReport/journalReportPage.js"), - "JournalReportPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/reports/ledgerReportRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/reports/ledgerReportRoute.tsx deleted file mode 100644 index 9d1af1d2..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/reports/ledgerReportRoute.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { reportsLayoutRoute } from "./reportsLayoutRoute.js" - -export const ledgerReportRoute = createRoute({ - getParentRoute: () => reportsLayoutRoute, - path: "/grand-livre", - beforeLoad: () => ({ - title: "Grand livre", - }), - component: lazyRouteComponent( - () => import("../../../../../../../../features/dashboard/$idYear/reports/ledgerReport/ledgerReportPage.js"), - "LedgerReportPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/reports/reportsLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/reports/reportsLayoutRoute.tsx deleted file mode 100644 index 1c7b065e..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/reports/reportsLayoutRoute.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { yearLayoutRoute } from "../yearLayoutRoute.js" - -export const reportsLayoutRoute = createRoute({ - getParentRoute: () => yearLayoutRoute, - path: "/documents", - beforeLoad: () => ({ - title: "Documents comptables", - }), - component: lazyRouteComponent( - () => import("../../../../../../../../features/dashboard/$idYear/reports/reportsLayout.js"), - "ReportsLayout", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/reports/reportsRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/reports/reportsRoute.tsx deleted file mode 100644 index aa6266f3..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/reports/reportsRoute.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { createRoute, redirect } from "@tanstack/react-router" -import { reportsLayoutRoute } from "./reportsLayoutRoute.js" - -export const reportsRoute = createRoute({ - getParentRoute: () => reportsLayoutRoute, - path: "/", - beforeLoad: ({ params }) => { - throw redirect({ - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/documents/livre-journal", - params: params, - }) - }, -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearLayoutRoute.tsx deleted file mode 100644 index f22c2ada..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearLayoutRoute.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { CircularLoader } from "@arrhes/ui" -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { Page } from "../../../../../../../components/layouts/page/page.js" -import { prefetchYearData } from "../../../../../../../utilities/prefetchYearData.js" -import { yearPathRoute } from "./yearPathRoute.js" - -export const yearLayoutRoute = createRoute({ - getParentRoute: () => yearPathRoute, - id: "yearLayout", - pendingComponent: () => ( - - - - - - ), - beforeLoad: ({ params }) => { - prefetchYearData({ - idYear: params.idYear, - }) - - return { - title: undefined, - } - }, - component: lazyRouteComponent( - () => import("../../../../../../../features/dashboard/$idYear/yearLayout.js"), - "YearLayout", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearPathRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearPathRoute.tsx deleted file mode 100644 index 122ba089..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearPathRoute.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { createRoute } from "@tanstack/react-router" -import { yearsPathRoute } from "../yearsPathRoute.tsx" - -export const yearPathRoute = createRoute({ - getParentRoute: () => yearsPathRoute, - path: "/$idYear", - beforeLoad: () => ({ - title: "Exercice", - }), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearRoute.tsx deleted file mode 100644 index 37166558..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearRoute.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { createRoute, redirect } from "@tanstack/react-router" -import { yearLayoutRoute } from "./yearLayoutRoute.tsx" - -export const yearRoute = createRoute({ - getParentRoute: () => yearLayoutRoute, - path: "/", - beforeLoad: ({ params }) => { - throw redirect({ - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/écritures", - params: params, - }) - }, -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/accountLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/accountLayoutRoute.tsx deleted file mode 100644 index 5664678a..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/accountLayoutRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { accountsLayoutRoute } from "../accountsLayoutRoute.js" - -export const accountLayoutRoute = createRoute({ - getParentRoute: () => accountsLayoutRoute, - path: "/$idAccount", - beforeLoad: () => ({ - title: "Compte", - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../../../../../features/dashboard/$idYear/yearSettings/accounts/$idAccount/accountLayout.js" - ), - "AccountLayout", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/accountMetadataRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/accountMetadataRoute.tsx deleted file mode 100644 index f75fbea4..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/accountMetadataRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { accountLayoutRoute } from "./accountLayoutRoute.js" - -export const accountMetadataRoute = createRoute({ - getParentRoute: () => accountLayoutRoute, - path: "/métadonnées", - beforeLoad: () => ({ - title: "Métadonnées", - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../../../../../features/dashboard/$idYear/yearSettings/accounts/$idAccount/accountMetadataTab.js" - ), - "AccountMetadataTab", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/accountRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/accountRoute.tsx deleted file mode 100644 index d0297bf0..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/accounts/$idAccount/accountRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { accountLayoutRoute } from "./accountLayoutRoute.js" - -export const accountRoute = createRoute({ - getParentRoute: () => accountLayoutRoute, - path: "/", - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../../../../../features/dashboard/$idYear/yearSettings/accounts/$idAccount/accountPage.js" - ), - "AccountPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/accounts/accountsLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/accounts/accountsLayoutRoute.tsx deleted file mode 100644 index 9870ce10..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/accounts/accountsLayoutRoute.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createRoute, Outlet } from "@tanstack/react-router" -import { yearSettingsLayoutRoute } from "../yearSettingsLayoutRoute.js" - -export const accountsLayoutRoute = createRoute({ - getParentRoute: () => yearSettingsLayoutRoute, - path: "/comptes", - beforeLoad: () => ({ - title: "Plan des comptes", - }), - component: () => , -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/accounts/accountsRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/accounts/accountsRoute.tsx deleted file mode 100644 index a707ddd4..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/accounts/accountsRoute.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { accountsLayoutRoute } from "./accountsLayoutRoute.js" - -export const accountsRoute = createRoute({ - getParentRoute: () => accountsLayoutRoute, - path: "/", - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent( - () => import("../../../../../../../../../features/dashboard/$idYear/yearSettings/accounts/accountsPage.js"), - "AccountsPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/balanceSheetLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/balanceSheetLayoutRoute.tsx deleted file mode 100644 index 86015cd5..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/balanceSheetLayoutRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { balanceSheetsLayoutRoute } from "../balanceSheetsLayoutRoute.js" - -export const balanceSheetLayoutRoute = createRoute({ - getParentRoute: () => balanceSheetsLayoutRoute, - path: "/$idBalanceSheet", - beforeLoad: () => ({ - title: "Ligne de bilan", - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../../../../../features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/balanceSheetLayout.js" - ), - "BalanceSheetLayout", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/balanceSheetMetadataRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/balanceSheetMetadataRoute.tsx deleted file mode 100644 index 2c1a094f..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/balanceSheetMetadataRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { balanceSheetLayoutRoute } from "./balanceSheetLayoutRoute.js" - -export const balanceSheetMetadataRoute = createRoute({ - getParentRoute: () => balanceSheetLayoutRoute, - path: "/métadonnées", - beforeLoad: () => ({ - title: "Métadonnées", - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../../../../../features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/balanceSheetMetadataTab.js" - ), - "BalanceSheetMetadataTab", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/balanceSheetRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/balanceSheetRoute.tsx deleted file mode 100644 index 9da00902..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/$idBalanceSheet/balanceSheetRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { balanceSheetLayoutRoute } from "./balanceSheetLayoutRoute.js" - -export const balanceSheetRoute = createRoute({ - getParentRoute: () => balanceSheetLayoutRoute, - path: "/", - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../../../../../features/dashboard/$idYear/yearSettings/balanceSheets/$idBalanceSheet/balanceSheetPage.js" - ), - "BalanceSheetPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/actif/actifLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/actif/actifLayoutRoute.tsx deleted file mode 100644 index b3b2655b..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/actif/actifLayoutRoute.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createRoute, Outlet } from "@tanstack/react-router" -import { balanceSheetsLayoutRoute } from "../balanceSheetsLayoutRoute.js" - -export const actifLayoutRoute = createRoute({ - getParentRoute: () => balanceSheetsLayoutRoute, - path: "/actif", - beforeLoad: () => ({ - title: "Actif", - }), - component: () => , -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/actif/actifRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/actif/actifRoute.tsx deleted file mode 100644 index 02c8d768..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/actif/actifRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { actifLayoutRoute } from "./actifLayoutRoute.js" - -export const actifRoute = createRoute({ - getParentRoute: () => actifLayoutRoute, - path: "/", - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../../../../../features/dashboard/$idYear/yearSettings/balanceSheets/balanceSheetsPage.js" - ), - "BalanceSheetsPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/balanceSheetsLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/balanceSheetsLayoutRoute.tsx deleted file mode 100644 index c3e1c162..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/balanceSheetsLayoutRoute.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createRoute, Outlet } from "@tanstack/react-router" -import { yearSettingsLayoutRoute } from "../yearSettingsLayoutRoute.js" - -export const balanceSheetsLayoutRoute = createRoute({ - getParentRoute: () => yearSettingsLayoutRoute, - path: "/bilan", - beforeLoad: () => ({ - title: "Bilan", - }), - component: () => , -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/balanceSheetsRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/balanceSheetsRoute.tsx deleted file mode 100644 index 58ffd1f5..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/balanceSheetsRoute.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { createRoute, redirect } from "@tanstack/react-router" -import { balanceSheetsLayoutRoute } from "./balanceSheetsLayoutRoute.js" - -export const balanceSheetsRoute = createRoute({ - getParentRoute: () => balanceSheetsLayoutRoute, - path: "/", - beforeLoad: ({ params }) => { - throw redirect({ - to: "/dashboard/organisations/$idOrganization/exercices/$idYear/paramètres/bilan/actif", - params: params, - }) - }, -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/passif/passifLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/passif/passifLayoutRoute.tsx deleted file mode 100644 index b63fb1c7..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/passif/passifLayoutRoute.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createRoute, Outlet } from "@tanstack/react-router" -import { balanceSheetsLayoutRoute } from "../balanceSheetsLayoutRoute.js" - -export const passifLayoutRoute = createRoute({ - getParentRoute: () => balanceSheetsLayoutRoute, - path: "/passif", - beforeLoad: () => ({ - title: "Passif", - }), - component: () => , -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/passif/passifRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/passif/passifRoute.tsx deleted file mode 100644 index 609b4678..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/balanceSheets/passif/passifRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { passifLayoutRoute } from "./passifLayoutRoute.js" - -export const passifRoute = createRoute({ - getParentRoute: () => passifLayoutRoute, - path: "/", - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../../../../../features/dashboard/$idYear/yearSettings/balanceSheets/balanceSheetsPage.js" - ), - "BalanceSheetsPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/incomeStatementLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/incomeStatementLayoutRoute.tsx deleted file mode 100644 index bf725a60..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/incomeStatementLayoutRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { incomeStatementsLayoutRoute } from "../incomeStatementsLayoutRoute.js" - -export const incomeStatementLayoutRoute = createRoute({ - getParentRoute: () => incomeStatementsLayoutRoute, - path: "/$idIncomeStatement", - beforeLoad: () => ({ - title: "Ligne de compte de résultat", - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../../../../../features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/incomeStatementLayout.js" - ), - "IncomeStatementLayout", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/incomeStatementMetadataRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/incomeStatementMetadataRoute.tsx deleted file mode 100644 index 14356331..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/incomeStatementMetadataRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { incomeStatementLayoutRoute } from "./incomeStatementLayoutRoute.js" - -export const incomeStatementMetadataRoute = createRoute({ - getParentRoute: () => incomeStatementLayoutRoute, - path: "/métadonnées", - beforeLoad: () => ({ - title: "Métadonnées", - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../../../../../features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/incomeStatementMetadataTab.js" - ), - "IncomeStatementMetadataTab", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/incomeStatementRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/incomeStatementRoute.tsx deleted file mode 100644 index 81547666..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/$idIncomeStatement/incomeStatementRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { incomeStatementLayoutRoute } from "./incomeStatementLayoutRoute.js" - -export const incomeStatementRoute = createRoute({ - getParentRoute: () => incomeStatementLayoutRoute, - path: "/", - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../../../../../features/dashboard/$idYear/yearSettings/incomeStatements/$idIncomeStatement/incomeStatementPage.js" - ), - "IncomeStatementPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/$idComputationIncomeStatement/computationIncomeStatementLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/$idComputationIncomeStatement/computationIncomeStatementLayoutRoute.tsx deleted file mode 100644 index 4b5dd152..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/$idComputationIncomeStatement/computationIncomeStatementLayoutRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { computationsLayoutRoute } from "../../computationsLayoutRoute.js" - -export const computationIncomeStatementLayoutRoute = createRoute({ - getParentRoute: () => computationsLayoutRoute, - path: "/$idComputation/postes/$idComputationIncomeStatement", - beforeLoad: () => ({ - title: "Terme du calcul", - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../../../../../../../features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/computationIncomeStatementLayout.js" - ), - "ComputationIncomeStatementLayout", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/$idComputationIncomeStatement/computationIncomeStatementMetadataRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/$idComputationIncomeStatement/computationIncomeStatementMetadataRoute.tsx deleted file mode 100644 index 519ed1a8..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/$idComputationIncomeStatement/computationIncomeStatementMetadataRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { computationIncomeStatementLayoutRoute } from "./computationIncomeStatementLayoutRoute.js" - -export const computationIncomeStatementMetadataRoute = createRoute({ - getParentRoute: () => computationIncomeStatementLayoutRoute, - path: "/métadonnées", - beforeLoad: () => ({ - title: "Métadonnées", - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../../../../../../../features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/computationIncomeStatementMetadataTab.js" - ), - "ComputationIncomeStatementMetadataTab", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/$idComputationIncomeStatement/computationIncomeStatementRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/$idComputationIncomeStatement/computationIncomeStatementRoute.tsx deleted file mode 100644 index a3cbb367..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/$idComputationIncomeStatement/computationIncomeStatementRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { computationIncomeStatementLayoutRoute } from "./computationIncomeStatementLayoutRoute.js" - -export const computationIncomeStatementRoute = createRoute({ - getParentRoute: () => computationIncomeStatementLayoutRoute, - path: "/", - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../../../../../../../features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationIncomeStatements/$idComputationIncomeStatement/computationIncomeStatementPage.js" - ), - "ComputationIncomeStatementPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationLayoutRoute.tsx deleted file mode 100644 index c5d06932..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationLayoutRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { computationsLayoutRoute } from "../computationsLayoutRoute.js" - -export const computationLayoutRoute = createRoute({ - getParentRoute: () => computationsLayoutRoute, - path: "/$idComputation", - beforeLoad: () => ({ - title: "Calcul", - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../../../../../../features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationLayout.js" - ), - "ComputationLayout", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationMetadataRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationMetadataRoute.tsx deleted file mode 100644 index 93d9634d..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationMetadataRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { computationLayoutRoute } from "./computationLayoutRoute.js" - -export const computationMetadataRoute = createRoute({ - getParentRoute: () => computationLayoutRoute, - path: "/métadonnées", - beforeLoad: () => ({ - title: "Métadonnées", - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../../../../../../features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationMetadataTab.js" - ), - "ComputationMetadataTab", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationPostesRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationPostesRoute.tsx deleted file mode 100644 index 126149dc..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationPostesRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { computationLayoutRoute } from "./computationLayoutRoute.js" - -export const computationPostesRoute = createRoute({ - getParentRoute: () => computationLayoutRoute, - path: "/postes", - beforeLoad: () => ({ - title: "Postes", - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../../../../../../features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationPostesTab.js" - ), - "ComputationPostesTab", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationRoute.tsx deleted file mode 100644 index 9b808746..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { computationLayoutRoute } from "./computationLayoutRoute.js" - -export const computationRoute = createRoute({ - getParentRoute: () => computationLayoutRoute, - path: "/", - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../../../../../../features/dashboard/$idYear/yearSettings/incomeStatements/computations/$idComputation/computationPage.js" - ), - "ComputationPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/computationsLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/computationsLayoutRoute.tsx deleted file mode 100644 index 26178db8..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/computationsLayoutRoute.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createRoute, Outlet } from "@tanstack/react-router" -import { incomeStatementsLayoutRoute } from "../incomeStatementsLayoutRoute.js" - -export const computationsLayoutRoute = createRoute({ - getParentRoute: () => incomeStatementsLayoutRoute, - path: "/calculs", - beforeLoad: () => ({ - title: "Calculs", - }), - component: () => , -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/computationsRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/computationsRoute.tsx deleted file mode 100644 index 4de88528..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/computations/computationsRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { computationsLayoutRoute } from "./computationsLayoutRoute.js" - -export const computationsRoute = createRoute({ - getParentRoute: () => computationsLayoutRoute, - path: "/", - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../../../../../features/dashboard/$idYear/yearSettings/incomeStatements/computations/computationsPage.js" - ), - "ComputationsPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/incomeStatementsLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/incomeStatementsLayoutRoute.tsx deleted file mode 100644 index 99234119..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/incomeStatementsLayoutRoute.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createRoute, Outlet } from "@tanstack/react-router" -import { yearSettingsLayoutRoute } from "../yearSettingsLayoutRoute.js" - -export const incomeStatementsLayoutRoute = createRoute({ - getParentRoute: () => yearSettingsLayoutRoute, - path: "/compte-de-résultat", - beforeLoad: () => ({ - title: "Compte de résultat", - }), - component: () => , -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/incomeStatementsRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/incomeStatementsRoute.tsx deleted file mode 100644 index a198d0bc..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/incomeStatements/incomeStatementsRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { incomeStatementsLayoutRoute } from "./incomeStatementsLayoutRoute.js" - -export const incomeStatementsRoute = createRoute({ - getParentRoute: () => incomeStatementsLayoutRoute, - path: "/", - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../../../../features/dashboard/$idYear/yearSettings/incomeStatements/incomeStatementsPage.js" - ), - "IncomeStatementsPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/journalLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/journalLayoutRoute.tsx deleted file mode 100644 index db5d5d1b..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/journalLayoutRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { journalsLayoutRoute } from "../journalsLayoutRoute.js" - -export const journalLayoutRoute = createRoute({ - getParentRoute: () => journalsLayoutRoute, - path: "/$idJournal", - beforeLoad: () => ({ - title: "Journal", - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../../../../../features/dashboard/$idYear/yearSettings/journals/$idJournal/journalLayout.js" - ), - "JournalLayout", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/journalMetadataRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/journalMetadataRoute.tsx deleted file mode 100644 index 1e30fece..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/journalMetadataRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { journalLayoutRoute } from "./journalLayoutRoute.js" - -export const journalMetadataRoute = createRoute({ - getParentRoute: () => journalLayoutRoute, - path: "/métadonnées", - beforeLoad: () => ({ - title: "Métadonnées", - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../../../../../features/dashboard/$idYear/yearSettings/journals/$idJournal/journalMetadataTab.js" - ), - "JournalMetadataTab", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/journalRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/journalRoute.tsx deleted file mode 100644 index 4f1d2ccf..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/journals/$idJournal/journalRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { journalLayoutRoute } from "./journalLayoutRoute.js" - -export const journalRoute = createRoute({ - getParentRoute: () => journalLayoutRoute, - path: "/", - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../../../../../features/dashboard/$idYear/yearSettings/journals/$idJournal/journalPage.js" - ), - "JournalPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/journals/journalsLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/journals/journalsLayoutRoute.tsx deleted file mode 100644 index 843511f1..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/journals/journalsLayoutRoute.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createRoute, Outlet } from "@tanstack/react-router" -import { yearSettingsLayoutRoute } from "../yearSettingsLayoutRoute.js" - -export const journalsLayoutRoute = createRoute({ - getParentRoute: () => yearSettingsLayoutRoute, - path: "/journaux", - beforeLoad: () => ({ - title: "Journaux", - }), - component: () => , -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/journals/journalsRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/journals/journalsRoute.tsx deleted file mode 100644 index 51f3448c..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/journals/journalsRoute.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { journalsLayoutRoute } from "./journalsLayoutRoute.js" - -export const journalsRoute = createRoute({ - getParentRoute: () => journalsLayoutRoute, - path: "/", - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent( - () => import("../../../../../../../../../features/dashboard/$idYear/yearSettings/journals/journalsPage.js"), - "JournalsPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/tagLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/tagLayoutRoute.tsx deleted file mode 100644 index d71d98ef..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/tagLayoutRoute.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { tagsLayoutRoute } from "../tagsLayoutRoute.js" - -export const tagLayoutRoute = createRoute({ - getParentRoute: () => tagsLayoutRoute, - path: "/$idTag", - beforeLoad: () => ({ - title: "Catégorie", - }), - component: lazyRouteComponent( - () => import("../../../../../../../../../../features/dashboard/$idYear/yearSettings/tags/$idTag/tagLayout.js"), - "TagLayout", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/tagMetadataRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/tagMetadataRoute.tsx deleted file mode 100644 index d25d515a..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/tagMetadataRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { tagLayoutRoute } from "./tagLayoutRoute.js" - -export const tagMetadataRoute = createRoute({ - getParentRoute: () => tagLayoutRoute, - path: "/métadonnées", - beforeLoad: () => ({ - title: "Métadonnées", - }), - component: lazyRouteComponent( - () => - import( - "../../../../../../../../../../features/dashboard/$idYear/yearSettings/tags/$idTag/tagMetadataTab.js" - ), - "TagMetadataTab", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/tagRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/tagRoute.tsx deleted file mode 100644 index 1161f058..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/tags/$idTag/tagRoute.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { tagLayoutRoute } from "./tagLayoutRoute.js" - -export const tagRoute = createRoute({ - getParentRoute: () => tagLayoutRoute, - path: "/", - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent( - () => import("../../../../../../../../../../features/dashboard/$idYear/yearSettings/tags/$idTag/tagPage.js"), - "TagPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/tags/tagsLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/tags/tagsLayoutRoute.tsx deleted file mode 100644 index ada2b19e..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/tags/tagsLayoutRoute.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createRoute, Outlet } from "@tanstack/react-router" -import { yearSettingsLayoutRoute } from "../yearSettingsLayoutRoute.js" - -export const tagsLayoutRoute = createRoute({ - getParentRoute: () => yearSettingsLayoutRoute, - path: "/catégories", - beforeLoad: () => ({ - title: "Catégories", - }), - component: () => , -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/tags/tagsRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/tags/tagsRoute.tsx deleted file mode 100644 index 2c130a05..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/tags/tagsRoute.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { tagsLayoutRoute } from "./tagsLayoutRoute.js" - -export const tagsRoute = createRoute({ - getParentRoute: () => tagsLayoutRoute, - path: "/", - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent( - () => import("../../../../../../../../../features/dashboard/$idYear/yearSettings/tags/tagsPage.js"), - "TagsPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/yearSettingsLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/yearSettingsLayoutRoute.tsx deleted file mode 100644 index 2321bfb0..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/yearSettingsLayoutRoute.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { yearLayoutRoute } from "../yearLayoutRoute.js" - -export const yearSettingsLayoutRoute = createRoute({ - getParentRoute: () => yearLayoutRoute, - path: "/paramètres", - beforeLoad: () => ({ - title: "Paramètres", - }), - component: lazyRouteComponent( - () => import("../../../../../../../../features/dashboard/$idYear/yearSettings/yearSettingsLayout.js"), - "YearSettingsLayout", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/yearSettingsRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/yearSettingsRoute.tsx deleted file mode 100644 index 505568e3..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/$idYear/yearSettings/yearSettingsRoute.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { yearSettingsLayoutRoute } from "./yearSettingsLayoutRoute.js" - -export const yearSettingsRoute = createRoute({ - getParentRoute: () => yearSettingsLayoutRoute, - path: "/", - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent( - () => import("../../../../../../../../features/dashboard/$idYear/yearSettings/yearSettingsPage.js"), - "YearSettingsPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/yearsLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/yearsLayoutRoute.tsx deleted file mode 100644 index 169cb397..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/yearsLayoutRoute.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createRoute, Outlet } from "@tanstack/react-router" -import { organizationLayoutRoute } from "../organizationLayoutRoute.js" - -export const yearsLayoutRoute = createRoute({ - getParentRoute: () => organizationLayoutRoute, - path: "/exercices", - beforeLoad: () => ({ - title: "Exercices", - }), - component: () => , -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/yearsPathRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/yearsPathRoute.tsx deleted file mode 100644 index 79d4a136..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/yearsPathRoute.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { createRoute } from "@tanstack/react-router" -import { organizationPathRoute } from "../organizationPathRoute.js" - -export const yearsPathRoute = createRoute({ - getParentRoute: () => organizationPathRoute, - path: "/exercices", - beforeLoad: () => ({ - title: "Exercices", - }), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/yearsRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/yearsRoute.tsx deleted file mode 100644 index dc254bf2..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/$idOrganization/years/yearsRoute.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { yearsLayoutRoute } from "./yearsLayoutRoute.js" - -export const yearsRoute = createRoute({ - getParentRoute: () => yearsLayoutRoute, - path: "/", - beforeLoad: () => ({ - title: "Exercices fiscaux", - }), - component: lazyRouteComponent( - () => import("../../../../../../features/dashboard/$idOrganization/years/yearsPage.js"), - "YearsPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/organizationsLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/organizationsLayoutRoute.tsx deleted file mode 100644 index 76900106..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/organizationsLayoutRoute.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createRoute, Outlet } from "@tanstack/react-router" -import { dashboardLayoutRoute } from "../dashboardLayoutRoute.js" - -export const organizationsLayoutRoute = createRoute({ - getParentRoute: () => dashboardLayoutRoute, - path: "/organisations", - beforeLoad: () => ({ - title: undefined, - }), - component: () => , -}) diff --git a/packages/website/src/routes/root/dashboard/organizations/organizationsRoute.tsx b/packages/website/src/routes/root/dashboard/organizations/organizationsRoute.tsx deleted file mode 100644 index 551b0731..00000000 --- a/packages/website/src/routes/root/dashboard/organizations/organizationsRoute.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { organizationsLayoutRoute } from "./organizationsLayoutRoute.js" - -export const organizationsRoute = createRoute({ - getParentRoute: () => organizationsLayoutRoute, - path: "/", - beforeLoad: () => ({ - title: "Organisations", - }), - component: lazyRouteComponent( - () => import("../../../../features/dashboard/organizations/organizationsPage.js"), - "OrganizationsPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/profile/userProfileLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/profile/userProfileLayoutRoute.tsx deleted file mode 100644 index 4c80301c..00000000 --- a/packages/website/src/routes/root/dashboard/profile/userProfileLayoutRoute.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createRoute, Outlet } from "@tanstack/react-router" -import { dashboardLayoutRoute } from "../dashboardLayoutRoute.js" - -export const userProfileLayoutRoute = createRoute({ - getParentRoute: () => dashboardLayoutRoute, - path: "/profil", - beforeLoad: () => ({ - title: "Profil", - }), - component: () => , -}) diff --git a/packages/website/src/routes/root/dashboard/profile/userProfileRoute.tsx b/packages/website/src/routes/root/dashboard/profile/userProfileRoute.tsx deleted file mode 100644 index 1e346985..00000000 --- a/packages/website/src/routes/root/dashboard/profile/userProfileRoute.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { userProfileLayoutRoute } from "./userProfileLayoutRoute.js" - -export const userProfileRoute = createRoute({ - getParentRoute: () => userProfileLayoutRoute, - path: "/", - beforeLoad: () => {}, - component: lazyRouteComponent( - () => import("../../../../features/dashboard/profile/userProfilePage.js"), - "UserProfilePage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/settings/settingsLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/settings/settingsLayoutRoute.tsx deleted file mode 100644 index c950326c..00000000 --- a/packages/website/src/routes/root/dashboard/settings/settingsLayoutRoute.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createRoute, Outlet } from "@tanstack/react-router" -import { dashboardLayoutRoute } from "../dashboardLayoutRoute.js" - -export const settingsLayoutRoute = createRoute({ - getParentRoute: () => dashboardLayoutRoute, - path: "/paramètres", - beforeLoad: () => ({ - title: "Paramètres", - }), - component: () => , -}) diff --git a/packages/website/src/routes/root/dashboard/settings/settingsRoute.tsx b/packages/website/src/routes/root/dashboard/settings/settingsRoute.tsx deleted file mode 100644 index 59d1206e..00000000 --- a/packages/website/src/routes/root/dashboard/settings/settingsRoute.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { settingsLayoutRoute } from "./settingsLayoutRoute.js" - -export const settingsRoute = createRoute({ - getParentRoute: () => settingsLayoutRoute, - path: "/", - beforeLoad: () => {}, - component: lazyRouteComponent( - () => import("../../../../features/dashboard/settings/settingsPage.js"), - "SettingsPage", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/support/supportLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/support/supportLayoutRoute.tsx deleted file mode 100644 index 69d03f3b..00000000 --- a/packages/website/src/routes/root/dashboard/support/supportLayoutRoute.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createRoute, Outlet } from "@tanstack/react-router" -import { dashboardLayoutRoute } from "../dashboardLayoutRoute.js" - -export const supportLayoutRoute = createRoute({ - getParentRoute: () => dashboardLayoutRoute, - path: "/support", - beforeLoad: () => ({ - title: "Support", - }), - component: () => , -}) diff --git a/packages/website/src/routes/root/dashboard/support/supportRoute.tsx b/packages/website/src/routes/root/dashboard/support/supportRoute.tsx deleted file mode 100644 index b5fd1ce9..00000000 --- a/packages/website/src/routes/root/dashboard/support/supportRoute.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { supportLayoutRoute } from "./supportLayoutRoute.js" - -export const supportRoute = createRoute({ - getParentRoute: () => supportLayoutRoute, - path: "/", - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent(() => import("../../../../features/dashboard/support/supportPage.js"), "SupportPage"), -}) diff --git a/packages/website/src/routes/root/dashboard/support/ticketLayoutRoute.tsx b/packages/website/src/routes/root/dashboard/support/ticketLayoutRoute.tsx deleted file mode 100644 index 307ac2f6..00000000 --- a/packages/website/src/routes/root/dashboard/support/ticketLayoutRoute.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { supportLayoutRoute } from "./supportLayoutRoute.js" - -export const ticketLayoutRoute = createRoute({ - getParentRoute: () => supportLayoutRoute, - path: "/tickets/$idTicket", - beforeLoad: () => ({ - title: "Ticket", - }), - component: lazyRouteComponent( - () => import("../../../../features/dashboard/support/$idTicket/ticketLayout.js"), - "TicketLayout", - ), -}) diff --git a/packages/website/src/routes/root/dashboard/support/ticketRoute.tsx b/packages/website/src/routes/root/dashboard/support/ticketRoute.tsx deleted file mode 100644 index 73ee17c4..00000000 --- a/packages/website/src/routes/root/dashboard/support/ticketRoute.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoute, lazyRouteComponent } from "@tanstack/react-router" -import { ticketLayoutRoute } from "./ticketLayoutRoute.js" - -export const ticketRoute = createRoute({ - getParentRoute: () => ticketLayoutRoute, - path: "/", - beforeLoad: () => ({ - title: undefined, - }), - component: lazyRouteComponent( - () => import("../../../../features/dashboard/support/$idTicket/ticketPage.js"), - "TicketPage", - ), -}) diff --git a/packages/website/src/routes/root/docs/accounting/introduction/accountsAccountingDocRoute.tsx b/packages/website/src/routes/root/docs/accounting/introduction/accountsAccountingDocRoute.tsx index 7b40abc0..69ba10e4 100644 --- a/packages/website/src/routes/root/docs/accounting/introduction/accountsAccountingDocRoute.tsx +++ b/packages/website/src/routes/root/docs/accounting/introduction/accountsAccountingDocRoute.tsx @@ -10,7 +10,7 @@ export const accountsAccountingDocRoute = createRoute({ "Présentation des comptes comptables : structure, numérotation et fonctionnement du plan comptable général français.", }), component: lazyRouteComponent( - () => import("../../../../../features/docs/accounting/introduction/accountsAccountingDocPage.tsx"), + () => import("../../../../../features/docs/accounting/introduction/AccountsAccountingDocPage.tsx"), "AccountsAccountingDocPage", ), }) diff --git a/packages/website/src/routes/root/docs/accounting/introduction/classesAccountingDocRoute.tsx b/packages/website/src/routes/root/docs/accounting/introduction/classesAccountingDocRoute.tsx index 88634bd7..0fc170c7 100644 --- a/packages/website/src/routes/root/docs/accounting/introduction/classesAccountingDocRoute.tsx +++ b/packages/website/src/routes/root/docs/accounting/introduction/classesAccountingDocRoute.tsx @@ -10,7 +10,7 @@ export const classesAccountingDocRoute = createRoute({ "Les 8 classes du plan comptable général français : comptes de bilan (1 à 5) et comptes de gestion (6 et 7).", }), component: lazyRouteComponent( - () => import("../../../../../features/docs/accounting/introduction/classesAccountingDocPage.tsx"), + () => import("../../../../../features/docs/accounting/introduction/ClassesAccountingDocPage.tsx"), "ClassesAccountingDocPage", ), }) diff --git a/packages/website/src/routes/root/docs/accounting/introduction/doubleEntryAccountingDocRoute.tsx b/packages/website/src/routes/root/docs/accounting/introduction/doubleEntryAccountingDocRoute.tsx index 05093534..0403b23d 100644 --- a/packages/website/src/routes/root/docs/accounting/introduction/doubleEntryAccountingDocRoute.tsx +++ b/packages/website/src/routes/root/docs/accounting/introduction/doubleEntryAccountingDocRoute.tsx @@ -10,7 +10,7 @@ export const doubleEntryAccountingDocRoute = createRoute({ "Comprendre le principe de la partie double en comptabilité : chaque opération génère au moins un débit et un crédit.", }), component: lazyRouteComponent( - () => import("../../../../../features/docs/accounting/introduction/doubleEntryAccountingDocPage.js"), + () => import("../../../../../features/docs/accounting/introduction/DoubleEntryAccountingDocPage.js"), "DoubleEntryAccountingDocPage", ), }) diff --git a/packages/website/src/routes/root/docs/accounting/introduction/entriesAccountingDocRoute.tsx b/packages/website/src/routes/root/docs/accounting/introduction/entriesAccountingDocRoute.tsx index 100f53f1..b0964385 100644 --- a/packages/website/src/routes/root/docs/accounting/introduction/entriesAccountingDocRoute.tsx +++ b/packages/website/src/routes/root/docs/accounting/introduction/entriesAccountingDocRoute.tsx @@ -10,7 +10,7 @@ export const entriesAccountingDocRoute = createRoute({ "Apprenez à passer des écritures comptables : lignes de débit et crédit, pièces justificatives et enregistrement des opérations.", }), component: lazyRouteComponent( - () => import("../../../../../features/docs/accounting/introduction/entriesAccountingDocPage.tsx"), + () => import("../../../../../features/docs/accounting/introduction/EntriesAccountingDocPage.tsx"), "EntriesAccountingDocPage", ), }) diff --git a/packages/website/src/routes/root/docs/accounting/introduction/introductionAccountingDocRoute.tsx b/packages/website/src/routes/root/docs/accounting/introduction/introductionAccountingDocRoute.tsx index efbcfd71..82179d53 100644 --- a/packages/website/src/routes/root/docs/accounting/introduction/introductionAccountingDocRoute.tsx +++ b/packages/website/src/routes/root/docs/accounting/introduction/introductionAccountingDocRoute.tsx @@ -10,7 +10,7 @@ export const introductionAccountingDocRoute = createRoute({ "Introduction aux fondamentaux de la comptabilité française : principes, obligations légales et concepts de base.", }), component: lazyRouteComponent( - () => import("../../../../../features/docs/accounting/introduction/introductionAccountingDocPage.js"), + () => import("../../../../../features/docs/accounting/introduction/IntroductionAccountingDocPage.js"), "IntroductionAccountingDocPage", ), }) diff --git a/packages/website/src/routes/root/docs/accounting/reports/balanceAccountingDocRoute.tsx b/packages/website/src/routes/root/docs/accounting/reports/balanceAccountingDocRoute.tsx index c4459ee7..7929d3e6 100644 --- a/packages/website/src/routes/root/docs/accounting/reports/balanceAccountingDocRoute.tsx +++ b/packages/website/src/routes/root/docs/accounting/reports/balanceAccountingDocRoute.tsx @@ -10,7 +10,7 @@ export const balanceAccountingDocRoute = createRoute({ "La balance comptable : tableau récapitulatif des soldes débiteurs et créditeurs de tous les comptes d'un exercice.", }), component: lazyRouteComponent( - () => import("../../../../../features/docs/accounting/reports/balanceAccountingDocPage.tsx"), + () => import("../../../../../features/docs/accounting/reports/BalanceAccountingDocPage.tsx"), "BalanceAccountingDocPage", ), }) diff --git a/packages/website/src/routes/root/docs/accounting/reports/balanceSheetAccountingDocRoute.tsx b/packages/website/src/routes/root/docs/accounting/reports/balanceSheetAccountingDocRoute.tsx index 1c9f4ddc..01bbee02 100644 --- a/packages/website/src/routes/root/docs/accounting/reports/balanceSheetAccountingDocRoute.tsx +++ b/packages/website/src/routes/root/docs/accounting/reports/balanceSheetAccountingDocRoute.tsx @@ -10,7 +10,7 @@ export const balanceSheetAccountingDocRoute = createRoute({ "Le bilan comptable : document de synthèse présentant le patrimoine de l'entreprise (actif et passif) à la clôture de l'exercice.", }), component: lazyRouteComponent( - () => import("../../../../../features/docs/accounting/reports/balanceSheetAccountingDocPage.tsx"), + () => import("../../../../../features/docs/accounting/reports/BalanceSheetAccountingDocPage.tsx"), "BalanceSheetAccountingDocPage", ), }) diff --git a/packages/website/src/routes/root/docs/accounting/reports/fecAccountingDocRoute.tsx b/packages/website/src/routes/root/docs/accounting/reports/fecAccountingDocRoute.tsx index ac6f49f6..9224667e 100644 --- a/packages/website/src/routes/root/docs/accounting/reports/fecAccountingDocRoute.tsx +++ b/packages/website/src/routes/root/docs/accounting/reports/fecAccountingDocRoute.tsx @@ -10,7 +10,7 @@ export const fecAccountingDocRoute = createRoute({ "Le FEC : fichier normé des écritures comptables requis par l'administration fiscale lors d'un contrôle.", }), component: lazyRouteComponent( - () => import("../../../../../features/docs/accounting/reports/fecAccountingDocPage.tsx"), + () => import("../../../../../features/docs/accounting/reports/FecAccountingDocPage.tsx"), "FecAccountingDocPage", ), }) diff --git a/packages/website/src/routes/root/docs/accounting/reports/incomeStatementAccountingDocRoute.tsx b/packages/website/src/routes/root/docs/accounting/reports/incomeStatementAccountingDocRoute.tsx index d7ff5df0..40e9a3ba 100644 --- a/packages/website/src/routes/root/docs/accounting/reports/incomeStatementAccountingDocRoute.tsx +++ b/packages/website/src/routes/root/docs/accounting/reports/incomeStatementAccountingDocRoute.tsx @@ -10,7 +10,7 @@ export const incomeStatementAccountingDocRoute = createRoute({ "Le compte de résultat : document de synthèse présentant les produits et charges de l'exercice pour déterminer le résultat net.", }), component: lazyRouteComponent( - () => import("../../../../../features/docs/accounting/reports/incomeStatementAccountingDocPage.tsx"), + () => import("../../../../../features/docs/accounting/reports/IncomeStatementAccountingDocPage.tsx"), "IncomeStatementAccountingDocPage", ), }) diff --git a/packages/website/src/routes/root/docs/accounting/reports/journalAccountingDocRoute.tsx b/packages/website/src/routes/root/docs/accounting/reports/journalAccountingDocRoute.tsx index 04d9f37d..e3ecd139 100644 --- a/packages/website/src/routes/root/docs/accounting/reports/journalAccountingDocRoute.tsx +++ b/packages/website/src/routes/root/docs/accounting/reports/journalAccountingDocRoute.tsx @@ -10,7 +10,7 @@ export const journalAccountingDocRoute = createRoute({ "Le journal comptable : document chronologique enregistrant toutes les opérations comptables de l'entreprise.", }), component: lazyRouteComponent( - () => import("../../../../../features/docs/accounting/reports/journalAccountingDocPage.tsx"), + () => import("../../../../../features/docs/accounting/reports/JournalAccountingDocPage.tsx"), "JournalAccountingDocPage", ), }) diff --git a/packages/website/src/routes/root/docs/accounting/reports/ledgerAccountingDocRoute.tsx b/packages/website/src/routes/root/docs/accounting/reports/ledgerAccountingDocRoute.tsx index 988290b4..75ed1ec4 100644 --- a/packages/website/src/routes/root/docs/accounting/reports/ledgerAccountingDocRoute.tsx +++ b/packages/website/src/routes/root/docs/accounting/reports/ledgerAccountingDocRoute.tsx @@ -10,7 +10,7 @@ export const ledgerAccountingDocRoute = createRoute({ "Le grand livre comptable : regroupement de toutes les écritures par compte, outil essentiel de suivi comptable.", }), component: lazyRouteComponent( - () => import("../../../../../features/docs/accounting/reports/ledgerAccountingDocPage.tsx"), + () => import("../../../../../features/docs/accounting/reports/LedgerAccountingDocPage.tsx"), "LedgerAccountingDocPage", ), }) diff --git a/packages/website/src/routes/root/docs/accounting/reports/notesAccountingDocRoute.tsx b/packages/website/src/routes/root/docs/accounting/reports/notesAccountingDocRoute.tsx index 41a36aac..e76a05d7 100644 --- a/packages/website/src/routes/root/docs/accounting/reports/notesAccountingDocRoute.tsx +++ b/packages/website/src/routes/root/docs/accounting/reports/notesAccountingDocRoute.tsx @@ -10,7 +10,7 @@ export const notesAccountingDocRoute = createRoute({ "L'annexe comptable : document complémentaire au bilan et au compte de résultat, fournissant des informations détaillées.", }), component: lazyRouteComponent( - () => import("../../../../../features/docs/accounting/reports/notesAccountingDocPage.tsx"), + () => import("../../../../../features/docs/accounting/reports/NotesAccountingDocPage.tsx"), "NotesAccountingDocPage", ), }) diff --git a/packages/website/src/routes/root/docs/accounting/reports/reportsAccountingDocRoute.tsx b/packages/website/src/routes/root/docs/accounting/reports/reportsAccountingDocRoute.tsx index 642d2bda..fe43e580 100644 --- a/packages/website/src/routes/root/docs/accounting/reports/reportsAccountingDocRoute.tsx +++ b/packages/website/src/routes/root/docs/accounting/reports/reportsAccountingDocRoute.tsx @@ -10,7 +10,7 @@ export const reportsAccountingDocRoute = createRoute({ "Les documents comptables obligatoires : journal, grand livre, balance, bilan, compte de résultat et annexe.", }), component: lazyRouteComponent( - () => import("../../../../../features/docs/accounting/reports/reportsAccountingDocPage.tsx"), + () => import("../../../../../features/docs/accounting/reports/ReportsAccountingDocPage.tsx"), "ReportsAccountingDocPage", ), }) diff --git a/packages/website/src/routes/root/docs/accounting/resources/accounts/accountAccountingDocRoute.tsx b/packages/website/src/routes/root/docs/accounting/resources/accounts/accountAccountingDocRoute.tsx index df7ddf54..87a12bbf 100644 --- a/packages/website/src/routes/root/docs/accounting/resources/accounts/accountAccountingDocRoute.tsx +++ b/packages/website/src/routes/root/docs/accounting/resources/accounts/accountAccountingDocRoute.tsx @@ -17,7 +17,7 @@ export const accountAccountingDocRoute = createRoute({ component: lazyRouteComponent( () => import( - "../../../../../../features/docs/accounting/resources/accounts/accountResourcesAccountingDocPage.tsx" + "../../../../../../features/docs/accounting/resources/accounts/AccountResourcesAccountingDocPage.js" ), "AccountResourcesAccountingDocPage", ), diff --git a/packages/website/src/routes/root/docs/accounting/resources/accounts/accountsAccountingDocRoute.tsx b/packages/website/src/routes/root/docs/accounting/resources/accounts/accountsAccountingDocRoute.tsx index 5d2b0956..885e948a 100644 --- a/packages/website/src/routes/root/docs/accounting/resources/accounts/accountsAccountingDocRoute.tsx +++ b/packages/website/src/routes/root/docs/accounting/resources/accounts/accountsAccountingDocRoute.tsx @@ -12,7 +12,7 @@ export const accountsAccountingDocRoute = createRoute({ component: lazyRouteComponent( () => import( - "../../../../../../features/docs/accounting/resources/accounts/accountsResourcesAccountingDocPage.tsx" + "../../../../../../features/docs/accounting/resources/accounts/AccountsResourcesAccountingDocPage.js" ), "AccountsResourcesAccountingDocPage", ), diff --git a/packages/website/src/routes/root/docs/accounting/resources/glossary/glossaryAccountingDocRoute.tsx b/packages/website/src/routes/root/docs/accounting/resources/glossary/glossaryAccountingDocRoute.tsx index ca665bc1..6ca8ab01 100644 --- a/packages/website/src/routes/root/docs/accounting/resources/glossary/glossaryAccountingDocRoute.tsx +++ b/packages/website/src/routes/root/docs/accounting/resources/glossary/glossaryAccountingDocRoute.tsx @@ -12,7 +12,7 @@ export const glossaryAccountingDocIndexRoute = createRoute({ component: lazyRouteComponent( () => import( - "../../../../../../features/docs/accounting/resources/glossary/glossaryResourcesAccountingDocPage.tsx" + "../../../../../../features/docs/accounting/resources/glossary/GlossaryResourcesAccountingDocPage.js" ), "GlossaryResourcesAccountingDocPage", ), diff --git a/packages/website/src/routes/root/docs/accounting/resources/glossary/glossaryTermAccountingDocRoute.tsx b/packages/website/src/routes/root/docs/accounting/resources/glossary/glossaryTermAccountingDocRoute.tsx index 5f516b08..8bf10eba 100644 --- a/packages/website/src/routes/root/docs/accounting/resources/glossary/glossaryTermAccountingDocRoute.tsx +++ b/packages/website/src/routes/root/docs/accounting/resources/glossary/glossaryTermAccountingDocRoute.tsx @@ -17,7 +17,7 @@ export const glossaryTermAccountingDocRoute = createRoute({ component: lazyRouteComponent( () => import( - "../../../../../../features/docs/accounting/resources/glossary/glossaryTermResourcesAccountingDocPage.tsx" + "../../../../../../features/docs/accounting/resources/glossary/GlossaryTermResourcesAccountingDocPage.js" ), "GlossaryTermResourcesAccountingDocPage", ), diff --git a/packages/website/src/routes/root/docs/accounting/resources/scenarios/scenarioAccountingDocRoute.tsx b/packages/website/src/routes/root/docs/accounting/resources/scenarios/scenarioAccountingDocRoute.tsx index d9acb253..247622fc 100644 --- a/packages/website/src/routes/root/docs/accounting/resources/scenarios/scenarioAccountingDocRoute.tsx +++ b/packages/website/src/routes/root/docs/accounting/resources/scenarios/scenarioAccountingDocRoute.tsx @@ -16,7 +16,7 @@ export const scenarioAccountingDocRoute = createRoute({ component: lazyRouteComponent( () => import( - "../../../../../../features/docs/accounting/resources/scenarios/scenarioResourcesAccountingDocPage.tsx" + "../../../../../../features/docs/accounting/resources/scenarios/ScenarioResourcesAccountingDocPage.js" ), "ScenarioResourcesAccountingDocPage", ), diff --git a/packages/website/src/routes/root/docs/accounting/resources/scenarios/scenariosAccountingDocRoute.tsx b/packages/website/src/routes/root/docs/accounting/resources/scenarios/scenariosAccountingDocRoute.tsx index fc8b2e93..818b5733 100644 --- a/packages/website/src/routes/root/docs/accounting/resources/scenarios/scenariosAccountingDocRoute.tsx +++ b/packages/website/src/routes/root/docs/accounting/resources/scenarios/scenariosAccountingDocRoute.tsx @@ -11,7 +11,7 @@ export const scenariosAccountingDocIndexRoute = createRoute({ component: lazyRouteComponent( () => import( - "../../../../../../features/docs/accounting/resources/scenarios/scenariosResourcesAccountingDocPage.tsx" + "../../../../../../features/docs/accounting/resources/scenarios/ScenariosResourcesAccountingDocPage.js" ), "ScenariosResourcesAccountingDocPage", ), diff --git a/packages/website/src/routes/root/docs/accounting/rootAccountingDocRoute.tsx b/packages/website/src/routes/root/docs/accounting/rootAccountingDocRoute.tsx index e3706a58..80a32445 100644 --- a/packages/website/src/routes/root/docs/accounting/rootAccountingDocRoute.tsx +++ b/packages/website/src/routes/root/docs/accounting/rootAccountingDocRoute.tsx @@ -10,7 +10,7 @@ export const rootAccountingDocRoute = createRoute({ "Cours de comptabilité française complet : partie double, écritures, comptes, documents de synthèse et glossaire.", }), component: lazyRouteComponent( - () => import("../../../../features/docs/accounting/introduction/rootAccountingDocPage.js"), + () => import("../../../../features/docs/accounting/introduction/RootAccountingDocPage.js"), "RootAccountingDocPage", ), }) diff --git a/packages/website/src/routes/root/docs/ai/modelsAiDocRoute.tsx b/packages/website/src/routes/root/docs/ai/modelsAiDocRoute.tsx index 9fc0249c..93c1d633 100644 --- a/packages/website/src/routes/root/docs/ai/modelsAiDocRoute.tsx +++ b/packages/website/src/routes/root/docs/ai/modelsAiDocRoute.tsx @@ -8,5 +8,5 @@ export const modelsAiDocRoute = createRoute({ title: "Modèles IA", description: "Les modèles de langage utilisés par l'assistant comptable d'Arrhes.", }), - component: lazyRouteComponent(() => import("../../../../features/docs/ai/modelsAiDocPage.tsx"), "ModelsAiDocPage"), + component: lazyRouteComponent(() => import("../../../../features/docs/ai/ModelsAiDocPage.tsx"), "ModelsAiDocPage"), }) diff --git a/packages/website/src/routes/root/docs/ai/ocrAiDocRoute.tsx b/packages/website/src/routes/root/docs/ai/ocrAiDocRoute.tsx index 4bd1d8ae..c84a7359 100644 --- a/packages/website/src/routes/root/docs/ai/ocrAiDocRoute.tsx +++ b/packages/website/src/routes/root/docs/ai/ocrAiDocRoute.tsx @@ -8,5 +8,5 @@ export const ocrAiDocRoute = createRoute({ title: "OCR", description: "Reconnaissance optique de caractères pour l'extraction automatique de données comptables.", }), - component: lazyRouteComponent(() => import("../../../../features/docs/ai/ocrAiDocPage.tsx"), "OcrAiDocPage"), + component: lazyRouteComponent(() => import("../../../../features/docs/ai/OcrAiDocPage.tsx"), "OcrAiDocPage"), }) diff --git a/packages/website/src/routes/root/docs/ai/rootAiDocRoute.tsx b/packages/website/src/routes/root/docs/ai/rootAiDocRoute.tsx index fcaca6ab..bca31325 100644 --- a/packages/website/src/routes/root/docs/ai/rootAiDocRoute.tsx +++ b/packages/website/src/routes/root/docs/ai/rootAiDocRoute.tsx @@ -9,5 +9,5 @@ export const rootAiDocRoute = createRoute({ description: "Documentation de l'assistant IA d'Arrhes : un assistant comptable intelligent pour gérer vos données.", }), - component: lazyRouteComponent(() => import("../../../../features/docs/ai/rootAiDocPage.tsx"), "RootAiDocPage"), + component: lazyRouteComponent(() => import("../../../../features/docs/ai/RootAiDocPage.tsx"), "RootAiDocPage"), }) diff --git a/packages/website/src/routes/root/docs/ai/toolsAiDocRoute.tsx b/packages/website/src/routes/root/docs/ai/toolsAiDocRoute.tsx index f7d0be60..01cec5ca 100644 --- a/packages/website/src/routes/root/docs/ai/toolsAiDocRoute.tsx +++ b/packages/website/src/routes/root/docs/ai/toolsAiDocRoute.tsx @@ -8,5 +8,5 @@ export const toolsAiDocRoute = createRoute({ title: "Outils de l'assistant", description: "Liste complète des outils disponibles pour l'assistant comptable IA d'Arrhes.", }), - component: lazyRouteComponent(() => import("../../../../features/docs/ai/toolsAiDocPage.tsx"), "ToolsAiDocPage"), + component: lazyRouteComponent(() => import("../../../../features/docs/ai/ToolsAiDocPage.tsx"), "ToolsAiDocPage"), }) diff --git a/packages/website/src/routes/root/docs/api/accountsApiDocRoute.tsx b/packages/website/src/routes/root/docs/api/accountsApiDocRoute.tsx new file mode 100644 index 00000000..b7afff31 --- /dev/null +++ b/packages/website/src/routes/root/docs/api/accountsApiDocRoute.tsx @@ -0,0 +1,15 @@ +import { createRoute, lazyRouteComponent } from "@tanstack/react-router" +import { apiDocLayoutRoute } from "./apiDocLayoutRoute.tsx" + +export const accountsApiDocRoute = createRoute({ + getParentRoute: () => apiDocLayoutRoute, + path: "/comptes", + beforeLoad: () => ({ + title: "Comptes", + description: "Endpoints API pour la gestion du plan comptable d'un exercice Arrhes.", + }), + component: lazyRouteComponent( + () => import("../../../../features/docs/api/AccountsApiDocPage.tsx"), + "AccountsApiDocPage", + ), +}) diff --git a/packages/website/src/routes/root/docs/api/apiKeysApiDocRoute.tsx b/packages/website/src/routes/root/docs/api/apiKeysApiDocRoute.tsx new file mode 100644 index 00000000..e3fe99c1 --- /dev/null +++ b/packages/website/src/routes/root/docs/api/apiKeysApiDocRoute.tsx @@ -0,0 +1,15 @@ +import { createRoute, lazyRouteComponent } from "@tanstack/react-router" +import { apiDocLayoutRoute } from "./apiDocLayoutRoute.tsx" + +export const apiKeysApiDocRoute = createRoute({ + getParentRoute: () => apiDocLayoutRoute, + path: "/clés-api", + beforeLoad: () => ({ + title: "Clés API", + description: "Endpoints API pour la gestion des clés d'accès programmatique à l'API Arrhes.", + }), + component: lazyRouteComponent( + () => import("../../../../features/docs/api/ApiKeysApiDocPage.tsx"), + "ApiKeysApiDocPage", + ), +}) diff --git a/packages/website/src/routes/root/docs/api/authenticationApiDocRoute.tsx b/packages/website/src/routes/root/docs/api/authenticationApiDocRoute.tsx index eccaae23..28ee2655 100644 --- a/packages/website/src/routes/root/docs/api/authenticationApiDocRoute.tsx +++ b/packages/website/src/routes/root/docs/api/authenticationApiDocRoute.tsx @@ -10,7 +10,7 @@ export const authenticationApiDocRoute = createRoute({ "Authentification à l'API Arrhes : gestion des clés API, sessions utilisateurs et sécurité des requêtes.", }), component: lazyRouteComponent( - () => import("../../../../features/docs/api/authenticationApiDocPage.tsx"), + () => import("../../../../features/docs/api/AuthenticationApiDocPage.tsx"), "AuthenticationApiDocPage", ), }) diff --git a/packages/website/src/routes/root/docs/api/balanceSheetsApiDocRoute.tsx b/packages/website/src/routes/root/docs/api/balanceSheetsApiDocRoute.tsx new file mode 100644 index 00000000..f6f84ecb --- /dev/null +++ b/packages/website/src/routes/root/docs/api/balanceSheetsApiDocRoute.tsx @@ -0,0 +1,15 @@ +import { createRoute, lazyRouteComponent } from "@tanstack/react-router" +import { apiDocLayoutRoute } from "./apiDocLayoutRoute.tsx" + +export const balanceSheetsApiDocRoute = createRoute({ + getParentRoute: () => apiDocLayoutRoute, + path: "/bilans", + beforeLoad: () => ({ + title: "Bilans", + description: "Endpoints API pour la structure du bilan comptable d'un exercice Arrhes.", + }), + component: lazyRouteComponent( + () => import("../../../../features/docs/api/BalanceSheetsApiDocPage.tsx"), + "BalanceSheetsApiDocPage", + ), +}) diff --git a/packages/website/src/routes/root/docs/api/computationsApiDocRoute.tsx b/packages/website/src/routes/root/docs/api/computationsApiDocRoute.tsx new file mode 100644 index 00000000..53de1eb4 --- /dev/null +++ b/packages/website/src/routes/root/docs/api/computationsApiDocRoute.tsx @@ -0,0 +1,15 @@ +import { createRoute, lazyRouteComponent } from "@tanstack/react-router" +import { apiDocLayoutRoute } from "./apiDocLayoutRoute.tsx" + +export const computationsApiDocRoute = createRoute({ + getParentRoute: () => apiDocLayoutRoute, + path: "/calculs", + beforeLoad: () => ({ + title: "Calculs", + description: "Endpoints API pour la gestion des calculs et formules personnalisées d'un exercice Arrhes.", + }), + component: lazyRouteComponent( + () => import("../../../../features/docs/api/ComputationsApiDocPage.tsx"), + "ComputationsApiDocPage", + ), +}) diff --git a/packages/website/src/routes/root/docs/api/entriesApiDocRoute.tsx b/packages/website/src/routes/root/docs/api/entriesApiDocRoute.tsx new file mode 100644 index 00000000..d574df31 --- /dev/null +++ b/packages/website/src/routes/root/docs/api/entriesApiDocRoute.tsx @@ -0,0 +1,15 @@ +import { createRoute, lazyRouteComponent } from "@tanstack/react-router" +import { apiDocLayoutRoute } from "./apiDocLayoutRoute.tsx" + +export const entriesApiDocRoute = createRoute({ + getParentRoute: () => apiDocLayoutRoute, + path: "/écritures", + beforeLoad: () => ({ + title: "Écritures", + description: "Endpoints API pour la gestion des écritures comptables d'un exercice Arrhes.", + }), + component: lazyRouteComponent( + () => import("../../../../features/docs/api/EntriesApiDocPage.tsx"), + "EntriesApiDocPage", + ), +}) diff --git a/packages/website/src/routes/root/docs/api/entryLinesApiDocRoute.tsx b/packages/website/src/routes/root/docs/api/entryLinesApiDocRoute.tsx new file mode 100644 index 00000000..9f81ecd9 --- /dev/null +++ b/packages/website/src/routes/root/docs/api/entryLinesApiDocRoute.tsx @@ -0,0 +1,15 @@ +import { createRoute, lazyRouteComponent } from "@tanstack/react-router" +import { apiDocLayoutRoute } from "./apiDocLayoutRoute.tsx" + +export const entryLinesApiDocRoute = createRoute({ + getParentRoute: () => apiDocLayoutRoute, + path: "/lignes", + beforeLoad: () => ({ + title: "Lignes d'écriture", + description: "Endpoints API pour la gestion des lignes de débit/crédit et des tags des écritures Arrhes.", + }), + component: lazyRouteComponent( + () => import("../../../../features/docs/api/EntryLinesApiDocPage.tsx"), + "EntryLinesApiDocPage", + ), +}) diff --git a/packages/website/src/routes/root/docs/api/filesApiDocRoute.tsx b/packages/website/src/routes/root/docs/api/filesApiDocRoute.tsx index bca7efb9..9c04ea55 100644 --- a/packages/website/src/routes/root/docs/api/filesApiDocRoute.tsx +++ b/packages/website/src/routes/root/docs/api/filesApiDocRoute.tsx @@ -9,5 +9,5 @@ export const filesApiDocRoute = createRoute({ description: "Endpoints API pour la gestion des fichiers et documents : upload, téléchargement et association aux écritures.", }), - component: lazyRouteComponent(() => import("../../../../features/docs/api/filesApiDocPage.tsx"), "FilesApiDocPage"), + component: lazyRouteComponent(() => import("../../../../features/docs/api/FilesApiDocPage.tsx"), "FilesApiDocPage"), }) diff --git a/packages/website/src/routes/root/docs/api/foldersApiDocRoute.tsx b/packages/website/src/routes/root/docs/api/foldersApiDocRoute.tsx new file mode 100644 index 00000000..6c67c146 --- /dev/null +++ b/packages/website/src/routes/root/docs/api/foldersApiDocRoute.tsx @@ -0,0 +1,15 @@ +import { createRoute, lazyRouteComponent } from "@tanstack/react-router" +import { apiDocLayoutRoute } from "./apiDocLayoutRoute.tsx" + +export const foldersApiDocRoute = createRoute({ + getParentRoute: () => apiDocLayoutRoute, + path: "/dossiers", + beforeLoad: () => ({ + title: "Dossiers", + description: "Endpoints API pour la gestion des dossiers de fichiers d'un exercice Arrhes.", + }), + component: lazyRouteComponent( + () => import("../../../../features/docs/api/FoldersApiDocPage.tsx"), + "FoldersApiDocPage", + ), +}) diff --git a/packages/website/src/routes/root/docs/api/incomeStatementsApiDocRoute.tsx b/packages/website/src/routes/root/docs/api/incomeStatementsApiDocRoute.tsx new file mode 100644 index 00000000..35b0fc9d --- /dev/null +++ b/packages/website/src/routes/root/docs/api/incomeStatementsApiDocRoute.tsx @@ -0,0 +1,15 @@ +import { createRoute, lazyRouteComponent } from "@tanstack/react-router" +import { apiDocLayoutRoute } from "./apiDocLayoutRoute.tsx" + +export const incomeStatementsApiDocRoute = createRoute({ + getParentRoute: () => apiDocLayoutRoute, + path: "/comptes-de-résultat", + beforeLoad: () => ({ + title: "Comptes de résultat", + description: "Endpoints API pour la structure du compte de résultat d'un exercice Arrhes.", + }), + component: lazyRouteComponent( + () => import("../../../../features/docs/api/IncomeStatementsApiDocPage.tsx"), + "IncomeStatementsApiDocPage", + ), +}) diff --git a/packages/website/src/routes/root/docs/api/introductionApiDocRoute.tsx b/packages/website/src/routes/root/docs/api/introductionApiDocRoute.tsx index 9fffbe8f..79a791c2 100644 --- a/packages/website/src/routes/root/docs/api/introductionApiDocRoute.tsx +++ b/packages/website/src/routes/root/docs/api/introductionApiDocRoute.tsx @@ -10,7 +10,7 @@ export const introductionApiDocRoute = createRoute({ "Introduction à l'API Arrhes : authentification, format des requêtes, gestion des erreurs et bonnes pratiques.", }), component: lazyRouteComponent( - () => import("../../../../features/docs/api/introductionApiDocPage.tsx"), + () => import("../../../../features/docs/api/IntroductionApiDocPage.tsx"), "IntroductionApiDocPage", ), }) diff --git a/packages/website/src/routes/root/docs/api/journalsApiDocRoute.tsx b/packages/website/src/routes/root/docs/api/journalsApiDocRoute.tsx new file mode 100644 index 00000000..3d77ab40 --- /dev/null +++ b/packages/website/src/routes/root/docs/api/journalsApiDocRoute.tsx @@ -0,0 +1,15 @@ +import { createRoute, lazyRouteComponent } from "@tanstack/react-router" +import { apiDocLayoutRoute } from "./apiDocLayoutRoute.tsx" + +export const journalsApiDocRoute = createRoute({ + getParentRoute: () => apiDocLayoutRoute, + path: "/journaux", + beforeLoad: () => ({ + title: "Journaux", + description: "Endpoints API pour la gestion des journaux comptables d'un exercice Arrhes.", + }), + component: lazyRouteComponent( + () => import("../../../../features/docs/api/JournalsApiDocPage.tsx"), + "JournalsApiDocPage", + ), +}) diff --git a/packages/website/src/routes/root/docs/api/orgUsersApiDocRoute.tsx b/packages/website/src/routes/root/docs/api/orgUsersApiDocRoute.tsx new file mode 100644 index 00000000..3b8d017d --- /dev/null +++ b/packages/website/src/routes/root/docs/api/orgUsersApiDocRoute.tsx @@ -0,0 +1,15 @@ +import { createRoute, lazyRouteComponent } from "@tanstack/react-router" +import { apiDocLayoutRoute } from "./apiDocLayoutRoute.tsx" + +export const orgUsersApiDocRoute = createRoute({ + getParentRoute: () => apiDocLayoutRoute, + path: "/membres", + beforeLoad: () => ({ + title: "Membres", + description: "Endpoints API pour la gestion des membres d'une organisation Arrhes.", + }), + component: lazyRouteComponent( + () => import("../../../../features/docs/api/OrgUsersApiDocPage.tsx"), + "OrgUsersApiDocPage", + ), +}) diff --git a/packages/website/src/routes/root/docs/api/organizationApiDocRoute.tsx b/packages/website/src/routes/root/docs/api/organizationApiDocRoute.tsx index 4ee4cd0e..ce7cc095 100644 --- a/packages/website/src/routes/root/docs/api/organizationApiDocRoute.tsx +++ b/packages/website/src/routes/root/docs/api/organizationApiDocRoute.tsx @@ -10,7 +10,7 @@ export const organizationApiDocRoute = createRoute({ "Endpoints API pour la gestion des organisations Arrhes : création, lecture, mise à jour et suppression.", }), component: lazyRouteComponent( - () => import("../../../../features/docs/api/organizationApiDocPage.tsx"), + () => import("../../../../features/docs/api/OrganizationApiDocPage.tsx"), "OrganizationApiDocPage", ), }) diff --git a/packages/website/src/routes/root/docs/api/rootApiDocRoute.tsx b/packages/website/src/routes/root/docs/api/rootApiDocRoute.tsx index bbf32b84..2f9cfc4e 100644 --- a/packages/website/src/routes/root/docs/api/rootApiDocRoute.tsx +++ b/packages/website/src/routes/root/docs/api/rootApiDocRoute.tsx @@ -9,5 +9,5 @@ export const rootApiDocRoute = createRoute({ description: "Documentation de l'API Arrhes : endpoints REST pour intégrer la comptabilité dans vos applications.", }), - component: lazyRouteComponent(() => import("../../../../features/docs/api/rootApiDocPage.tsx"), "RootApiDocPage"), + component: lazyRouteComponent(() => import("../../../../features/docs/api/RootApiDocPage.tsx"), "RootApiDocPage"), }) diff --git a/packages/website/src/routes/root/docs/api/tagsApiDocRoute.tsx b/packages/website/src/routes/root/docs/api/tagsApiDocRoute.tsx new file mode 100644 index 00000000..b8c4f0ce --- /dev/null +++ b/packages/website/src/routes/root/docs/api/tagsApiDocRoute.tsx @@ -0,0 +1,12 @@ +import { createRoute, lazyRouteComponent } from "@tanstack/react-router" +import { apiDocLayoutRoute } from "./apiDocLayoutRoute.tsx" + +export const tagsApiDocRoute = createRoute({ + getParentRoute: () => apiDocLayoutRoute, + path: "/libellés", + beforeLoad: () => ({ + title: "Libellés", + description: "Endpoints API pour la gestion des libellés d'écriture d'un exercice Arrhes.", + }), + component: lazyRouteComponent(() => import("../../../../features/docs/api/TagsApiDocPage.tsx"), "TagsApiDocPage"), +}) diff --git a/packages/website/src/routes/root/docs/api/xbrlReportsApiDocRoute.tsx b/packages/website/src/routes/root/docs/api/xbrlReportsApiDocRoute.tsx new file mode 100644 index 00000000..0e92cb12 --- /dev/null +++ b/packages/website/src/routes/root/docs/api/xbrlReportsApiDocRoute.tsx @@ -0,0 +1,15 @@ +import { createRoute, lazyRouteComponent } from "@tanstack/react-router" +import { apiDocLayoutRoute } from "./apiDocLayoutRoute.tsx" + +export const xbrlReportsApiDocRoute = createRoute({ + getParentRoute: () => apiDocLayoutRoute, + path: "/exports", + beforeLoad: () => ({ + title: "Exports", + description: "Endpoints API pour la génération de rapports comptables XBRL (taxonomie ANC française).", + }), + component: lazyRouteComponent( + () => import("../../../../features/docs/api/XBRLReportsApiDocPage.tsx"), + "XBRLReportsApiDocPage", + ), +}) diff --git a/packages/website/src/routes/root/docs/api/yearApiDocRoute.tsx b/packages/website/src/routes/root/docs/api/yearApiDocRoute.tsx index 6eae9b1e..e2b43e6d 100644 --- a/packages/website/src/routes/root/docs/api/yearApiDocRoute.tsx +++ b/packages/website/src/routes/root/docs/api/yearApiDocRoute.tsx @@ -9,5 +9,5 @@ export const yearApiDocRoute = createRoute({ description: "Endpoints API pour la gestion des exercices comptables : création, ouverture, clôture et consultation.", }), - component: lazyRouteComponent(() => import("../../../../features/docs/api/yearApiDocPage.tsx"), "YearApiDocPage"), + component: lazyRouteComponent(() => import("../../../../features/docs/api/YearApiDocPage.tsx"), "YearApiDocPage"), }) diff --git a/packages/website/src/routes/root/docs/cli/authenticationCliDocRoute.tsx b/packages/website/src/routes/root/docs/cli/authenticationCliDocRoute.tsx new file mode 100644 index 00000000..9cdaea3d --- /dev/null +++ b/packages/website/src/routes/root/docs/cli/authenticationCliDocRoute.tsx @@ -0,0 +1,16 @@ +import { createRoute, lazyRouteComponent } from "@tanstack/react-router" +import { cliDocLayoutRoute } from "./cliDocLayoutRoute.js" + +export const authenticationCliDocRoute = createRoute({ + getParentRoute: () => cliDocLayoutRoute, + path: "/authentification", + beforeLoad: () => ({ + title: "Authentification", + description: + "Connectez le CLI Arrhes à votre compte via une clé API et configurez votre organisation par défaut.", + }), + component: lazyRouteComponent( + () => import("../../../../features/docs/cli/AuthenticationCliDocPage.js"), + "AuthenticationCliDocPage", + ), +}) diff --git a/packages/website/src/routes/root/docs/cli/cliDocLayoutRoute.tsx b/packages/website/src/routes/root/docs/cli/cliDocLayoutRoute.tsx new file mode 100644 index 00000000..745f6bca --- /dev/null +++ b/packages/website/src/routes/root/docs/cli/cliDocLayoutRoute.tsx @@ -0,0 +1,8 @@ +import { createRoute, Outlet } from "@tanstack/react-router" +import { docsLayoutRoute } from "../docsLayoutRoute.js" + +export const cliDocLayoutRoute = createRoute({ + getParentRoute: () => docsLayoutRoute, + path: "/cli", + component: () => , +}) diff --git a/packages/website/src/routes/root/docs/cli/commands/balanceSheetsCommandsCliDocRoute.tsx b/packages/website/src/routes/root/docs/cli/commands/balanceSheetsCommandsCliDocRoute.tsx new file mode 100644 index 00000000..ddc1d816 --- /dev/null +++ b/packages/website/src/routes/root/docs/cli/commands/balanceSheetsCommandsCliDocRoute.tsx @@ -0,0 +1,15 @@ +import { createRoute, lazyRouteComponent } from "@tanstack/react-router" +import { commandsCliDocLayoutRoute } from "../commandsCliDocRoute.js" + +export const balanceSheetsCommandsCliDocRoute = createRoute({ + getParentRoute: () => commandsCliDocLayoutRoute, + path: "/bilans", + beforeLoad: () => ({ + title: "Bilans", + description: "Commandes de gestion de la structure du bilan comptable : arrhes balance-sheets.", + }), + component: lazyRouteComponent( + () => import("../../../../../features/docs/cli/commands/BalanceSheetsCommandsCliDocPage.js"), + "BalanceSheetsCommandsCliDocPage", + ), +}) diff --git a/packages/website/src/routes/root/docs/cli/commands/clesApiCommandsCliDocRoute.tsx b/packages/website/src/routes/root/docs/cli/commands/clesApiCommandsCliDocRoute.tsx new file mode 100644 index 00000000..c579bd7b --- /dev/null +++ b/packages/website/src/routes/root/docs/cli/commands/clesApiCommandsCliDocRoute.tsx @@ -0,0 +1,15 @@ +import { createRoute, lazyRouteComponent } from "@tanstack/react-router" +import { commandsCliDocLayoutRoute } from "../commandsCliDocRoute.js" + +export const clesApiCommandsCliDocRoute = createRoute({ + getParentRoute: () => commandsCliDocLayoutRoute, + path: "/cles-api", + beforeLoad: () => ({ + title: "Clés API", + description: "Commandes de gestion des clés API : arrhes api-keys.", + }), + component: lazyRouteComponent( + () => import("../../../../../features/docs/cli/commands/ClesApiCommandsCliDocPage.js"), + "ClesApiCommandsCliDocPage", + ), +}) diff --git a/packages/website/src/routes/root/docs/cli/commands/commandsCliDocTree.ts b/packages/website/src/routes/root/docs/cli/commands/commandsCliDocTree.ts new file mode 100644 index 00000000..967a51a4 --- /dev/null +++ b/packages/website/src/routes/root/docs/cli/commands/commandsCliDocTree.ts @@ -0,0 +1,29 @@ +import type { AnyRoute } from "@tanstack/react-router" +import { commandsCliDocLayoutRoute } from "../commandsCliDocRoute.js" +import { balanceSheetsCommandsCliDocRoute } from "./balanceSheetsCommandsCliDocRoute.js" +import { clesApiCommandsCliDocRoute } from "./clesApiCommandsCliDocRoute.js" +import { comptesCommandsCliDocRoute } from "./comptesCommandsCliDocRoute.js" +import { ecrituresCommandsCliDocRoute } from "./ecrituresCommandsCliDocRoute.js" +import { exercicesCommandsCliDocRoute } from "./exercicesCommandsCliDocRoute.js" +import { exportsCommandsCliDocRoute } from "./exportsCommandsCliDocRoute.js" +import { fichiersCommandsCliDocRoute } from "./fichiersCommandsCliDocRoute.js" +import { incomeStatementsCommandsCliDocRoute } from "./incomeStatementsCommandsCliDocRoute.js" +import { journauxCommandsCliDocRoute } from "./journauxCommandsCliDocRoute.js" +import { libellesCommandsCliDocRoute } from "./libellesCommandsCliDocRoute.js" +import { membresCommandsCliDocRoute } from "./membresCommandsCliDocRoute.js" +import { orgCommandsCliDocRoute } from "./orgCommandsCliDocRoute.js" + +export const commandsCliDocTree: AnyRoute = commandsCliDocLayoutRoute.addChildren([ + orgCommandsCliDocRoute, + exercicesCommandsCliDocRoute, + journauxCommandsCliDocRoute, + comptesCommandsCliDocRoute, + libellesCommandsCliDocRoute, + ecrituresCommandsCliDocRoute, + fichiersCommandsCliDocRoute, + membresCommandsCliDocRoute, + clesApiCommandsCliDocRoute, + exportsCommandsCliDocRoute, + balanceSheetsCommandsCliDocRoute, + incomeStatementsCommandsCliDocRoute, +]) diff --git a/packages/website/src/routes/root/docs/cli/commands/comptesCommandsCliDocRoute.tsx b/packages/website/src/routes/root/docs/cli/commands/comptesCommandsCliDocRoute.tsx new file mode 100644 index 00000000..2702f412 --- /dev/null +++ b/packages/website/src/routes/root/docs/cli/commands/comptesCommandsCliDocRoute.tsx @@ -0,0 +1,15 @@ +import { createRoute, lazyRouteComponent } from "@tanstack/react-router" +import { commandsCliDocLayoutRoute } from "../commandsCliDocRoute.js" + +export const comptesCommandsCliDocRoute = createRoute({ + getParentRoute: () => commandsCliDocLayoutRoute, + path: "/comptes", + beforeLoad: () => ({ + title: "Comptes", + description: "Commandes de gestion du plan comptable : arrhes accounts.", + }), + component: lazyRouteComponent( + () => import("../../../../../features/docs/cli/commands/ComptesCommandsCliDocPage.js"), + "ComptesCommandsCliDocPage", + ), +}) diff --git a/packages/website/src/routes/root/docs/cli/commands/ecrituresCommandsCliDocRoute.tsx b/packages/website/src/routes/root/docs/cli/commands/ecrituresCommandsCliDocRoute.tsx new file mode 100644 index 00000000..84668e7c --- /dev/null +++ b/packages/website/src/routes/root/docs/cli/commands/ecrituresCommandsCliDocRoute.tsx @@ -0,0 +1,15 @@ +import { createRoute, lazyRouteComponent } from "@tanstack/react-router" +import { commandsCliDocLayoutRoute } from "../commandsCliDocRoute.js" + +export const ecrituresCommandsCliDocRoute = createRoute({ + getParentRoute: () => commandsCliDocLayoutRoute, + path: "/ecritures", + beforeLoad: () => ({ + title: "Écritures", + description: "Commandes de gestion des écritures comptables : arrhes entries list, get, create et delete.", + }), + component: lazyRouteComponent( + () => import("../../../../../features/docs/cli/commands/EcrituresCommandsCliDocPage.js"), + "EcrituresCommandsCliDocPage", + ), +}) diff --git a/packages/website/src/routes/root/docs/cli/commands/exercicesCommandsCliDocRoute.tsx b/packages/website/src/routes/root/docs/cli/commands/exercicesCommandsCliDocRoute.tsx new file mode 100644 index 00000000..22569a24 --- /dev/null +++ b/packages/website/src/routes/root/docs/cli/commands/exercicesCommandsCliDocRoute.tsx @@ -0,0 +1,15 @@ +import { createRoute, lazyRouteComponent } from "@tanstack/react-router" +import { commandsCliDocLayoutRoute } from "../commandsCliDocRoute.js" + +export const exercicesCommandsCliDocRoute = createRoute({ + getParentRoute: () => commandsCliDocLayoutRoute, + path: "/exercices", + beforeLoad: () => ({ + title: "Exercices", + description: "Commandes de gestion des exercices comptables : arrhes years list, get et create.", + }), + component: lazyRouteComponent( + () => import("../../../../../features/docs/cli/commands/ExercicesCommandsCliDocPage.js"), + "ExercicesCommandsCliDocPage", + ), +}) diff --git a/packages/website/src/routes/root/docs/cli/commands/exportsCommandsCliDocRoute.tsx b/packages/website/src/routes/root/docs/cli/commands/exportsCommandsCliDocRoute.tsx new file mode 100644 index 00000000..34323ae0 --- /dev/null +++ b/packages/website/src/routes/root/docs/cli/commands/exportsCommandsCliDocRoute.tsx @@ -0,0 +1,15 @@ +import { createRoute, lazyRouteComponent } from "@tanstack/react-router" +import { commandsCliDocLayoutRoute } from "../commandsCliDocRoute.js" + +export const exportsCommandsCliDocRoute = createRoute({ + getParentRoute: () => commandsCliDocLayoutRoute, + path: "/exports", + beforeLoad: () => ({ + title: "Exports", + description: "Commandes d'export comptable : arrhes exports.", + }), + component: lazyRouteComponent( + () => import("../../../../../features/docs/cli/commands/ExportsCommandsCliDocPage.js"), + "ExportsCommandsCliDocPage", + ), +}) diff --git a/packages/website/src/routes/root/docs/cli/commands/fichiersCommandsCliDocRoute.tsx b/packages/website/src/routes/root/docs/cli/commands/fichiersCommandsCliDocRoute.tsx new file mode 100644 index 00000000..7f23ca71 --- /dev/null +++ b/packages/website/src/routes/root/docs/cli/commands/fichiersCommandsCliDocRoute.tsx @@ -0,0 +1,15 @@ +import { createRoute, lazyRouteComponent } from "@tanstack/react-router" +import { commandsCliDocLayoutRoute } from "../commandsCliDocRoute.js" + +export const fichiersCommandsCliDocRoute = createRoute({ + getParentRoute: () => commandsCliDocLayoutRoute, + path: "/stockage", + beforeLoad: () => ({ + title: "Stockage", + description: "Commandes de gestion des fichiers et dossiers d'un exercice : arrhes files.", + }), + component: lazyRouteComponent( + () => import("../../../../../features/docs/cli/commands/FichiersCommandsCliDocPage.js"), + "FichiersCommandsCliDocPage", + ), +}) diff --git a/packages/website/src/routes/root/docs/cli/commands/incomeStatementsCommandsCliDocRoute.tsx b/packages/website/src/routes/root/docs/cli/commands/incomeStatementsCommandsCliDocRoute.tsx new file mode 100644 index 00000000..388124a4 --- /dev/null +++ b/packages/website/src/routes/root/docs/cli/commands/incomeStatementsCommandsCliDocRoute.tsx @@ -0,0 +1,16 @@ +import { createRoute, lazyRouteComponent } from "@tanstack/react-router" +import { commandsCliDocLayoutRoute } from "../commandsCliDocRoute.js" + +export const incomeStatementsCommandsCliDocRoute = createRoute({ + getParentRoute: () => commandsCliDocLayoutRoute, + path: "/comptes-de-resultat", + beforeLoad: () => ({ + title: "Comptes de résultat", + description: + "Commandes de gestion de la structure du compte de résultat et des calculs : arrhes income-statements.", + }), + component: lazyRouteComponent( + () => import("../../../../../features/docs/cli/commands/IncomeStatementsCommandsCliDocPage.js"), + "IncomeStatementsCommandsCliDocPage", + ), +}) diff --git a/packages/website/src/routes/root/docs/cli/commands/journauxCommandsCliDocRoute.tsx b/packages/website/src/routes/root/docs/cli/commands/journauxCommandsCliDocRoute.tsx new file mode 100644 index 00000000..9d3abc41 --- /dev/null +++ b/packages/website/src/routes/root/docs/cli/commands/journauxCommandsCliDocRoute.tsx @@ -0,0 +1,15 @@ +import { createRoute, lazyRouteComponent } from "@tanstack/react-router" +import { commandsCliDocLayoutRoute } from "../commandsCliDocRoute.js" + +export const journauxCommandsCliDocRoute = createRoute({ + getParentRoute: () => commandsCliDocLayoutRoute, + path: "/journaux", + beforeLoad: () => ({ + title: "Journaux", + description: "Commandes de gestion des journaux comptables : arrhes journals.", + }), + component: lazyRouteComponent( + () => import("../../../../../features/docs/cli/commands/JournauxCommandsCliDocPage.js"), + "JournauxCommandsCliDocPage", + ), +}) diff --git a/packages/website/src/routes/root/docs/cli/commands/libellesCommandsCliDocRoute.tsx b/packages/website/src/routes/root/docs/cli/commands/libellesCommandsCliDocRoute.tsx new file mode 100644 index 00000000..2da28fbb --- /dev/null +++ b/packages/website/src/routes/root/docs/cli/commands/libellesCommandsCliDocRoute.tsx @@ -0,0 +1,15 @@ +import { createRoute, lazyRouteComponent } from "@tanstack/react-router" +import { commandsCliDocLayoutRoute } from "../commandsCliDocRoute.js" + +export const libellesCommandsCliDocRoute = createRoute({ + getParentRoute: () => commandsCliDocLayoutRoute, + path: "/libelles", + beforeLoad: () => ({ + title: "Libellés", + description: "Commandes de gestion des libellés (tags) : arrhes tags.", + }), + component: lazyRouteComponent( + () => import("../../../../../features/docs/cli/commands/LibellesCommandsCliDocPage.js"), + "LibellesCommandsCliDocPage", + ), +}) diff --git a/packages/website/src/routes/root/docs/cli/commands/membresCommandsCliDocRoute.tsx b/packages/website/src/routes/root/docs/cli/commands/membresCommandsCliDocRoute.tsx new file mode 100644 index 00000000..324c544f --- /dev/null +++ b/packages/website/src/routes/root/docs/cli/commands/membresCommandsCliDocRoute.tsx @@ -0,0 +1,15 @@ +import { createRoute, lazyRouteComponent } from "@tanstack/react-router" +import { commandsCliDocLayoutRoute } from "../commandsCliDocRoute.js" + +export const membresCommandsCliDocRoute = createRoute({ + getParentRoute: () => commandsCliDocLayoutRoute, + path: "/membres", + beforeLoad: () => ({ + title: "Membres", + description: "Commandes de gestion des membres d'une organisation : arrhes members.", + }), + component: lazyRouteComponent( + () => import("../../../../../features/docs/cli/commands/MembresCommandsCliDocPage.js"), + "MembresCommandsCliDocPage", + ), +}) diff --git a/packages/website/src/routes/root/docs/cli/commands/orgCommandsCliDocRoute.tsx b/packages/website/src/routes/root/docs/cli/commands/orgCommandsCliDocRoute.tsx new file mode 100644 index 00000000..b8c1b711 --- /dev/null +++ b/packages/website/src/routes/root/docs/cli/commands/orgCommandsCliDocRoute.tsx @@ -0,0 +1,15 @@ +import { createRoute, lazyRouteComponent } from "@tanstack/react-router" +import { commandsCliDocLayoutRoute } from "../commandsCliDocRoute.js" + +export const orgCommandsCliDocRoute = createRoute({ + getParentRoute: () => commandsCliDocLayoutRoute, + path: "/organisation", + beforeLoad: () => ({ + title: "Organisation", + description: "Commandes de gestion de l'organisation configurée : arrhes org.", + }), + component: lazyRouteComponent( + () => import("../../../../../features/docs/cli/commands/OrgCommandsCliDocPage.js"), + "OrgCommandsCliDocPage", + ), +}) diff --git a/packages/website/src/routes/root/docs/cli/commandsCliDocRoute.tsx b/packages/website/src/routes/root/docs/cli/commandsCliDocRoute.tsx new file mode 100644 index 00000000..adbaaed9 --- /dev/null +++ b/packages/website/src/routes/root/docs/cli/commandsCliDocRoute.tsx @@ -0,0 +1,8 @@ +import { createRoute, Outlet } from "@tanstack/react-router" +import { cliDocLayoutRoute } from "./cliDocLayoutRoute.js" + +export const commandsCliDocLayoutRoute = createRoute({ + getParentRoute: () => cliDocLayoutRoute, + path: "/commandes", + component: () => , +}) diff --git a/packages/website/src/routes/root/docs/cli/demarrerCliDocRoute.tsx b/packages/website/src/routes/root/docs/cli/demarrerCliDocRoute.tsx new file mode 100644 index 00000000..9caf8108 --- /dev/null +++ b/packages/website/src/routes/root/docs/cli/demarrerCliDocRoute.tsx @@ -0,0 +1,15 @@ +import { createRoute, lazyRouteComponent } from "@tanstack/react-router" +import { cliDocLayoutRoute } from "./cliDocLayoutRoute.js" + +export const demarrerCliDocRoute = createRoute({ + getParentRoute: () => cliDocLayoutRoute, + path: "/demarrer", + beforeLoad: () => ({ + title: "Démarrer", + description: "Mettre en place le CLI Arrhes et effectuer votre première opération en moins de 5 minutes.", + }), + component: lazyRouteComponent( + () => import("../../../../features/docs/cli/DemarrerCliDocPage.js"), + "DemarrerCliDocPage", + ), +}) diff --git a/packages/website/src/routes/root/docs/cli/installationCliDocRoute.tsx b/packages/website/src/routes/root/docs/cli/installationCliDocRoute.tsx new file mode 100644 index 00000000..1a578e0d --- /dev/null +++ b/packages/website/src/routes/root/docs/cli/installationCliDocRoute.tsx @@ -0,0 +1,15 @@ +import { createRoute, lazyRouteComponent } from "@tanstack/react-router" +import { cliDocLayoutRoute } from "./cliDocLayoutRoute.js" + +export const installationCliDocRoute = createRoute({ + getParentRoute: () => cliDocLayoutRoute, + path: "/installation", + beforeLoad: () => ({ + title: "Installation", + description: "Installez le CLI Arrhes sur macOS et Linux en une commande. Seul curl est requis.", + }), + component: lazyRouteComponent( + () => import("../../../../features/docs/cli/InstallationCliDocPage.js"), + "InstallationCliDocPage", + ), +}) diff --git a/packages/website/src/routes/root/docs/cli/rootCliDocRoute.tsx b/packages/website/src/routes/root/docs/cli/rootCliDocRoute.tsx new file mode 100644 index 00000000..1ba46fc4 --- /dev/null +++ b/packages/website/src/routes/root/docs/cli/rootCliDocRoute.tsx @@ -0,0 +1,13 @@ +import { createRoute, lazyRouteComponent } from "@tanstack/react-router" +import { cliDocLayoutRoute } from "./cliDocLayoutRoute.js" + +export const rootCliDocRoute = createRoute({ + getParentRoute: () => cliDocLayoutRoute, + path: "/", + beforeLoad: () => ({ + title: "CLI", + description: + "Présentation de l'interface en ligne de commande Arrhes : automatisez la gestion de votre comptabilité depuis le terminal.", + }), + component: lazyRouteComponent(() => import("../../../../features/docs/cli/RootCliDocPage.js"), "RootCliDocPage"), +}) diff --git a/packages/website/src/routes/root/docs/dashboard/entriesDashboardDocRoute.tsx b/packages/website/src/routes/root/docs/dashboard/entriesDashboardDocRoute.tsx index 19ecd97c..d4ee76e7 100644 --- a/packages/website/src/routes/root/docs/dashboard/entriesDashboardDocRoute.tsx +++ b/packages/website/src/routes/root/docs/dashboard/entriesDashboardDocRoute.tsx @@ -10,7 +10,7 @@ export const entriesDashboardDocRoute = createRoute({ "Saisissez et gérez vos écritures comptables dans Arrhes : création, modification, suppression et recherche d'écritures.", }), component: lazyRouteComponent( - () => import("../../../../features/docs/dashboard/entriesDashboardDocPage.js"), + () => import("../../../../features/docs/dashboard/EntriesDashboardDocPage.js"), "EntriesDashboardDocPage", ), }) diff --git a/packages/website/src/routes/root/docs/dashboard/filesDashboardDocRoute.tsx b/packages/website/src/routes/root/docs/dashboard/filesDashboardDocRoute.tsx index 02daf378..53b43ef5 100644 --- a/packages/website/src/routes/root/docs/dashboard/filesDashboardDocRoute.tsx +++ b/packages/website/src/routes/root/docs/dashboard/filesDashboardDocRoute.tsx @@ -10,7 +10,7 @@ export const filesDashboardDocRoute = createRoute({ "Gérez vos pièces justificatives dans Arrhes : importation, association aux écritures et stockage sécurisé de vos documents.", }), component: lazyRouteComponent( - () => import("../../../../features/docs/dashboard/filesDashboardDocPage.js"), + () => import("../../../../features/docs/dashboard/FilesDashboardDocPage.js"), "FilesDashboardDocPage", ), }) diff --git a/packages/website/src/routes/root/docs/dashboard/gettingStartedDashboardDocRoute.tsx b/packages/website/src/routes/root/docs/dashboard/gettingStartedDashboardDocRoute.tsx index b5b114fd..c07d3c16 100644 --- a/packages/website/src/routes/root/docs/dashboard/gettingStartedDashboardDocRoute.tsx +++ b/packages/website/src/routes/root/docs/dashboard/gettingStartedDashboardDocRoute.tsx @@ -10,7 +10,7 @@ export const gettingStartedDashboardDocRoute = createRoute({ "Guide de démarrage rapide d'Arrhes : créez votre compte, configurez votre première organisation et commencez votre comptabilité.", }), component: lazyRouteComponent( - () => import("../../../../features/docs/dashboard/gettingStartedDashboardDocPage.tsx"), + () => import("../../../../features/docs/dashboard/GettingStartedDashboardDocPage.tsx"), "GettingStartedDashboardDocPage", ), }) diff --git a/packages/website/src/routes/root/docs/dashboard/organizationsDashboardDocRoute.tsx b/packages/website/src/routes/root/docs/dashboard/organizationsDashboardDocRoute.tsx index e2c83d73..b1057ea9 100644 --- a/packages/website/src/routes/root/docs/dashboard/organizationsDashboardDocRoute.tsx +++ b/packages/website/src/routes/root/docs/dashboard/organizationsDashboardDocRoute.tsx @@ -10,7 +10,7 @@ export const organizationsDashboardDocRoute = createRoute({ "Gérez vos organisations dans Arrhes : création, paramétrage, gestion des membres et des abonnements.", }), component: lazyRouteComponent( - () => import("../../../../features/docs/dashboard/organizationsDashboardDocPage.js"), + () => import("../../../../features/docs/dashboard/OrganizationsDashboardDocPage.js"), "OrganizationsDashboardDocPage", ), }) diff --git a/packages/website/src/routes/root/docs/dashboard/reportsDashboardDocRoute.tsx b/packages/website/src/routes/root/docs/dashboard/reportsDashboardDocRoute.tsx index 1ef418be..c4731822 100644 --- a/packages/website/src/routes/root/docs/dashboard/reportsDashboardDocRoute.tsx +++ b/packages/website/src/routes/root/docs/dashboard/reportsDashboardDocRoute.tsx @@ -10,7 +10,7 @@ export const reportsDashboardDocRoute = createRoute({ "Générez vos documents comptables de synthèse dans Arrhes : journal, grand livre, balance, bilan et compte de résultat.", }), component: lazyRouteComponent( - () => import("../../../../features/docs/dashboard/reportsDashboardDocPage.tsx"), + () => import("../../../../features/docs/dashboard/ReportsDashboardDocPage.tsx"), "ReportsDashboardDocPage", ), }) diff --git a/packages/website/src/routes/root/docs/dashboard/rootDashboardDocRoute.tsx b/packages/website/src/routes/root/docs/dashboard/rootDashboardDocRoute.tsx index 15b48322..e7f347eb 100644 --- a/packages/website/src/routes/root/docs/dashboard/rootDashboardDocRoute.tsx +++ b/packages/website/src/routes/root/docs/dashboard/rootDashboardDocRoute.tsx @@ -10,7 +10,7 @@ export const rootDashboardDocRoute = createRoute({ "Guide d'utilisation du dashboard Arrhes : créez vos organisations, gérez vos exercices et saisissez vos écritures comptables.", }), component: lazyRouteComponent( - () => import("../../../../features/docs/dashboard/rootDashboardDocPage.tsx"), + () => import("../../../../features/docs/dashboard/RootDashboardDocPage.tsx"), "RootDashboardDocPage", ), }) diff --git a/packages/website/src/routes/root/docs/dashboard/yearsDashboardDocRoute.tsx b/packages/website/src/routes/root/docs/dashboard/yearsDashboardDocRoute.tsx index cb617934..de7a0391 100644 --- a/packages/website/src/routes/root/docs/dashboard/yearsDashboardDocRoute.tsx +++ b/packages/website/src/routes/root/docs/dashboard/yearsDashboardDocRoute.tsx @@ -10,7 +10,7 @@ export const yearsDashboardDocRoute = createRoute({ "Gérez vos exercices comptables dans Arrhes : création, ouverture, clôture et paramétrage des périodes fiscales.", }), component: lazyRouteComponent( - () => import("../../../../features/docs/dashboard/yearsDashboardDocPage.js"), + () => import("../../../../features/docs/dashboard/YearsDashboardDocPage.js"), "YearsDashboardDocPage", ), }) diff --git a/packages/website/src/routes/root/docs/docsLayoutRoute.tsx b/packages/website/src/routes/root/docs/docsLayoutRoute.tsx index ab540ba8..4ea39288 100644 --- a/packages/website/src/routes/root/docs/docsLayoutRoute.tsx +++ b/packages/website/src/routes/root/docs/docsLayoutRoute.tsx @@ -12,5 +12,5 @@ export const docsLayoutRoute = createRoute({ description: "Documentation complète d'Arrhes : guide d'utilisation, cours de comptabilité, référence API et informations générales.", }), - component: lazyRouteComponent(() => import("../../../features/docs/docsLayout.js"), "DocsLayout"), + component: lazyRouteComponent(() => import("../../../features/docs/DocsLayout.js"), "DocsLayout"), }) diff --git a/packages/website/src/routes/root/docs/docsTree.ts b/packages/website/src/routes/root/docs/docsTree.ts index 9884406e..c9005f27 100644 --- a/packages/website/src/routes/root/docs/docsTree.ts +++ b/packages/website/src/routes/root/docs/docsTree.ts @@ -9,13 +9,28 @@ import { modelsAiDocRoute } from "./ai/modelsAiDocRoute.js" import { ocrAiDocRoute } from "./ai/ocrAiDocRoute.js" import { rootAiDocRoute } from "./ai/rootAiDocRoute.js" import { toolsAiDocRoute } from "./ai/toolsAiDocRoute.js" +import { accountsApiDocRoute } from "./api/accountsApiDocRoute.js" import { apiDocLayoutRoute } from "./api/apiDocLayoutRoute.js" +import { apiKeysApiDocRoute } from "./api/apiKeysApiDocRoute.js" import { authenticationApiDocRoute } from "./api/authenticationApiDocRoute.js" +import { balanceSheetsApiDocRoute } from "./api/balanceSheetsApiDocRoute.js" +import { entriesApiDocRoute } from "./api/entriesApiDocRoute.js" import { filesApiDocRoute } from "./api/filesApiDocRoute.js" +import { incomeStatementsApiDocRoute } from "./api/incomeStatementsApiDocRoute.js" import { introductionApiDocRoute } from "./api/introductionApiDocRoute.js" +import { journalsApiDocRoute } from "./api/journalsApiDocRoute.js" import { organizationApiDocRoute } from "./api/organizationApiDocRoute.js" +import { orgUsersApiDocRoute } from "./api/orgUsersApiDocRoute.js" import { rootApiDocRoute } from "./api/rootApiDocRoute.js" +import { tagsApiDocRoute } from "./api/tagsApiDocRoute.js" +import { xbrlReportsApiDocRoute } from "./api/xbrlReportsApiDocRoute.js" import { yearApiDocRoute } from "./api/yearApiDocRoute.js" +import { authenticationCliDocRoute } from "./cli/authenticationCliDocRoute.js" +import { cliDocLayoutRoute } from "./cli/cliDocLayoutRoute.js" +import { commandsCliDocTree } from "./cli/commands/commandsCliDocTree.js" +import { demarrerCliDocRoute } from "./cli/demarrerCliDocRoute.js" +import { installationCliDocRoute } from "./cli/installationCliDocRoute.js" +import { rootCliDocRoute } from "./cli/rootCliDocRoute.js" import { billingDashboardDocRoute } from "./dashboard/BillingDashboardDocRoute.js" import { dashboardDocLayoutRoute } from "./dashboard/dashboardDocLayoutRoute.js" import { entriesDashboardDocRoute } from "./dashboard/entriesDashboardDocRoute.js" @@ -27,6 +42,7 @@ import { rootDashboardDocRoute } from "./dashboard/rootDashboardDocRoute.js" import { updatesDashboardDocRoute } from "./dashboard/updatesDashboardDocRoute.js" import { yearsDashboardDocRoute } from "./dashboard/yearsDashboardDocRoute.js" import { docsLayoutRoute } from "./docsLayoutRoute.js" +import { architectureGeneralDocRoute } from "./root/architectureGeneralDocRoute.js" import { featuresGeneralDocRoute } from "./root/featuresGeneralDocRoute.js" import { generalDocLayoutRoute } from "./root/generalDocLayoutRoute.js" import { legalGeneralDocRoute } from "./root/legalGeneralDocRoute.js" @@ -42,6 +58,7 @@ export const docsTree: AnyRoute = docsLayoutRoute.addChildren([ generalDocLayoutRoute.addChildren([ rootGeneralDocRoute, featuresGeneralDocRoute, + architectureGeneralDocRoute, pricingGeneralDocRoute, whitepaperGeneralDocRoute, supportGeneralDocRoute, @@ -58,6 +75,15 @@ export const docsTree: AnyRoute = docsLayoutRoute.addChildren([ resourcesAccountingTree, ]), + // CLI section + cliDocLayoutRoute.addChildren([ + rootCliDocRoute, + installationCliDocRoute, + demarrerCliDocRoute, + authenticationCliDocRoute, + commandsCliDocTree, + ]), + // Dashboard section (guide d'utilisation) dashboardDocLayoutRoute.addChildren([ rootDashboardDocRoute, @@ -84,7 +110,16 @@ export const docsTree: AnyRoute = docsLayoutRoute.addChildren([ introductionApiDocRoute, authenticationApiDocRoute, organizationApiDocRoute, - yearApiDocRoute, + apiKeysApiDocRoute, + orgUsersApiDocRoute, filesApiDocRoute, + yearApiDocRoute, + accountsApiDocRoute, + journalsApiDocRoute, + balanceSheetsApiDocRoute, + incomeStatementsApiDocRoute, + tagsApiDocRoute, + entriesApiDocRoute, + xbrlReportsApiDocRoute, ]), ]) diff --git a/packages/website/src/routes/root/docs/root/architectureGeneralDocRoute.tsx b/packages/website/src/routes/root/docs/root/architectureGeneralDocRoute.tsx new file mode 100644 index 00000000..2274ec3a --- /dev/null +++ b/packages/website/src/routes/root/docs/root/architectureGeneralDocRoute.tsx @@ -0,0 +1,16 @@ +import { createRoute, lazyRouteComponent } from "@tanstack/react-router" +import { generalDocLayoutRoute } from "./generalDocLayoutRoute.tsx" + +export const architectureGeneralDocRoute = createRoute({ + getParentRoute: () => generalDocLayoutRoute, + path: "/architecture", + beforeLoad: () => ({ + title: "Architecture", + description: + "Vue d'ensemble de l'architecture technique d'Arrhes : Website, API, CLI et infrastructure (PostgreSQL, Stockage S3, SMTP).", + }), + component: lazyRouteComponent( + () => import("../../../../features/docs/general/architecture/ArchitectureGeneralDocPage.tsx"), + "ArchitectureGeneralDocPage", + ), +}) diff --git a/packages/website/src/routes/root/docs/root/featuresGeneralDocRoute.tsx b/packages/website/src/routes/root/docs/root/featuresGeneralDocRoute.tsx index c1298aca..8bfde20a 100644 --- a/packages/website/src/routes/root/docs/root/featuresGeneralDocRoute.tsx +++ b/packages/website/src/routes/root/docs/root/featuresGeneralDocRoute.tsx @@ -10,7 +10,7 @@ export const featuresGeneralDocRoute = createRoute({ "Découvrez les fonctionnalités d'Arrhes : saisie d'écritures, plan comptable, documents de synthèse, gestion multi-organisations et plus.", }), component: lazyRouteComponent( - () => import("../../../../features/docs/general/features/featuresGeneralDocPage.tsx"), + () => import("../../../../features/docs/general/features/FeaturesGeneralDocPage.tsx"), "FeaturesGeneralDocPage", ), }) diff --git a/packages/website/src/routes/root/docs/root/legalGeneralDocRoute.tsx b/packages/website/src/routes/root/docs/root/legalGeneralDocRoute.tsx index 82a643f9..eb7da105 100644 --- a/packages/website/src/routes/root/docs/root/legalGeneralDocRoute.tsx +++ b/packages/website/src/routes/root/docs/root/legalGeneralDocRoute.tsx @@ -9,7 +9,7 @@ export const legalGeneralDocRoute = createRoute({ description: "Mentions légales du logiciel de comptabilité Arrhes. Informations sur l'éditeur et l'hébergeur.", }), component: lazyRouteComponent( - () => import("../../../../features/docs/general/legalGeneralDocPage.tsx"), + () => import("../../../../features/docs/general/LegalGeneralDocPage.tsx"), "LegalGeneralDocPage", ), }) diff --git a/packages/website/src/routes/root/docs/root/pricingGeneralDocRoute.tsx b/packages/website/src/routes/root/docs/root/pricingGeneralDocRoute.tsx index ae7cab04..5acf6bbc 100644 --- a/packages/website/src/routes/root/docs/root/pricingGeneralDocRoute.tsx +++ b/packages/website/src/routes/root/docs/root/pricingGeneralDocRoute.tsx @@ -10,7 +10,7 @@ export const pricingGeneralDocRoute = createRoute({ "Consultez les tarifs d'Arrhes. Plan basique gratuit et services payants optionnels (licence, stockage, OCR et tokens IA).", }), component: lazyRouteComponent( - () => import("../../../../features/docs/general/pricing/pricingGeneralDocPage.tsx"), + () => import("../../../../features/docs/general/pricing/PricingGeneralDocPage.tsx"), "PricingGeneralDocPage", ), }) diff --git a/packages/website/src/routes/root/docs/root/privacyGeneralDocRoute.tsx b/packages/website/src/routes/root/docs/root/privacyGeneralDocRoute.tsx index a8881a16..f39f85b9 100644 --- a/packages/website/src/routes/root/docs/root/privacyGeneralDocRoute.tsx +++ b/packages/website/src/routes/root/docs/root/privacyGeneralDocRoute.tsx @@ -10,7 +10,7 @@ export const privacyGeneralDocRoute = createRoute({ "Politique de confidentialité d'Arrhes. Découvrez comment nous protégeons vos données personnelles et comptables.", }), component: lazyRouteComponent( - () => import("../../../../features/docs/general/privacyGeneralDocPage.tsx"), + () => import("../../../../features/docs/general/PrivacyGeneralDocPage.tsx"), "PrivacyGeneralDocPage", ), }) diff --git a/packages/website/src/routes/root/docs/root/rootGeneralDocRoute.tsx b/packages/website/src/routes/root/docs/root/rootGeneralDocRoute.tsx index 30cbef83..57876f0b 100644 --- a/packages/website/src/routes/root/docs/root/rootGeneralDocRoute.tsx +++ b/packages/website/src/routes/root/docs/root/rootGeneralDocRoute.tsx @@ -10,7 +10,7 @@ export const rootGeneralDocRoute = createRoute({ "Documentation complète d'Arrhes : guide d'utilisation, cours de comptabilité, référence API et informations générales.", }), component: lazyRouteComponent( - () => import("../../../../features/docs/general/rootGeneralDocPage.tsx"), + () => import("../../../../features/docs/general/RootGeneralDocPage.tsx"), "RootGeneralDocPage", ), }) diff --git a/packages/website/src/routes/root/docs/root/supportGeneralDocRoute.tsx b/packages/website/src/routes/root/docs/root/supportGeneralDocRoute.tsx index d8adbdfb..6538da74 100644 --- a/packages/website/src/routes/root/docs/root/supportGeneralDocRoute.tsx +++ b/packages/website/src/routes/root/docs/root/supportGeneralDocRoute.tsx @@ -10,7 +10,7 @@ export const supportGeneralDocRoute = createRoute({ "Besoin d'aide avec Arrhes ? Contactez notre support ou consultez la documentation pour résoudre vos problèmes.", }), component: lazyRouteComponent( - () => import("../../../../features/docs/general/supportGeneralDocPage.tsx"), + () => import("../../../../features/docs/general/SupportGeneralDocPage.tsx"), "SupportGeneralDocPage", ), }) diff --git a/packages/website/src/routes/root/docs/root/termsGeneralDocRoute.tsx b/packages/website/src/routes/root/docs/root/termsGeneralDocRoute.tsx index 19d910d8..bac7cea9 100644 --- a/packages/website/src/routes/root/docs/root/termsGeneralDocRoute.tsx +++ b/packages/website/src/routes/root/docs/root/termsGeneralDocRoute.tsx @@ -10,7 +10,7 @@ export const termsGeneralDocRoute = createRoute({ "Conditions Générales d'Utilisation d'Arrhes. Consultez les règles et conditions d'utilisation du logiciel.", }), component: lazyRouteComponent( - () => import("../../../../features/docs/general/termsGeneralDocPage.tsx"), + () => import("../../../../features/docs/general/TermsGeneralDocPage.tsx"), "TermsGeneralDocPage", ), }) diff --git a/packages/website/src/routes/root/docs/root/whitepaperGeneralDocRoute.tsx b/packages/website/src/routes/root/docs/root/whitepaperGeneralDocRoute.tsx index 80151250..119ff186 100644 --- a/packages/website/src/routes/root/docs/root/whitepaperGeneralDocRoute.tsx +++ b/packages/website/src/routes/root/docs/root/whitepaperGeneralDocRoute.tsx @@ -10,7 +10,7 @@ export const whitepaperGeneralDocRoute = createRoute({ "La philosophie d'Arrhes : transparence, open source et accessibilité pour la comptabilité des entreprises et associations.", }), component: lazyRouteComponent( - () => import("../../../../features/docs/general/whitepaperGeneralDocPage.tsx"), + () => import("../../../../features/docs/general/WhitepaperGeneralDocPage.tsx"), "WhitepaperGeneralDocPage", ), }) diff --git a/packages/website/src/routes/root/errorRoute.tsx b/packages/website/src/routes/root/errorRoute.tsx index 2ea162a7..79b7435a 100644 --- a/packages/website/src/routes/root/errorRoute.tsx +++ b/packages/website/src/routes/root/errorRoute.tsx @@ -8,5 +8,5 @@ export const errorRoute = createRoute({ title: "Error", robots: "noindex", }), - component: lazyRouteComponent(() => import("../../features/error/errorPage.js"), "ErrorPage"), + component: lazyRouteComponent(() => import("../../features/error/ErrorPage.js"), "ErrorPage"), }) diff --git a/packages/website/src/routes/root/home/homeRootRoute.tsx b/packages/website/src/routes/root/home/homeRootRoute.tsx index 2ec87e13..5db6add3 100644 --- a/packages/website/src/routes/root/home/homeRootRoute.tsx +++ b/packages/website/src/routes/root/home/homeRootRoute.tsx @@ -1,5 +1,5 @@ import { createRoute } from "@tanstack/react-router" -import { HomePage } from "../../../features/home/homePage.tsx" +import { HomePage } from "../../../features/home/HomePage.tsx" import { homeLayoutRoute } from "./homeLayoutRoute.tsx" export const homeRootRoute = createRoute({ diff --git a/packages/website/src/routes/root/publicInvoiceXmlRoute.tsx b/packages/website/src/routes/root/publicInvoiceXmlRoute.tsx index 4ecd0954..4a3557db 100644 --- a/packages/website/src/routes/root/publicInvoiceXmlRoute.tsx +++ b/packages/website/src/routes/root/publicInvoiceXmlRoute.tsx @@ -12,7 +12,7 @@ export const publicInvoiceXmlRoute = createRoute({ robots: "noindex, nofollow", }), component: lazyRouteComponent( - () => import("../../features/public/invoiceXml/publicInvoiceXmlPage.js"), + () => import("../../features/public/invoiceXml/PublicInvoiceXmlPage.js"), "PublicInvoiceXmlPage", ), }) diff --git a/packages/website/src/routes/root/signIn/resetPasswordRoute.tsx b/packages/website/src/routes/root/signIn/resetPasswordRoute.tsx index 24481177..d03963b8 100644 --- a/packages/website/src/routes/root/signIn/resetPasswordRoute.tsx +++ b/packages/website/src/routes/root/signIn/resetPasswordRoute.tsx @@ -18,5 +18,5 @@ export const resetPasswordRoute = createRoute({ description: "Recevez un nouveau mot de passe temporaire par email.", } }, - component: lazyRouteComponent(() => import("../../../features/signIn/resetPasswordPage.js"), "ResetPasswordPage"), + component: lazyRouteComponent(() => import("../../../features/signIn/ResetPasswordPage.js"), "ResetPasswordPage"), }) diff --git a/packages/website/src/routes/root/signIn/signInRoute.tsx b/packages/website/src/routes/root/signIn/signInRoute.tsx index ffd5548c..741035b6 100644 --- a/packages/website/src/routes/root/signIn/signInRoute.tsx +++ b/packages/website/src/routes/root/signIn/signInRoute.tsx @@ -18,5 +18,5 @@ export const signInRoute = createRoute({ description: "Connectez-vous à votre compte Arrhes pour accéder à votre espace comptable.", } }, - component: lazyRouteComponent(() => import("../../../features/signIn/signInPage.js"), "SignInPage"), + component: lazyRouteComponent(() => import("../../../features/signIn/SignInPage.js"), "SignInPage"), }) diff --git a/packages/website/src/routes/root/signUp/signUpRoute.tsx b/packages/website/src/routes/root/signUp/signUpRoute.tsx index c11aec9e..0451c969 100644 --- a/packages/website/src/routes/root/signUp/signUpRoute.tsx +++ b/packages/website/src/routes/root/signUp/signUpRoute.tsx @@ -19,5 +19,5 @@ export const signUpRoute = createRoute({ "Créez votre compte Arrhes gratuitement et commencez à gérer votre comptabilité en quelques minutes.", } }, - component: lazyRouteComponent(() => import("../../../features/signUp/signUpPage.js"), "SignUpPage"), + component: lazyRouteComponent(() => import("../../../features/signUp/SignUpPage.js"), "SignUpPage"), }) diff --git a/packages/website/src/routes/rootLayoutRoute.tsx b/packages/website/src/routes/rootLayoutRoute.tsx index fb35acd6..1bcd65c3 100644 --- a/packages/website/src/routes/rootLayoutRoute.tsx +++ b/packages/website/src/routes/rootLayoutRoute.tsx @@ -3,7 +3,7 @@ import { CircularLoader } from "@arrhes/ui" import { createRootRouteWithContext, useRouterState } from "@tanstack/react-router" import { Fragment } from "react/jsx-runtime" import type * as v from "valibot" -import { RootLayout } from "../features/rootLayout.js" +import { RootLayout } from "../features/RootLayout.js" const DEFAULT_DESCRIPTION = "Logiciel de comptabilité open source pour les entreprises et associations françaises. Gérez vos écritures, comptes et documents comptables simplement." diff --git a/packages/website/src/utilities/cookies/getCookie.ts b/packages/website/src/utilities/cookies/getCookie.ts index 0ae832b8..2d74d4d4 100644 --- a/packages/website/src/utilities/cookies/getCookie.ts +++ b/packages/website/src/utilities/cookies/getCookie.ts @@ -1,4 +1,5 @@ export function getCookie(name: string) { + if (typeof document === "undefined") return undefined const encodedName = encodeURIComponent(name) const stringCookies = document?.cookie?.split("; ") const cookie = stringCookies?.find((x) => x.startsWith(`${encodedName}=`)) diff --git a/packages/website/src/utilities/getResponseBodyFromAPI.ts b/packages/website/src/utilities/getResponseBodyFromAPI.ts index d93b40bc..fe58a2bf 100644 --- a/packages/website/src/utilities/getResponseBodyFromAPI.ts +++ b/packages/website/src/utilities/getResponseBodyFromAPI.ts @@ -7,6 +7,51 @@ import { resolveApiBaseUrl } from "./resolveApiBaseUrl.js" import { validate } from "./validate.js" import { cookiePrefix } from "./variables.js" +/** + * Interpolates URL path params (e.g. `:idOrganization`) with values from the + * `params` map. Any remaining body fields (not consumed as path params) are + * sent as query string for GET requests, or as the JSON body for POST/PATCH/DELETE. + */ +function buildUrl( + apiBaseUrl: string, + rawPath: string, + params: Record | undefined, + body: Record, + method: "GET" | "POST" | "PATCH" | "DELETE", +): { + url: URL + remainingBody: Record +} { + let path = rawPath + const consumed = new Set() + + if (params) { + for (const [key, value] of Object.entries(params)) { + const token = `:${key}` + if (path.includes(token)) { + path = path.replace(token, encodeURIComponent(value)) + consumed.add(key) + } + } + } + + const url = new URL(`${apiBaseUrl}${path}`) + const remaining = Object.fromEntries(Object.entries(body).filter(([k]) => !consumed.has(k))) + + if (method === "GET") { + for (const [key, value] of Object.entries(remaining)) { + if (value !== undefined && value !== null) { + url.searchParams.set(key, String(value)) + } + } + } + + return { + url, + remainingBody: remaining, + } +} + export async function getResponseBodyFromAPI< TSchemaBody extends v.ObjectSchema, TSchemaReturn extends @@ -15,6 +60,8 @@ export async function getResponseBodyFromAPI< >(parameters: { routeDefinition: ReturnType> body: v.InferOutput + /** URL path params to interpolate (e.g. `{ idOrganization: "abc" }` for `:idOrganization`) */ + params?: Record signal?: AbortSignal hasToastMessage?: boolean }) { @@ -34,6 +81,7 @@ export async function getResponseBodyFromAPI< } } + const method = parameters.routeDefinition.method const abortController = parameters.signal ? undefined : new AbortController() const signal = parameters.signal ?? abortController!.signal try { @@ -46,17 +94,25 @@ export async function getResponseBodyFromAPI< headers["X-Organization-Id"] = idOrganization } - const response = await fetch(new URL(`${apiBaseUrl}${parameters.routeDefinition.path}`), { - method: "POST", + const { url, remainingBody } = buildUrl( + apiBaseUrl, + parameters.routeDefinition.path, + parameters.params, + parameters.body as Record, + method, + ) + + const response = await fetch(url, { + method, headers, credentials: "include", - body: JSON.stringify(parameters.body), + body: method === "GET" ? undefined : JSON.stringify(remainingBody), signal, }) const jsonResponse = JSON.parse((await response.text()) || "{}") if (response.ok === false) { throw new ClientError({ - message: "Error with the POST request response", + message: `Error with the ${method} request response`, cause: jsonResponse.cause ?? jsonResponse.message, }) } diff --git a/packages/website/src/utilities/prefetchYearData.ts b/packages/website/src/utilities/prefetchYearData.ts index 35b1c83a..5828f950 100644 --- a/packages/website/src/utilities/prefetchYearData.ts +++ b/packages/website/src/utilities/prefetchYearData.ts @@ -21,6 +21,7 @@ type YearScopedBody = { } type YearScopedRouteDefinition = { + method: "GET" | "POST" | "PATCH" | "DELETE" path: string schemas: { body: v.ObjectSchema diff --git a/packages/website/src/utilities/useHTTPData.ts b/packages/website/src/utilities/useHTTPData.ts index 971bf691..d5018730 100644 --- a/packages/website/src/utilities/useHTTPData.ts +++ b/packages/website/src/utilities/useHTTPData.ts @@ -13,18 +13,22 @@ export function useDataFromAPI< >(parameters: { routeDefinition: ReturnType> body: v.InferOutput + /** URL path params to interpolate (e.g. `{ idOrganization: "abc" }` for `:idOrganization`) */ + params?: Record enabled?: boolean select?: (data: v.InferOutput) => TSelected }) { return useQuery({ queryKey: [ parameters.routeDefinition.path, + parameters.params, parameters.body, ], queryFn: async (context) => { const response = await getResponseBodyFromAPI({ routeDefinition: parameters.routeDefinition, body: parameters.body, + params: parameters.params, signal: context.signal, }) if (response.ok === false) { diff --git a/packages/website/tsconfig.json b/packages/website/tsconfig.json index c7d0a1fa..182b1ba8 100644 --- a/packages/website/tsconfig.json +++ b/packages/website/tsconfig.json @@ -26,9 +26,12 @@ "noFallthroughCasesInSwitch": true, /* Path aliases */ "ignoreDeprecations": "6.0", - "baseUrl": "." + "baseUrl": ".", + "incremental": true, + "tsBuildInfoFile": "./.tsbuildinfo" }, - "include": ["src"], + "include": ["src", "plugins"], + "exclude": ["styled-system", "build", "node_modules"], "references": [ { "path": "../metadata" diff --git a/packages/website/vite.config.ts b/packages/website/vite.config.ts index d15954ac..cfdcac80 100644 --- a/packages/website/vite.config.ts +++ b/packages/website/vite.config.ts @@ -1,943 +1,9 @@ -import { readFileSync, writeFileSync } from "node:fs" -import { resolve } from "node:path" import react from "@vitejs/plugin-react" -import { defineConfig, type Plugin } from "vite" - -// ─────────────────────────── Docs Search Index Plugin ───────────────────────────── - -interface DocPageManifestEntry { - path: string - file: string // relative from package root - section: string - navGroup: string - navLabel: string -} - -// Maps every static doc page route to its source file and nav metadata. -// Content strings are extracted automatically from the TSX source at build time. -const DOC_PAGE_MANIFEST: DocPageManifestEntry[] = [ - // ── Général / Introduction ──────────────────────────────────────────────── - { - path: "/documentation", - file: "src/features/docs/general/rootGeneralDocPage.tsx", - section: "Général", - navGroup: "Introduction", - navLabel: "Accueil", - }, - { - path: "/documentation/fonctionnalités", - file: "src/features/docs/general/features/featuresGeneralDocPage.tsx", - section: "Général", - navGroup: "Introduction", - navLabel: "Fonctionnalités", - }, - { - path: "/documentation/philosophie", - file: "src/features/docs/general/whitepaperGeneralDocPage.tsx", - section: "Général", - navGroup: "Introduction", - navLabel: "Philosophie", - }, - { - path: "/documentation/tarifs", - file: "src/features/docs/general/pricing/pricingGeneralDocPage.tsx", - section: "Général", - navGroup: "Introduction", - navLabel: "Tarifs", - }, - { - path: "/documentation/support", - file: "src/features/docs/general/supportGeneralDocPage.tsx", - section: "Général", - navGroup: "Introduction", - navLabel: "Support", - }, - // ── Général / Légal ─────────────────────────────────────────────────────── - { - path: "/documentation/mentions-légales", - file: "src/features/docs/general/legalGeneralDocPage.tsx", - section: "Général", - navGroup: "Légal", - navLabel: "Mentions légales", - }, - { - path: "/documentation/cgu", - file: "src/features/docs/general/termsGeneralDocPage.tsx", - section: "Général", - navGroup: "Légal", - navLabel: "Conditions Générales d'Utilisation", - }, - { - path: "/documentation/confidentialité", - file: "src/features/docs/general/privacyGeneralDocPage.tsx", - section: "Général", - navGroup: "Légal", - navLabel: "Politique de confidentialité", - }, - // ── Comptabilité / Introduction ─────────────────────────────────────────── - { - path: "/documentation/comptabilité", - file: "src/features/docs/accounting/introduction/rootAccountingDocPage.tsx", - section: "Comptabilité", - navGroup: "Introduction", - navLabel: "Accueil", - }, - { - path: "/documentation/comptabilité/introduction", - file: "src/features/docs/accounting/introduction/introductionAccountingDocPage.tsx", - section: "Comptabilité", - navGroup: "Introduction", - navLabel: "Introduction", - }, - { - path: "/documentation/comptabilité/introduction/partie-double", - file: "src/features/docs/accounting/introduction/doubleEntryAccountingDocPage.tsx", - section: "Comptabilité", - navGroup: "Introduction", - navLabel: "La partie double", - }, - { - path: "/documentation/comptabilité/introduction/écritures", - file: "src/features/docs/accounting/introduction/entriesAccountingDocPage.tsx", - section: "Comptabilité", - navGroup: "Introduction", - navLabel: "Les écritures", - }, - // ── Comptabilité / Comptes ──────────────────────────────────────────────── - { - path: "/documentation/comptabilité/introduction/comptes", - file: "src/features/docs/accounting/introduction/accountsAccountingDocPage.tsx", - section: "Comptabilité", - navGroup: "Comptes", - navLabel: "Introduction", - }, - { - path: "/documentation/comptabilité/introduction/classes", - file: "src/features/docs/accounting/introduction/classesAccountingDocPage.tsx", - section: "Comptabilité", - navGroup: "Comptes", - navLabel: "Classes de comptes", - }, - { - path: "/documentation/comptabilité/ressources/comptes", - file: "src/features/docs/accounting/resources/accounts/accountsResourcesAccountingDocPage.tsx", - section: "Comptabilité", - navGroup: "Comptes", - navLabel: "Liste des comptes", - }, - // ── Comptabilité / Documents ────────────────────────────────────────────── - { - path: "/documentation/comptabilité/documents", - file: "src/features/docs/accounting/reports/reportsAccountingDocPage.tsx", - section: "Comptabilité", - navGroup: "Documents", - navLabel: "Introduction", - }, - { - path: "/documentation/comptabilité/documents/journal", - file: "src/features/docs/accounting/reports/journalAccountingDocPage.tsx", - section: "Comptabilité", - navGroup: "Documents", - navLabel: "Journal", - }, - { - path: "/documentation/comptabilité/documents/grand-livre", - file: "src/features/docs/accounting/reports/ledgerAccountingDocPage.tsx", - section: "Comptabilité", - navGroup: "Documents", - navLabel: "Grand livre", - }, - { - path: "/documentation/comptabilité/documents/balance", - file: "src/features/docs/accounting/reports/balanceAccountingDocPage.tsx", - section: "Comptabilité", - navGroup: "Documents", - navLabel: "Balance", - }, - { - path: "/documentation/comptabilité/documents/bilan", - file: "src/features/docs/accounting/reports/balanceSheetAccountingDocPage.tsx", - section: "Comptabilité", - navGroup: "Documents", - navLabel: "Bilan", - }, - { - path: "/documentation/comptabilité/documents/compte-de-résultat", - file: "src/features/docs/accounting/reports/incomeStatementAccountingDocPage.tsx", - section: "Comptabilité", - navGroup: "Documents", - navLabel: "Compte de résultat", - }, - { - path: "/documentation/comptabilité/documents/annexe", - file: "src/features/docs/accounting/reports/notesAccountingDocPage.tsx", - section: "Comptabilité", - navGroup: "Documents", - navLabel: "Annexe", - }, - { - path: "/documentation/comptabilité/documents/fec", - file: "src/features/docs/accounting/reports/fecAccountingDocPage.tsx", - section: "Comptabilité", - navGroup: "Documents", - navLabel: "FEC", - }, - // ── Comptabilité / Scénarios ───────────────────────────────────────────── - { - path: "/documentation/comptabilité/ressources/scénarios", - file: "src/features/docs/accounting/resources/scenarios/scenariosResourcesAccountingDocPage.tsx", - section: "Comptabilité", - navGroup: "Scénarios", - navLabel: "Scénarios", - }, - // ── Comptabilité / Glossaire ────────────────────────────────────────────── - { - path: "/documentation/comptabilité/ressources/glossaire", - file: "src/features/docs/accounting/resources/glossary/glossaryResourcesAccountingDocPage.tsx", - section: "Comptabilité", - navGroup: "Glossaire", - navLabel: "Glossaire", - }, - // ── Dashboard / Guide d'utilisation ────────────────────────────────────── - { - path: "/documentation/dashboard", - file: "src/features/docs/dashboard/rootDashboardDocPage.tsx", - section: "Dashboard", - navGroup: "Guide d'utilisation", - navLabel: "Accueil", - }, - { - path: "/documentation/dashboard/démarrage", - file: "src/features/docs/dashboard/gettingStartedDashboardDocPage.tsx", - section: "Dashboard", - navGroup: "Guide d'utilisation", - navLabel: "Démarrage", - }, - { - path: "/documentation/dashboard/organisations", - file: "src/features/docs/dashboard/organizationsDashboardDocPage.tsx", - section: "Dashboard", - navGroup: "Guide d'utilisation", - navLabel: "Organisations", - }, - { - path: "/documentation/dashboard/exercices", - file: "src/features/docs/dashboard/yearsDashboardDocPage.tsx", - section: "Dashboard", - navGroup: "Guide d'utilisation", - navLabel: "Exercices", - }, - { - path: "/documentation/dashboard/écritures", - file: "src/features/docs/dashboard/entriesDashboardDocPage.tsx", - section: "Dashboard", - navGroup: "Guide d'utilisation", - navLabel: "Saisie des écritures", - }, - { - path: "/documentation/dashboard/stockage", - file: "src/features/docs/dashboard/filesDashboardDocPage.tsx", - section: "Dashboard", - navGroup: "Guide d'utilisation", - navLabel: "Stockage", - }, - { - path: "/documentation/dashboard/documents", - file: "src/features/docs/dashboard/reportsDashboardDocPage.tsx", - section: "Dashboard", - navGroup: "Guide d'utilisation", - navLabel: "Documents comptables", - }, - { - path: "/documentation/dashboard/facturation", - file: "src/features/docs/dashboard/BillingDashboardDocPage.tsx", - section: "Dashboard", - navGroup: "Guide d'utilisation", - navLabel: "Facturation", - }, - { - path: "/documentation/dashboard/màj", - file: "src/features/docs/dashboard/UpdatesDashboardDocPage.tsx", - section: "Dashboard", - navGroup: "Guide d'utilisation", - navLabel: "Mises à jour", - }, - // ── Dashboard / Assistant IA ────────────────────────────────────────────── - { - path: "/documentation/dashboard/assistant", - file: "src/features/docs/ai/rootAiDocPage.tsx", - section: "Dashboard", - navGroup: "Assistant IA", - navLabel: "Introduction", - }, - { - path: "/documentation/dashboard/assistant/modèles", - file: "src/features/docs/ai/modelsAiDocPage.tsx", - section: "Dashboard", - navGroup: "Assistant IA", - navLabel: "Modèles", - }, - { - path: "/documentation/dashboard/assistant/outils", - file: "src/features/docs/ai/toolsAiDocPage.tsx", - section: "Dashboard", - navGroup: "Assistant IA", - navLabel: "Outils", - }, - { - path: "/documentation/dashboard/assistant/ocr", - file: "src/features/docs/ai/ocrAiDocPage.tsx", - section: "Dashboard", - navGroup: "Assistant IA", - navLabel: "OCR", - }, - // ── API ─────────────────────────────────────────────────────────────────── - { - path: "/documentation/api", - file: "src/features/docs/api/rootApiDocPage.tsx", - section: "API", - navGroup: "API", - navLabel: "Présentation", - }, - { - path: "/documentation/api/introduction", - file: "src/features/docs/api/introductionApiDocPage.tsx", - section: "API", - navGroup: "API", - navLabel: "Introduction", - }, - { - path: "/documentation/api/authentification", - file: "src/features/docs/api/authenticationApiDocPage.tsx", - section: "API", - navGroup: "API", - navLabel: "Authentification", - }, - { - path: "/documentation/api/organisation", - file: "src/features/docs/api/organizationApiDocPage.tsx", - section: "API", - navGroup: "API", - navLabel: "Organisation", - }, - { - path: "/documentation/api/exercice", - file: "src/features/docs/api/yearApiDocPage.tsx", - section: "API", - navGroup: "API", - navLabel: "Exercice", - }, - { - path: "/documentation/api/stockage", - file: "src/features/docs/api/filesApiDocPage.tsx", - section: "API", - navGroup: "API", - navLabel: "Fichiers et documents", - }, -] - -/** - * Extracts searchable text from a TSX doc page source file. - * Pulls: title/description/text prop strings, DocList items, and JSX text nodes. - */ -function extractDocPageContent(source: string): string { - const parts: string[] = [] - - // Strip comments so we don't extract text from them - const stripped = source - .replace(/\{\/\*[\s\S]*?\*\/\}/g, "") // JSX block comments - .replace(/\/\/[^\n]*/g, "") // line comments - - // 1. Named text props (title, description, text, label, headers, variant-unrelated names) - const propRe = /\b(?:title|description)\s*=\s*"([^"]+)"/g - for (const m of stripped.matchAll(propRe)) parts.push(m[1]) - - // 2. String items inside items={["...", ...]} — DocList, DocTable rows, etc. - const itemsBlockRe = /\bitems\s*=\s*\{\s*\[([^\]]*?)\]\s*\}/gs - for (const m of stripped.matchAll(itemsBlockRe)) { - for (const s of m[1].matchAll(/"([^"]{4,})"/g)) parts.push(s[1]) - } - - // 3. DocTable rows={[["cell", "cell"], ...]} - const rowsBlockRe = /\brows\s*=\s*\{\s*\[([^\]]*?\][^\]]*?)\]\s*\}/gs - for (const m of stripped.matchAll(rowsBlockRe)) { - for (const s of m[1].matchAll(/"([^"]{4,})"/g)) parts.push(s[1]) - } - - // 4. JSX text nodes: text between > and < that is not whitespace-only - const textNodeRe = />([^<>{}\n]{8,})= 8 && !/^\s*$/.test(t)) parts.push(t) - } - - // Deduplicate consecutive identical strings and collapse whitespace - return [ - ...new Set(parts), - ] - .map((p) => p.replace(/\s+/g, " ").trim()) - .filter(Boolean) - .join(" ") -} - -const DOCS_SEARCH_VIRTUAL_MODULE_ID = "virtual:docs-search-index" -const RESOLVED_DOCS_SEARCH_VIRTUAL_MODULE_ID = `\0${DOCS_SEARCH_VIRTUAL_MODULE_ID}` - -interface GeneratedSearchEntry { - path: string - title: string - description: string - section: string - navGroup: string - navLabel: string - content: string -} - -/** - * Parses accountsData.ts source and returns one search entry per account. - * Content is compact: number + label + description + className + type + side. - */ -function extractAccountEntries(source: string): GeneratedSearchEntry[] { - const entries: GeneratedSearchEntry[] = [] - const chunks = source.split(/(?=\bdefineAccount\()/) - for (const chunk of chunks) { - if (!/^\s*defineAccount\s*\(\s*"/.test(chunk)) continue - const numMatch = chunk.match(/defineAccount\(\s*"([^"]+)"/) - if (!numMatch) continue - const number = numMatch[1] - const labelMatch = chunk.match(/defineAccount\(\s*"[^"]+",\s*"([^"]+)"/) - if (!labelMatch) continue - const label = labelMatch[1] - const description = chunk.match(/\bdescription\s*:\s*"([^"]+)"/)?.[1] ?? "" - const className = chunk.match(/\bclassName\s*:\s*"([^"]+)"/)?.[1] ?? "" - const type = chunk.match(/\btype\s*:\s*"([^"]+)"/)?.[1] ?? "" - const side = chunk.match(/\bside\s*:\s*"([^"]+)"/)?.[1] ?? "" - entries.push({ - path: `/documentation/comptabilité/ressources/comptes/${number}`, - title: `${number} — ${label}`, - description, - section: "Comptabilité", - navGroup: "Comptes", - navLabel: label, - content: [ - number, - label, - description, - className, - type, - side, - ] - .filter(Boolean) - .join(" "), - }) - } - return entries -} - -/** - * Parses glossaryData.ts source and returns one search entry per glossary term. - * Content: term + englishTranslation + definition + relatedTerms. - */ -function extractGlossaryEntries(source: string): GeneratedSearchEntry[] { - const toSlug = (term: string) => - term - .toLowerCase() - .normalize("NFD") - .replace(/[\u0300-\u036f]/g, "") - .replace(/[^a-z0-9]+/g, "-") - .replace(/(^-|-$)/g, "") - - const entries: GeneratedSearchEntry[] = [] - const chunks = source.split(/(?=\bdefineTerm\()/) - for (const chunk of chunks) { - if (!/^\s*defineTerm\s*\(\s*"/.test(chunk)) continue - const m = chunk.match(/defineTerm\(\s*"([^"]+)",\s*"([^"]+)",\s*"([^"]+)"/) - if (!m) continue - const [, term, englishTranslation, definition] = m - const relatedTerms: string[] = [] - const rtBlock = chunk.match(/relatedTerms\s*:\s*\[([^\]]*)\]/) - if (rtBlock) { - for (const rt of rtBlock[1].matchAll(/"([^"]+)"/g)) relatedTerms.push(rt[1]) - } - entries.push({ - path: `/documentation/comptabilité/ressources/glossaire/${toSlug(term)}`, - title: term, - description: definition, - section: "Comptabilité", - navGroup: "Glossaire", - navLabel: term, - content: [ - term, - englishTranslation, - definition, - ...relatedTerms, - ].join(" "), - }) - } - return entries -} - -function docsSearchIndexPlugin(): Plugin { - const pkgRoot = resolve(__dirname) - const accountsDataPath = resolve(pkgRoot, "src/features/docs/accounting/resources/accounts/accountsData.ts") - const glossaryDataPath = resolve(pkgRoot, "src/features/docs/accounting/resources/glossary/glossaryData.ts") - - function buildIndex(): string { - const pageEntries = DOC_PAGE_MANIFEST.map((entry) => { - const filePath = resolve(pkgRoot, entry.file) - let source = "" - try { - source = readFileSync(filePath, "utf-8") - } catch { - console.warn(`[docs-search] Could not read ${entry.file}`) - } - const content = extractDocPageContent(source) - return { - path: entry.path, - title: extractTitle(source, entry.navLabel), - description: extractDescription(source, ""), - section: entry.section, - navGroup: entry.navGroup, - navLabel: entry.navLabel, - content: [ - entry.navGroup, - entry.navLabel, - content, - ] - .filter(Boolean) - .join(" "), - } - }) - - const accountEntries = extractAccountEntries(readFileSync(accountsDataPath, "utf-8")) - const glossaryEntries = extractGlossaryEntries(readFileSync(glossaryDataPath, "utf-8")) - - const entries = [ - ...pageEntries, - ...accountEntries, - ...glossaryEntries, - ] - return `export const docsSearchIndex = ${JSON.stringify(entries, null, 4)};` - } - - return { - name: "docs-search-index", - resolveId(id) { - if (id === DOCS_SEARCH_VIRTUAL_MODULE_ID) return RESOLVED_DOCS_SEARCH_VIRTUAL_MODULE_ID - }, - load(id) { - if (id === RESOLVED_DOCS_SEARCH_VIRTUAL_MODULE_ID) return buildIndex() - }, - handleHotUpdate({ file, server }) { - const isDocPage = DOC_PAGE_MANIFEST.some((e) => file.endsWith(e.file.replace(/\//g, "/"))) - const isDataFile = file === accountsDataPath || file === glossaryDataPath - if (isDocPage || isDataFile) { - const mod = server.moduleGraph.getModuleById(RESOLVED_DOCS_SEARCH_VIRTUAL_MODULE_ID) - if (mod) server.moduleGraph.invalidateModule(mod) - server.ws.send({ - type: "full-reload", - }) - } - }, - } -} - -/** Extract the `title` prop from a DocHeader (falls back to navLabel). */ -function extractTitle(source: string, fallback: string): string { - const m = source.match(/DocHeader[^>]*\btitle\s*=\s*"([^"]+)"/) - if (m) return m[1] - // Custom pages with

or plain heading text - const h1 = source.match(/]*>\s*([^<\n]{3,})\s*<\/h1>/) - if (h1) return h1[1].trim() - return fallback -} - -/** Extract the `description` prop from a DocHeader, falling back to

below

. */ -function extractDescription(source: string, fallback: string): string { - const m = source.match(/DocHeader[^>]*\bdescription\s*=\s*"([^"]+)"/) - if (m) return m[1] - return fallback -} - -function fontPreloadPlugin(): Plugin { - return { - name: "font-preload", - transformIndexHtml: { - order: "post", - handler(_html, ctx) { - const fontAssets = (ctx.bundle ? Object.keys(ctx.bundle) : []).filter((name) => name.endsWith(".woff2")) - return fontAssets.map((font) => ({ - tag: "link", - attrs: { - rel: "preload", - as: "font", - type: "font/woff2", - href: `/${font}`, - crossorigin: "anonymous", - }, - injectTo: "head" as const, - })) - }, - }, - } -} - -function sitemapPlugin(): Plugin { - return { - name: "sitemap-generator", - closeBundle() { - const baseUrl = "https://arrhes.com" - const today = new Date().toISOString().split("T")[0] - - // Static public routes - const staticRoutes = [ - { - path: "/", - priority: "1.0", - changefreq: "weekly", - }, - { - path: "/connexion", - priority: "0.5", - changefreq: "monthly", - }, - { - path: "/inscription", - priority: "0.6", - changefreq: "monthly", - }, - { - path: "/mot-de-passe-oublié", - priority: "0.5", - changefreq: "monthly", - }, - - // General docs - { - path: "/documentation", - priority: "0.8", - changefreq: "weekly", - }, - { - path: "/documentation/fonctionnalités", - priority: "0.7", - changefreq: "monthly", - }, - { - path: "/documentation/philosophie", - priority: "0.5", - changefreq: "monthly", - }, - { - path: "/documentation/tarifs", - priority: "0.7", - changefreq: "monthly", - }, - { - path: "/documentation/support", - priority: "0.5", - changefreq: "monthly", - }, - { - path: "/documentation/mentions-légales", - priority: "0.3", - changefreq: "yearly", - }, - { - path: "/documentation/cgu", - priority: "0.3", - changefreq: "yearly", - }, - { - path: "/documentation/confidentialité", - priority: "0.3", - changefreq: "yearly", - }, - - // Accounting docs - { - path: "/documentation/comptabilité", - priority: "0.8", - changefreq: "weekly", - }, - { - path: "/documentation/comptabilité/introduction", - priority: "0.7", - changefreq: "monthly", - }, - { - path: "/documentation/comptabilité/partie-double", - priority: "0.7", - changefreq: "monthly", - }, - { - path: "/documentation/comptabilité/écritures", - priority: "0.7", - changefreq: "monthly", - }, - { - path: "/documentation/comptabilité/comptes/introduction", - priority: "0.7", - changefreq: "monthly", - }, - { - path: "/documentation/comptabilité/comptes/classes", - priority: "0.7", - changefreq: "monthly", - }, - { - path: "/documentation/comptabilité/comptes/liste", - priority: "0.8", - changefreq: "weekly", - }, - { - path: "/documentation/comptabilité/documents", - priority: "0.7", - changefreq: "monthly", - }, - { - path: "/documentation/comptabilité/documents/journal", - priority: "0.7", - changefreq: "monthly", - }, - { - path: "/documentation/comptabilité/documents/grand-livre", - priority: "0.7", - changefreq: "monthly", - }, - { - path: "/documentation/comptabilité/documents/balance", - priority: "0.7", - changefreq: "monthly", - }, - { - path: "/documentation/comptabilité/documents/bilan", - priority: "0.7", - changefreq: "monthly", - }, - { - path: "/documentation/comptabilité/documents/compte-de-résultat", - priority: "0.7", - changefreq: "monthly", - }, - { - path: "/documentation/comptabilité/documents/annexe", - priority: "0.7", - changefreq: "monthly", - }, - { - path: "/documentation/comptabilité/documents/fec", - priority: "0.7", - changefreq: "monthly", - }, - { - path: "/documentation/comptabilité/scénarios", - priority: "0.7", - changefreq: "monthly", - }, - { - path: "/documentation/comptabilité/glossaire", - priority: "0.7", - changefreq: "monthly", - }, - - // Dashboard docs - { - path: "/documentation/dashboard", - priority: "0.7", - changefreq: "monthly", - }, - { - path: "/documentation/dashboard/démarrage", - priority: "0.7", - changefreq: "monthly", - }, - { - path: "/documentation/dashboard/organisations", - priority: "0.6", - changefreq: "monthly", - }, - { - path: "/documentation/dashboard/exercices", - priority: "0.6", - changefreq: "monthly", - }, - { - path: "/documentation/dashboard/écritures", - priority: "0.6", - changefreq: "monthly", - }, - { - path: "/documentation/dashboard/stockage", - priority: "0.6", - changefreq: "monthly", - }, - { - path: "/documentation/dashboard/documents", - priority: "0.6", - changefreq: "monthly", - }, - { - path: "/documentation/dashboard/facturation", - priority: "0.6", - changefreq: "monthly", - }, - { - path: "/documentation/dashboard/màj", - priority: "0.6", - changefreq: "monthly", - }, - { - path: "/documentation/dashboard/assistant", - priority: "0.6", - changefreq: "monthly", - }, - { - path: "/documentation/dashboard/assistant/modèles", - priority: "0.6", - changefreq: "monthly", - }, - { - path: "/documentation/dashboard/assistant/outils", - priority: "0.6", - changefreq: "monthly", - }, - { - path: "/documentation/dashboard/assistant/ocr", - priority: "0.6", - changefreq: "monthly", - }, - - // API docs - { - path: "/documentation/api", - priority: "0.7", - changefreq: "monthly", - }, - { - path: "/documentation/api/introduction", - priority: "0.6", - changefreq: "monthly", - }, - { - path: "/documentation/api/authentification", - priority: "0.6", - changefreq: "monthly", - }, - { - path: "/documentation/api/organisation", - priority: "0.6", - changefreq: "monthly", - }, - { - path: "/documentation/api/exercice", - priority: "0.6", - changefreq: "monthly", - }, - { - path: "/documentation/api/stockage", - priority: "0.6", - changefreq: "monthly", - }, - ] - - // Extract dynamic account slugs from source - const accountsDataPath = resolve( - __dirname, - "src/features/docs/accounting/resources/accounts/accountsData.ts", - ) - const accountsSrc = readFileSync(accountsDataPath, "utf-8") - const accountSlugs = [ - ...accountsSrc.matchAll(/defineAccount\(\s*\n?\s*"([^"]+)"/g), - ].map((m) => m[1]) - - // Extract dynamic glossary slugs from source - const glossaryDataPath = resolve( - __dirname, - "src/features/docs/accounting/resources/glossary/glossaryData.ts", - ) - const glossarySrc = readFileSync(glossaryDataPath, "utf-8") - // The toSlug function: lowercase, NFD normalize, strip diacritics, replace non-alnum with -, trim - - const glossaryTerms = [ - ...glossarySrc.matchAll(/defineTerm\(\s*\n?\s*"([^"]+)"/g), - ].map((m) => m[1]) - const toSlug = (term: string) => - term - .toLowerCase() - .normalize("NFD") - .replace(/[\u0300-\u036f]/g, "") - .replace(/[^a-z0-9]+/g, "-") - .replace(/(^-|-$)/g, "") - const glossarySlugs = glossaryTerms.map(toSlug) - - // Extract scenario paths directly from scenariosData.ts - const scenariosDataPath = resolve( - __dirname, - "src/features/docs/accounting/resources/scenarios/scenariosData.ts", - ) - const scenariosSrc = readFileSync(scenariosDataPath, "utf-8") - const scenarioPaths = [ - ...scenariosSrc.matchAll(/path:\s*"(\/documentation\/comptabilité\/scénarios\/[^"]+)"/g), - ].map((m) => m[1]) - - // Build URL entries - const routeMap = new Map< - string, - { - changefreq: string - priority: string - } - >() - const addRoute = (path: string, changefreq: string, priority: string) => { - if (!routeMap.has(path)) { - routeMap.set(path, { - changefreq, - priority, - }) - } - } - - for (const route of staticRoutes) { - addRoute(route.path, route.changefreq, route.priority) - } - - for (const slug of accountSlugs) { - addRoute(`/documentation/comptabilité/comptes/liste/${slug}`, "monthly", "0.5") - } - - for (const slug of glossarySlugs) { - addRoute(`/documentation/comptabilité/glossaire/${slug}`, "monthly", "0.5") - } - - for (const path of scenarioPaths) { - addRoute(path, "monthly", "0.5") - } - - const urls = [ - ...routeMap.entries(), - ] - .sort(([pathA], [pathB]) => pathA.localeCompare(pathB, "fr")) - .map( - ([path, metadata]) => ` - ${encodeURI(`${baseUrl}${path}`)} - ${today} - ${metadata.changefreq} - ${metadata.priority} - `, - ) - - const sitemap = ` - -${urls.join("\n")} - -` - const outPath = resolve(__dirname, "build/sitemap.xml") - writeFileSync(outPath, sitemap, "utf-8") - - console.log(`[sitemap] Generated sitemap.xml with ${urls.length} URLs`) - }, - } -} +import { defineConfig } from "vite" +import { docsSearchIndexPlugin } from "./plugins/docsSearchIndexPlugin" +import { fontPreloadPlugin } from "./plugins/fontPreloadPlugin" +import { prerenderPlugin } from "./plugins/prerenderPlugin" +import { sitemapPlugin } from "./plugins/sitemapPlugin" export default defineConfig(() => { return { @@ -948,6 +14,7 @@ export default defineConfig(() => { fontPreloadPlugin(), sitemapPlugin(), docsSearchIndexPlugin(), + prerenderPlugin(), ], assetsInclude: [ "**/*.md", diff --git a/packages/worker/package.json b/packages/worker/package.json index ac657459..42deb979 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -3,7 +3,7 @@ "type": "module", "private": true, "scripts": { - "dev": "tsx watch --env-file=.env ./src/main.ts", + "dev": "tsx watch --conditions source --env-file=.env ./src/main.ts", "build": "tsc --build", "start": "node --env-file=.env ./build/main.js" }, diff --git a/packages/worker/src/jobs/runAgentSession/runAgentSession.ts b/packages/worker/src/jobs/runAgentSession/runAgentSession.ts index 9d12b24a..93d9069a 100644 --- a/packages/worker/src/jobs/runAgentSession/runAgentSession.ts +++ b/packages/worker/src/jobs/runAgentSession/runAgentSession.ts @@ -607,13 +607,7 @@ export async function runAgentSession(args: RunAgentSessionJobArgs): Promise Promise | unknow export async function processJobs() { try { - console.log(await ContextClients.queue.getJobCounts()) + console.log("Job counts:", await ContextClients.queue.getJobCounts()) ContextClients.queue.process(10, async (job) => { try { - console.log(await ContextClients.queue.getJobCounts()) - console.log(`process job : (${await job.getState()}) ${job.id}`) + console.log(`[processJobs] Processing job: (${await job.getState()}) ${job.id}`) const validatedJob = validate({ schema: jobSchema, data: job.data, diff --git a/packages/worker/tsconfig.json b/packages/worker/tsconfig.json index fb2b546c..558d7b69 100644 --- a/packages/worker/tsconfig.json +++ b/packages/worker/tsconfig.json @@ -22,7 +22,9 @@ "baseUrl": "./", "paths": { "#src/*": ["src/*"] - } + }, + "incremental": true, + "tsBuildInfoFile": "./.tsbuildinfo" }, "exclude": [ "node_modules", @@ -52,5 +54,10 @@ "./src/utilities/sql/**/*", "./src/main.ts", "./src/startWorker.ts" + ], + "references": [ + { + "path": "../metadata" + } ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f47f852c..b2ac5347 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -85,6 +85,8 @@ importers: specifier: 4.1.5 version: 4.1.5(@types/node@25.6.0)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) + packages/cli: {} + packages/metadata: dependencies: drizzle-orm: @@ -140,12 +142,6 @@ importers: '@pandacss/dev': specifier: 1.11.0 version: 1.11.0(typescript@6.0.3) - '@radix-ui/react-popover': - specifier: 1.1.15 - version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-switch': - specifier: 1.2.6 - version: 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@tabler/icons-react': specifier: 3.42.0 version: 3.42.0(react@19.2.6) @@ -189,21 +185,6 @@ importers: '@hookform/resolvers': specifier: 5.2.2 version: 5.2.2(react-hook-form@7.75.0(react@19.2.6)) - '@radix-ui/react-context-menu': - specifier: 2.2.16 - version: 2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-dialog': - specifier: 1.1.15 - version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-popover': - specifier: 1.1.15 - version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-slot': - specifier: 1.2.4 - version: 1.2.4(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-tooltip': - specifier: 1.2.8 - version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@react-pdf/renderer': specifier: 4.5.1 version: 4.5.1(react@19.2.6) @@ -1078,21 +1059,6 @@ packages: cpu: [x64] os: [win32] - '@floating-ui/core@1.7.5': - resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} - - '@floating-ui/dom@1.7.6': - resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} - - '@floating-ui/react-dom@2.1.8': - resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' - - '@floating-ui/utils@0.2.11': - resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} - '@google-cloud/aiplatform@6.7.0': resolution: {integrity: sha512-CxMkLxRPiJ6fUd9gLiM8tyENX58b9xuX+86RG8wXfYyEUqvZQZdvpSnHq50EQcsn9jzqPGeMkEFHdrO7+ugvCg==} engines: {node: '>=18'} @@ -1355,355 +1321,6 @@ packages: peerDependencies: typescript: '>=4.1' - '@radix-ui/primitive@1.1.3': - resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} - - '@radix-ui/react-arrow@1.1.7': - resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-collection@1.1.7': - resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-compose-refs@1.1.2': - resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-context-menu@2.2.16': - resolution: {integrity: sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-context@1.1.2': - resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-dialog@1.1.15': - resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-direction@1.1.1': - resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-dismissable-layer@1.1.11': - resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-focus-guards@1.1.3': - resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-focus-scope@1.1.7': - resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-id@1.1.1': - resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-menu@2.1.16': - resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-popover@1.1.15': - resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-popper@1.2.8': - resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-portal@1.1.9': - resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-presence@1.1.5': - resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-primitive@2.1.3': - resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-roving-focus@1.1.11': - resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-slot@1.2.3': - resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-slot@1.2.4': - resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-switch@1.2.6': - resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-tooltip@1.2.8': - resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-use-callback-ref@1.1.1': - resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-controllable-state@1.2.2': - resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-effect-event@0.0.2': - resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-escape-keydown@1.1.1': - resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-layout-effect@1.1.1': - resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-previous@1.1.1': - resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-rect@1.1.1': - resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-size@1.1.1': - resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-visually-hidden@1.2.3': - resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/rect@1.1.1': - resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} - '@react-pdf/fns@3.1.3': resolution: {integrity: sha512-0I7pApDr1/RLAKbizuLy/IHTEa93LSPy/bEwYniboC3Xqnp6Od8xFJKbKEzGw2wh/5zKFFwl00g4t9RwgIMc3w==} @@ -2324,10 +1941,6 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} - aria-hidden@1.2.6: - resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} - engines: {node: '>=10'} - assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -2576,9 +2189,6 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - detect-node-es@1.1.0: - resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} - devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -2914,10 +2524,6 @@ packages: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} - get-nonce@1.0.1: - resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} - engines: {node: '>=6'} - get-port@5.1.1: resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==} engines: {node: '>=8'} @@ -3796,36 +3402,6 @@ packages: '@types/react': '>=18' react: '>=18' - react-remove-scroll-bar@2.3.8: - resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - - react-remove-scroll@2.7.2: - resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - react-style-singleton@2.2.3: - resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - react@19.2.6: resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} engines: {node: '>=0.10.0'} @@ -4172,26 +3748,6 @@ packages: peerDependencies: browserslist: '>= 4.21.0' - use-callback-ref@1.3.3: - resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - use-sidecar@1.1.3: - resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - use-sync-external-store@1.6.0: resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: @@ -5205,23 +4761,6 @@ snapshots: '@esbuild/win32-x64@0.27.7': optional: true - '@floating-ui/core@1.7.5': - dependencies: - '@floating-ui/utils': 0.2.11 - - '@floating-ui/dom@1.7.6': - dependencies: - '@floating-ui/core': 1.7.5 - '@floating-ui/utils': 0.2.11 - - '@floating-ui/react-dom@2.1.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': - dependencies: - '@floating-ui/dom': 1.7.6 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - - '@floating-ui/utils@0.2.11': {} - '@google-cloud/aiplatform@6.7.0': dependencies: google-gax: 5.0.6 @@ -5633,347 +5172,6 @@ snapshots: '@qdrant/js-client-rest': 1.17.0(typescript@6.0.3) typescript: 6.0.3 - '@radix-ui/primitive@1.1.3': {} - - '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': - dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.6) - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.6)': - dependencies: - react: 19.2.6 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-context-menu@2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.6)': - dependencies: - react: 19.2.6 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) - aria-hidden: 1.2.6 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.6) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.2.6)': - dependencies: - react: 19.2.6 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.6) - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.2.6)': - dependencies: - react: 19.2.6 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.6) - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.6)': - dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) - react: 19.2.6 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.6) - aria-hidden: 1.2.6 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.6) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) - aria-hidden: 1.2.6 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.6) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': - dependencies: - '@floating-ui/react-dom': 2.1.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/rect': 1.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': - dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': - dependencies: - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.6) - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.6)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - react: 19.2.6 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-slot@1.2.4(@types/react@19.2.14)(react@19.2.6)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - react: 19.2.6 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.6) - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.6)': - dependencies: - react: 19.2.6 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.6)': - dependencies: - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) - react: 19.2.6 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.6)': - dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) - react: 19.2.6 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.6)': - dependencies: - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.6) - react: 19.2.6 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.6)': - dependencies: - react: 19.2.6 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@19.2.6)': - dependencies: - react: 19.2.6 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.14)(react@19.2.6)': - dependencies: - '@radix-ui/rect': 1.1.1 - react: 19.2.6 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@19.2.6)': - dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) - react: 19.2.6 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': - dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/rect@1.1.1': {} - '@react-pdf/fns@3.1.3': {} '@react-pdf/font@4.0.8': @@ -6744,10 +5942,6 @@ snapshots: ansi-styles@6.2.3: {} - aria-hidden@1.2.6: - dependencies: - tslib: 2.8.1 - assertion-error@2.0.1: {} astral-regex@2.0.0: {} @@ -6958,8 +6152,6 @@ snapshots: detect-libc@2.1.2: {} - detect-node-es@1.1.0: {} - devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -7331,8 +6523,6 @@ snapshots: hasown: 2.0.3 math-intrinsics: 1.1.0 - get-nonce@1.0.1: {} - get-port@5.1.1: {} get-proto@1.0.1: @@ -8304,33 +7494,6 @@ snapshots: transitivePeerDependencies: - supports-color - react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.6): - dependencies: - react: 19.2.6 - react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.6) - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.2.14 - - react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.2.6): - dependencies: - react: 19.2.6 - react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.6) - react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.6) - tslib: 2.8.1 - use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.6) - use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.6) - optionalDependencies: - '@types/react': 19.2.14 - - react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.6): - dependencies: - get-nonce: 1.0.1 - react: 19.2.6 - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.2.14 - react@19.2.6: {} readable-stream@3.6.2: @@ -8715,21 +7878,6 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 - use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.6): - dependencies: - react: 19.2.6 - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.2.14 - - use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.6): - dependencies: - detect-node-es: 1.1.0 - react: 19.2.6 - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.2.14 - use-sync-external-store@1.6.0(react@19.2.6): dependencies: react: 19.2.6 diff --git a/scripts/migrate-handlers.mjs b/scripts/migrate-handlers.mjs new file mode 100644 index 00000000..dd733778 --- /dev/null +++ b/scripts/migrate-handlers.mjs @@ -0,0 +1,115 @@ +/** + * Batch migration script: updates all API handler files to use registerRoute() + * instead of apiFactory.createApp().post(routeDef.path, ...). + * + * Run from the monorepo root: + * node scripts/migrate-handlers.mjs + */ + +import { readFileSync, writeFileSync } from "node:fs" +import { glob } from "node:fs/promises" +import { join } from "node:path" + +const ROOT = new URL("../packages/api/src/routes", import.meta.url).pathname + +let filesUpdated = 0 +let filesSkipped = 0 + +for await (const entry of glob("**/*.ts", { + cwd: ROOT, +})) { + // Skip aggregator route files (they just import + chain sub-routes) + if (entry.endsWith("Routes.ts") || entry.endsWith("Route.ts")) { + filesSkipped++ + continue + } + + const filePath = join(ROOT, entry) + const original = readFileSync(filePath, "utf8") + let updated = original + + // Match: apiFactory.createApp().post(someRouteDefinition.path, async (c) => { + // Replace with: registerRoute(someRouteDefinition, async (c) => { + // Also handles .get(), .patch(), .delete() (idempotent — already migrated) + const methodPattern = + /apiFactory\.createApp\(\)\.(post|get|patch|delete)\((\w+(?:RouteDefinition|route)?)\.path,\s*/g + + if (!methodPattern.test(updated)) { + filesSkipped++ + continue + } + + // Reset lastIndex after test() + updated = original.replace( + /apiFactory\.createApp\(\)\.(post|get|patch|delete)\((\w+(?:RouteDefinition)?)\.path,\s*/g, + "registerRoute($2, ", + ) + + if (updated === original) { + filesSkipped++ + continue + } + + // Replace apiFactory import with registerRoute import (add if missing) + if (!updated.includes("registerRoute")) { + // Should not happen since we just added it above — safety check + filesSkipped++ + continue + } + + // Update import: replace apiFactory.js import with registerRoute.js + // Keep apiFactory import only if still used elsewhere in the file + if (!updated.includes("apiFactory") || updated.match(/apiFactory/g)?.length === 1) { + // Only referenced in remaining import — swap it + updated = updated.replace( + /import \{ apiFactory \} from ["']([^"']+apiFactory\.js)["']/, + 'import { registerRoute } from "$1".replace("apiFactory", "registerRoute")', + ) + } + + // Simpler: just add registerRoute import after the apiFactory import if not present + // (The above regex-in-string replacement won't work — do it properly) + updated = original.replace( + /apiFactory\.createApp\(\)\.(post|get|patch|delete)\((\w+(?:RouteDefinition)?)\.path,\s*/g, + "registerRoute($2, ", + ) + + // Add registerRoute import; replace or augment apiFactory import line + const apiFactoryImportRegex = /import \{ apiFactory \} from "([^"]+apiFactory\.js)"/ + const apiFactoryImportRegexRelative = /import \{ apiFactory \} from '([^']+apiFactory\.js)'/ + const match = updated.match(apiFactoryImportRegex) || updated.match(apiFactoryImportRegexRelative) + + if (match) { + const quote = updated.includes(`import { apiFactory } from "`) ? `"` : `'` + const importPath = match[1] + const registerRoutePath = importPath.replace("apiFactory.js", "registerRoute.js") + + if (updated.includes("apiFactory")) { + // Still used elsewhere — add registerRoute as addtional import + updated = updated.replace( + new RegExp(`import \\{ apiFactory \\} from ${quote}${escapeRegex(importPath)}${quote}`), + `import { apiFactory } from ${quote}${importPath}${quote}\nimport { registerRoute } from ${quote}${registerRoutePath}${quote}`, + ) + } else { + // Not used anymore — replace import + updated = updated.replace( + new RegExp(`import \\{ apiFactory \\} from ${quote}${escapeRegex(importPath)}${quote}`), + `import { registerRoute } from ${quote}${registerRoutePath}${quote}`, + ) + } + } + + if (updated !== original) { + writeFileSync(filePath, updated, "utf8") + filesUpdated++ + console.log(`✓ ${entry}`) + } else { + filesSkipped++ + } +} + +console.log(`\nDone. Updated: ${filesUpdated}, Skipped: ${filesSkipped}`) + +function escapeRegex(str) { + return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") +} diff --git a/scripts/migrate-routes.mjs b/scripts/migrate-routes.mjs new file mode 100644 index 00000000..4918ceda --- /dev/null +++ b/scripts/migrate-routes.mjs @@ -0,0 +1,1146 @@ +/** + * Batch migration script: updates all routeDefinition() calls in + * packages/metadata/src/routes/dashboard/ to REST paths + correct HTTP methods. + * + * Run from the monorepo root: + * node scripts/migrate-routes.mjs + */ + +import { readFileSync, writeFileSync } from "node:fs" + +/** Map from OLD path template string to [NEW path, method] */ +const PATH_MAP = new Map([ + // ── Public auth ──────────────────────────────────────────────────────── + [ + "`${routePath.public}/sign-in`", + [ + "`${routePath.v1}/auth/sign-in`", + "POST", + ], + ], + [ + "`${routePath.public}/sign-up`", + [ + "`${routePath.v1}/auth/sign-up`", + "POST", + ], + ], + [ + "`${routePath.public}/sign-out`", + [ + "`${routePath.v1}/auth/sign-out`", + "POST", + ], + ], + [ + "`${routePath.public}/send-magic-link`", + [ + "`${routePath.v1}/auth/magic-link`", + "POST", + ], + ], + [ + "`${routePath.public}/reset-password`", + [ + "`${routePath.v1}/auth/reset-password`", + "POST", + ], + ], + [ + "`${routePath.public}/mollie-webhook`", + [ + "`${routePath.v1}/webhooks/mollie`", + "POST", + ], + ], + + // ── User (me) ─────────────────────────────────────────────────────────── + [ + "`${routePath.auth}/read-user-session`", + [ + "`${routePath.v1}/users/me`", + "GET", + ], + ], + [ + "`${routePath.auth}/update-user`", + [ + "`${routePath.v1}/users/me`", + "PATCH", + ], + ], + [ + "`${routePath.auth}/update-user-email`", + [ + "`${routePath.v1}/users/me/email`", + "PATCH", + ], + ], + [ + "`${routePath.auth}/update-user-password`", + [ + "`${routePath.v1}/users/me/password`", + "PATCH", + ], + ], + [ + "`${routePath.auth}/validate-user-email`", + [ + "`${routePath.v1}/users/me/email/validate`", + "POST", + ], + ], + [ + "`${routePath.auth}/resend-email-validation`", + [ + "`${routePath.v1}/users/me/email/resend-validation`", + "POST", + ], + ], + [ + "`${routePath.auth}/activate-user`", + [ + "`${routePath.v1}/users/me/activate`", + "POST", + ], + ], + [ + "`${routePath.auth}/delete-user`", + [ + "`${routePath.v1}/users/me`", + "DELETE", + ], + ], + + // ── Organizations ─────────────────────────────────────────────────────── + [ + "`${routePath.auth}/get-all-my-organization`", + [ + "`${routePath.v1}/organizations`", + "GET", + ], + ], + [ + "`${routePath.auth}/add-new-organization`", + [ + "`${routePath.v1}/organizations`", + "POST", + ], + ], + [ + "`${routePath.auth}/activate-organization-membership`", + [ + "`${routePath.v1}/organizations/activate-membership`", + "POST", + ], + ], + [ + "`${routePath.auth}/read-one-organization`", + [ + "`${routePath.v1}/organizations/:idOrganization`", + "GET", + ], + ], + [ + "`${routePath.auth}/update-one-organization`", + [ + "`${routePath.v1}/organizations/:idOrganization`", + "PATCH", + ], + ], + [ + "`${routePath.auth}/delete-one-organization`", + [ + "`${routePath.v1}/organizations/:idOrganization`", + "DELETE", + ], + ], + + // Org users + [ + "`${routePath.auth}/read-all-organization-users`", + [ + "`${routePath.v1}/organizations/:idOrganization/users`", + "GET", + ], + ], + [ + "`${routePath.auth}/create-one-organization-user`", + [ + "`${routePath.v1}/organizations/:idOrganization/users`", + "POST", + ], + ], + [ + "`${routePath.auth}/read-one-organization-user`", + [ + "`${routePath.v1}/organizations/:idOrganization/users/:idOrganizationUser`", + "GET", + ], + ], + [ + "`${routePath.auth}/update-one-organization-user`", + [ + "`${routePath.v1}/organizations/:idOrganization/users/:idOrganizationUser`", + "PATCH", + ], + ], + [ + "`${routePath.auth}/delete-one-organization-user`", + [ + "`${routePath.v1}/organizations/:idOrganization/users/:idOrganizationUser`", + "DELETE", + ], + ], + + // API keys + [ + "`${routePath.auth}/read-all-api-keys`", + [ + "`${routePath.v1}/organizations/:idOrganization/api-keys`", + "GET", + ], + ], + [ + "`${routePath.auth}/create-one-api-key`", + [ + "`${routePath.v1}/organizations/:idOrganization/api-keys`", + "POST", + ], + ], + [ + "`${routePath.auth}/delete-one-api-key`", + [ + "`${routePath.v1}/organizations/:idOrganization/api-keys/:idApiKey`", + "DELETE", + ], + ], + + // Payments & billing + [ + "`${routePath.auth}/read-all-organization-payments`", + [ + "`${routePath.v1}/organizations/:idOrganization/payments`", + "GET", + ], + ], + [ + "`${routePath.auth}/read-all-organization-billings`", + [ + "`${routePath.v1}/organizations/:idOrganization/billings`", + "GET", + ], + ], + [ + "`${routePath.auth}/read-organization-billing`", + [ + "`${routePath.v1}/organizations/:idOrganization/billing`", + "GET", + ], + ], + [ + "`${routePath.auth}/read-all-invoices`", + [ + "`${routePath.v1}/organizations/:idOrganization/invoices`", + "GET", + ], + ], + [ + "`${routePath.auth}/read-one-invoice`", + [ + "`${routePath.v1}/organizations/:idOrganization/invoices/:idInvoice`", + "GET", + ], + ], + [ + "`${routePath.auth}/generate-invoice-get-signed-url`", + [ + "`${routePath.v1}/organizations/:idOrganization/invoices/:idInvoice/download-url`", + "POST", + ], + ], + [ + "`${routePath.auth}/create-first-payment`", + [ + "`${routePath.v1}/organizations/:idOrganization/billing/first-payment`", + "POST", + ], + ], + [ + "`${routePath.auth}/create-payment-method-checkout`", + [ + "`${routePath.v1}/organizations/:idOrganization/billing/payment-method-checkout`", + "POST", + ], + ], + [ + "`${routePath.auth}/create-resource-subscription`", + [ + "`${routePath.v1}/organizations/:idOrganization/billing/resource-subscription`", + "POST", + ], + ], + [ + "`${routePath.auth}/create-wallet-top-up-checkout`", + [ + "`${routePath.v1}/organizations/:idOrganization/billing/wallet-top-up`", + "POST", + ], + ], + [ + "`${routePath.auth}/create-wallet-withdrawal`", + [ + "`${routePath.v1}/organizations/:idOrganization/billing/wallet-withdrawal`", + "POST", + ], + ], + [ + "`${routePath.auth}/update-tokens-subscription`", + [ + "`${routePath.v1}/organizations/:idOrganization/billing/tokens-subscription`", + "PATCH", + ], + ], + [ + "`${routePath.auth}/update-ocr-subscription`", + [ + "`${routePath.v1}/organizations/:idOrganization/billing/ocr-subscription`", + "PATCH", + ], + ], + [ + "`${routePath.auth}/update-storage-subscription`", + [ + "`${routePath.v1}/organizations/:idOrganization/billing/storage-subscription`", + "PATCH", + ], + ], + [ + "`${routePath.auth}/update-licence-subscription`", + [ + "`${routePath.v1}/organizations/:idOrganization/billing/licence-subscription`", + "PATCH", + ], + ], + [ + "`${routePath.auth}/cancel-subscription`", + [ + "`${routePath.v1}/organizations/:idOrganization/billing/cancel-subscription`", + "POST", + ], + ], + [ + "`${routePath.auth}/cancel-organization-billing`", + [ + "`${routePath.v1}/organizations/:idOrganization/billing`", + "DELETE", + ], + ], + + // ── Years ──────────────────────────────────────────────────────────────── + [ + "`${routePath.auth}/read-all-years`", + [ + "`${routePath.v1}/organizations/:idOrganization/years`", + "GET", + ], + ], + [ + "`${routePath.auth}/create-one-year`", + [ + "`${routePath.v1}/organizations/:idOrganization/years`", + "POST", + ], + ], + [ + "`${routePath.auth}/read-one-year`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear`", + "GET", + ], + ], + [ + "`${routePath.auth}/update-one-year`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear`", + "PATCH", + ], + ], + [ + "`${routePath.auth}/delete-one-year`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear`", + "DELETE", + ], + ], + [ + "`${routePath.auth}/open-year`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/open`", + "POST", + ], + ], + [ + "`${routePath.auth}/close-year`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/close`", + "POST", + ], + ], + [ + "`${routePath.auth}/settle-balance-sheet`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/settle-balance-sheet`", + "POST", + ], + ], + [ + "`${routePath.auth}/settle-income-statement`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/settle-income-statement`", + "POST", + ], + ], + + // ── Accounts ────────────────────────────────────────────────────────────── + [ + "`${routePath.auth}/read-all-accounts`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/accounts`", + "GET", + ], + ], + [ + "`${routePath.auth}/create-one-account`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/accounts`", + "POST", + ], + ], + [ + "`${routePath.auth}/generate-accounts`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/accounts/generate`", + "POST", + ], + ], + [ + "`${routePath.auth}/read-one-account`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/accounts/:idAccount`", + "GET", + ], + ], + [ + "`${routePath.auth}/update-one-account`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/accounts/:idAccount`", + "PATCH", + ], + ], + [ + "`${routePath.auth}/delete-one-account`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/accounts/:idAccount`", + "DELETE", + ], + ], + + // ── Journals ────────────────────────────────────────────────────────────── + [ + "`${routePath.auth}/read-all-journals`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/journals`", + "GET", + ], + ], + [ + "`${routePath.auth}/create-one-journal`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/journals`", + "POST", + ], + ], + [ + "`${routePath.auth}/generate-journals`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/journals/generate`", + "POST", + ], + ], + [ + "`${routePath.auth}/read-one-journal`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/journals/:idJournal`", + "GET", + ], + ], + [ + "`${routePath.auth}/update-one-journal`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/journals/:idJournal`", + "PATCH", + ], + ], + [ + "`${routePath.auth}/delete-one-journal`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/journals/:idJournal`", + "DELETE", + ], + ], + + // ── Balance sheets ──────────────────────────────────────────────────────── + [ + "`${routePath.auth}/read-all-balance-sheets`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/balance-sheets`", + "GET", + ], + ], + [ + "`${routePath.auth}/create-one-balance-sheet`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/balance-sheets`", + "POST", + ], + ], + [ + "`${routePath.auth}/generate-balance-sheets`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/balance-sheets/generate`", + "POST", + ], + ], + [ + "`${routePath.auth}/connect-accounts-to-balance-sheets`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/balance-sheets/connect-accounts`", + "POST", + ], + ], + [ + "`${routePath.auth}/read-one-balance-sheet`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/balance-sheets/:idBalanceSheet`", + "GET", + ], + ], + [ + "`${routePath.auth}/update-one-balance-sheet`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/balance-sheets/:idBalanceSheet`", + "PATCH", + ], + ], + [ + "`${routePath.auth}/delete-one-balance-sheet`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/balance-sheets/:idBalanceSheet`", + "DELETE", + ], + ], + + // ── Income statements ───────────────────────────────────────────────────── + [ + "`${routePath.auth}/read-all-income-statements`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/income-statements`", + "GET", + ], + ], + [ + "`${routePath.auth}/create-one-income-statement`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/income-statements`", + "POST", + ], + ], + [ + "`${routePath.auth}/generate-income-statements`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/income-statements/generate`", + "POST", + ], + ], + [ + "`${routePath.auth}/connect-accounts-to-income-statements`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/income-statements/connect-accounts`", + "POST", + ], + ], + [ + "`${routePath.auth}/read-one-income-statement`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/income-statements/:idIncomeStatement`", + "GET", + ], + ], + [ + "`${routePath.auth}/update-one-income-statement`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/income-statements/:idIncomeStatement`", + "PATCH", + ], + ], + [ + "`${routePath.auth}/delete-one-income-statement`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/income-statements/:idIncomeStatement`", + "DELETE", + ], + ], + + // ── Computations ────────────────────────────────────────────────────────── + [ + "`${routePath.auth}/read-all-computations`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/computations`", + "GET", + ], + ], + [ + "`${routePath.auth}/create-one-computation`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/computations`", + "POST", + ], + ], + [ + "`${routePath.auth}/generate-computations`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/computations/generate`", + "POST", + ], + ], + [ + "`${routePath.auth}/read-one-computation`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/computations/:idComputation`", + "GET", + ], + ], + [ + "`${routePath.auth}/update-one-computation`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/computations/:idComputation`", + "PATCH", + ], + ], + [ + "`${routePath.auth}/delete-one-computation`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/computations/:idComputation`", + "DELETE", + ], + ], + + // Computation income statements + [ + "`${routePath.auth}/read-all-computation-income-statements`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/computations/:idComputation/income-statements`", + "GET", + ], + ], + [ + "`${routePath.auth}/create-one-computation-income-statement`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/computations/:idComputation/income-statements`", + "POST", + ], + ], + [ + "`${routePath.auth}/read-one-computation-income-statement`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/computations/:idComputation/income-statements/:idComputationIncomeStatement`", + "GET", + ], + ], + [ + "`${routePath.auth}/update-one-computation-income-statement`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/computations/:idComputation/income-statements/:idComputationIncomeStatement`", + "PATCH", + ], + ], + [ + "`${routePath.auth}/delete-one-computation-income-statement`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/computations/:idComputation/income-statements/:idComputationIncomeStatement`", + "DELETE", + ], + ], + + // ── Tags ────────────────────────────────────────────────────────────────── + [ + "`${routePath.auth}/read-all-tags`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/tags`", + "GET", + ], + ], + [ + "`${routePath.auth}/create-one-tag`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/tags`", + "POST", + ], + ], + [ + "`${routePath.auth}/read-one-tag`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/tags/:idTag`", + "GET", + ], + ], + [ + "`${routePath.auth}/update-one-tag`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/tags/:idTag`", + "PATCH", + ], + ], + [ + "`${routePath.auth}/delete-one-tag`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/tags/:idTag`", + "DELETE", + ], + ], + + // ── Entries ─────────────────────────────────────────────────────────────── + [ + "`${routePath.auth}/read-all-entries`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/entries`", + "GET", + ], + ], + [ + "`${routePath.auth}/create-one-entry`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/entries`", + "POST", + ], + ], + [ + "`${routePath.auth}/create-one-entry-from-template`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/entries/from-template`", + "POST", + ], + ], + [ + "`${routePath.auth}/read-all-entry-tags`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/entry-tags`", + "GET", + ], + ], + [ + "`${routePath.auth}/read-one-entry`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/entries/:idEntry`", + "GET", + ], + ], + [ + "`${routePath.auth}/update-one-entry`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/entries/:idEntry`", + "PATCH", + ], + ], + [ + "`${routePath.auth}/delete-one-entry`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/entries/:idEntry`", + "DELETE", + ], + ], + [ + "`${routePath.auth}/reverse-one-entry`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/entries/:idEntry/reverse`", + "POST", + ], + ], + [ + "`${routePath.auth}/duplicate-one-entry`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/entries/:idEntry/duplicate`", + "POST", + ], + ], + [ + "`${routePath.auth}/compute-one-entry`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/entries/:idEntry/compute`", + "POST", + ], + ], + + // Entry lines + [ + "`${routePath.auth}/read-all-entry-lines`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/entries/:idEntry/lines`", + "GET", + ], + ], + [ + "`${routePath.auth}/create-one-entry-line`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/entries/:idEntry/lines`", + "POST", + ], + ], + [ + "`${routePath.auth}/update-many-entry-lines`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/entries/:idEntry/lines`", + "PATCH", + ], + ], + [ + "`${routePath.auth}/read-one-entry-line`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/entries/:idEntry/lines/:idEntryLine`", + "GET", + ], + ], + [ + "`${routePath.auth}/update-one-entry-line`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/entries/:idEntry/lines/:idEntryLine`", + "PATCH", + ], + ], + [ + "`${routePath.auth}/delete-one-entry-line`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/entries/:idEntry/lines/:idEntryLine`", + "DELETE", + ], + ], + + // Entry tags + [ + "`${routePath.auth}/add-one-entry-tag`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/entries/:idEntry/tags`", + "POST", + ], + ], + [ + "`${routePath.auth}/remove-one-entry-tag`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/entries/:idEntry/tags/:idTag`", + "DELETE", + ], + ], + + // ── Files ───────────────────────────────────────────────────────────────── + [ + "`${routePath.auth}/read-all-files`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/files`", + "GET", + ], + ], + [ + "`${routePath.auth}/create-one-file`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/files`", + "POST", + ], + ], + [ + "`${routePath.auth}/read-one-file`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/files/:idFile`", + "GET", + ], + ], + [ + "`${routePath.auth}/update-one-file`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/files/:idFile`", + "PATCH", + ], + ], + [ + "`${routePath.auth}/delete-one-file`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/files/:idFile`", + "DELETE", + ], + ], + [ + "`${routePath.auth}/ocr-file`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/files/:idFile/ocr`", + "POST", + ], + ], + [ + "`${routePath.auth}/generate-file-put-signed-url`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/files/:idFile/upload-url`", + "POST", + ], + ], + [ + "`${routePath.auth}/generate-file-get-signed-url`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/files/:idFile/download-url`", + "POST", + ], + ], + [ + "`${routePath.auth}/generate-file-delete-signed-url`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/files/:idFile/delete-url`", + "POST", + ], + ], + [ + "`${routePath.auth}/finalize-file-upload`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/files/:idFile/finalize`", + "POST", + ], + ], + + // ── Folders ─────────────────────────────────────────────────────────────── + [ + "`${routePath.auth}/read-all-folders`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/folders`", + "GET", + ], + ], + [ + "`${routePath.auth}/create-one-folder`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/folders`", + "POST", + ], + ], + [ + "`${routePath.auth}/read-one-folder`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/folders/:idFolder`", + "GET", + ], + ], + [ + "`${routePath.auth}/update-one-folder`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/folders/:idFolder`", + "PATCH", + ], + ], + [ + "`${routePath.auth}/delete-one-folder`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/folders/:idFolder`", + "DELETE", + ], + ], + + // ── Reports ─────────────────────────────────────────────────────────────── + [ + "`${routePath.auth}/generate-income-statement-xml`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/reports/income-statement`", + "POST", + ], + ], + [ + "`${routePath.auth}/generate-balance-sheet-xml`", + [ + "`${routePath.v1}/organizations/:idOrganization/years/:idYear/reports/balance-sheet`", + "POST", + ], + ], + + // ── Agent ───────────────────────────────────────────────────────────────── + [ + "`${routePath.auth}/read-all-agent-sessions`", + [ + "`${routePath.v1}/agent/sessions`", + "GET", + ], + ], + [ + "`${routePath.auth}/create-one-agent-session`", + [ + "`${routePath.v1}/agent/sessions`", + "POST", + ], + ], + [ + "`${routePath.auth}/read-one-agent-session`", + [ + "`${routePath.v1}/agent/sessions/:idAgentSession`", + "GET", + ], + ], + [ + "`${routePath.auth}/update-one-agent-session`", + [ + "`${routePath.v1}/agent/sessions/:idAgentSession`", + "PATCH", + ], + ], + [ + "`${routePath.auth}/delete-one-agent-session`", + [ + "`${routePath.v1}/agent/sessions/:idAgentSession`", + "DELETE", + ], + ], + [ + "`${routePath.auth}/read-all-agent-messages`", + [ + "`${routePath.v1}/agent/sessions/:idAgentSession/messages`", + "GET", + ], + ], + [ + "`${routePath.auth}/create-one-agent-message`", + [ + "`${routePath.v1}/agent/sessions/:idAgentSession/messages`", + "POST", + ], + ], + [ + "`${routePath.auth}/get-stream-for-agent-message`", + [ + "`${routePath.v1}/agent/sessions/:idAgentSession/messages/:idAgentMessage/stream`", + "GET", + ], + ], + [ + "`${routePath.auth}/get-chat-stream`", + [ + "`${routePath.v1}/agent/sessions/:idAgentSession/stream`", + "GET", + ], + ], + [ + "`${routePath.auth}/undo-agent-action`", + [ + "`${routePath.v1}/agent/sessions/:idAgentSession/undo`", + "POST", + ], + ], + [ + "`${routePath.auth}/create-one-agent-file`", + [ + "`${routePath.v1}/agent/sessions/:idAgentSession/files`", + "POST", + ], + ], + [ + "`${routePath.auth}/search-referenceable`", + [ + "`${routePath.v1}/agent/search`", + "GET", + ], + ], + + // ── Support / tickets ───────────────────────────────────────────────────── + [ + "`${routePath.auth}/read-all-tickets`", + [ + "`${routePath.v1}/support/tickets`", + "GET", + ], + ], + [ + "`${routePath.auth}/create-one-ticket`", + [ + "`${routePath.v1}/support/tickets`", + "POST", + ], + ], + [ + "`${routePath.auth}/read-one-ticket`", + [ + "`${routePath.v1}/support/tickets/:idTicket`", + "GET", + ], + ], + [ + "`${routePath.auth}/update-one-ticket`", + [ + "`${routePath.v1}/support/tickets/:idTicket`", + "PATCH", + ], + ], + [ + "`${routePath.auth}/update-one-ticket-status`", + [ + "`${routePath.v1}/support/tickets/:idTicket/status`", + "PATCH", + ], + ], + [ + "`${routePath.auth}/read-all-ticket-messages`", + [ + "`${routePath.v1}/support/tickets/:idTicket/messages`", + "GET", + ], + ], + [ + "`${routePath.auth}/create-one-ticket-message`", + [ + "`${routePath.v1}/support/tickets/:idTicket/messages`", + "POST", + ], + ], +]) + +import { glob } from "node:fs/promises" +import { join } from "node:path" + +const ROOT = new URL("../packages/metadata/src/routes/dashboard", import.meta.url).pathname + +let filesUpdated = 0 +let filesSkipped = 0 + +for await (const entry of glob("**/*.ts", { + cwd: ROOT, +})) { + const filePath = join(ROOT, entry) + const original = readFileSync(filePath, "utf8") + let updated = original + + for (const [oldPath, [newPath, method]] of PATH_MAP.entries()) { + if (!updated.includes(oldPath)) continue + + // Replace path value + updated = updated.replace(oldPath, newPath) + + // Inject method field after `protocol:` line if not already present + if (!updated.includes("method:")) { + updated = updated.replace(/(\s+protocol:\s*"(?:http|ws)",)/, `$1\n method: "${method}",`) + } + + break // each file has one routeDefinition + } + + if (updated !== original) { + writeFileSync(filePath, updated, "utf8") + filesUpdated++ + console.log(`✓ ${entry}`) + } else { + filesSkipped++ + } +} + +console.log(`\nDone. Updated: ${filesUpdated}, Skipped: ${filesSkipped}`)