From 86c3a3d1feaad152226bd3b9493f5f7eedf6d727 Mon Sep 17 00:00:00 2001 From: Gabrigas Date: Mon, 1 Jun 2026 13:34:24 -0300 Subject: [PATCH 1/6] fix: drizzle kit version --- package.json | 6 +- pnpm-lock.yaml | 631 ++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 570 insertions(+), 67 deletions(-) diff --git a/package.json b/package.json index 6ee118c..bfb9d0a 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,8 @@ "@nestjs/jwt": "^11.0.2", "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.1.24", - "@nestjs/swagger": "^11.4.4", "@nestjs/schedule": "^6.1.3", + "@nestjs/swagger": "^11.4.4", "@nestjs/terminus": "^11.1.1", "@nestjs/throttler": "^6.5.0", "@scalar/nestjs-api-reference": "^1.1.19", @@ -78,14 +78,14 @@ "@types/express": "^5.0.6", "@types/express-session": "^1.19.0", "@types/jest": "^30.0.0", - "@types/node": "^22.19.19", "@types/multer": "^2.1.0", + "@types/node": "^22.19.19", "@types/nodemailer": "^7.0.11", "@types/passport": "^1.0.17", "@types/passport-local": "^1.0.38", "@types/pg": "^8.20.0", "@types/supertest": "^6.0.3", - "drizzle-kit": "1.0.0-beta.22", + "drizzle-kit": "0.31.10", "eslint": "^9.39.4", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3065c37..87d7fd7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -179,17 +179,17 @@ importers: specifier: ^6.0.3 version: 6.0.3 drizzle-kit: - specifier: 1.0.0-beta.22 - version: 1.0.0-beta.22 + specifier: 0.31.10 + version: 0.31.10 eslint: specifier: ^9.39.4 - version: 9.39.4(jiti@2.7.0) + version: 9.39.4 eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@9.39.4(jiti@2.7.0)) + version: 10.1.8(eslint@9.39.4) eslint-plugin-prettier: specifier: ^5.5.6 - version: 5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0))(prettier@3.8.3) + version: 5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.4))(eslint@9.39.4)(prettier@3.8.3) globals: specifier: ^16.5.0 version: 16.5.0 @@ -228,7 +228,7 @@ importers: version: 6.0.3 typescript-eslint: specifier: ^8.60.0 - version: 8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + version: 8.60.0(eslint@9.39.4)(typescript@6.0.3) packages: @@ -564,8 +564,8 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} - '@drizzle-team/brocli@0.11.0': - resolution: {integrity: sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg==} + '@drizzle-team/brocli@0.10.2': + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -576,162 +576,458 @@ packages: '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.is' + '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.28.0': + resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.25.12': resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.28.0': + resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.25.12': resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.28.0': + resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.25.12': resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.28.0': + resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.25.12': resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.28.0': + resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.25.12': resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.28.0': + resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.25.12': resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.28.0': + resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.12': resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.28.0': + resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.25.12': resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.28.0': + resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.25.12': resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.28.0': + resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.25.12': resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.28.0': + resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.25.12': resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.28.0': + resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.25.12': resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.28.0': + resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.25.12': resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.28.0': + resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.25.12': resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.28.0': + resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.25.12': resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.28.0': + resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.25.12': resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.28.0': + resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.12': resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.28.0': + resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.12': resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.28.0': + resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.12': resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.28.0': + resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.12': resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.28.0': + resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.12': resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.28.0': + resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.25.12': resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.28.0': + resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.25.12': resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.28.0': + resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.25.12': resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.28.0': + resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.25.12': resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.28.0': + resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1066,10 +1362,6 @@ packages: '@js-sdsl/ordered-map@4.4.2': resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} - '@js-temporal/polyfill@0.5.1': - resolution: {integrity: sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ==} - engines: {node: '>=12'} - '@kwsites/file-exists@1.1.1': resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} @@ -2643,8 +2935,8 @@ packages: resolution: {integrity: sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==} engines: {node: '>=12'} - drizzle-kit@1.0.0-beta.22: - resolution: {integrity: sha512-9HTZuQRljQKTgCx4UhiGn8KYYfHGk4+B/bRR1714W67kz0qgJvdrG527i8rQD8uUyET9UTGR1u8syySJD4znGw==} + drizzle-kit@0.31.10: + resolution: {integrity: sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==} hasBin: true drizzle-orm@0.45.2: @@ -2819,11 +3111,21 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} hasBin: true + esbuild@0.28.0: + resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -3613,10 +3915,6 @@ packages: node-notifier: optional: true - jiti@2.7.0: - resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} - hasBin: true - joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -3632,9 +3930,6 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true - jsbi@4.3.2: - resolution: {integrity: sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==} - jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -5067,6 +5362,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.22.4: + resolution: {integrity: sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==} + engines: {node: '>=18.0.0'} + hasBin: true + tunnel@0.0.6: resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} @@ -5907,7 +6207,7 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 - '@drizzle-team/brocli@0.11.0': {} + '@drizzle-team/brocli@0.10.2': {} '@emnapi/core@1.10.0': dependencies: @@ -5925,87 +6225,241 @@ snapshots: tslib: 2.8.1 optional: true + '@esbuild-kit/core-utils@3.3.2': + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + + '@esbuild-kit/esm-loader@2.6.5': + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.14.0 + '@esbuild/aix-ppc64@0.25.12': optional: true + '@esbuild/aix-ppc64@0.28.0': + optional: true + + '@esbuild/android-arm64@0.18.20': + optional: true + '@esbuild/android-arm64@0.25.12': optional: true + '@esbuild/android-arm64@0.28.0': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + '@esbuild/android-arm@0.25.12': optional: true + '@esbuild/android-arm@0.28.0': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + '@esbuild/android-x64@0.25.12': optional: true + '@esbuild/android-x64@0.28.0': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + '@esbuild/darwin-arm64@0.25.12': optional: true + '@esbuild/darwin-arm64@0.28.0': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + '@esbuild/darwin-x64@0.25.12': optional: true + '@esbuild/darwin-x64@0.28.0': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + '@esbuild/freebsd-arm64@0.25.12': optional: true + '@esbuild/freebsd-arm64@0.28.0': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + '@esbuild/freebsd-x64@0.25.12': optional: true + '@esbuild/freebsd-x64@0.28.0': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + '@esbuild/linux-arm64@0.25.12': optional: true + '@esbuild/linux-arm64@0.28.0': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + '@esbuild/linux-arm@0.25.12': optional: true + '@esbuild/linux-arm@0.28.0': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + '@esbuild/linux-ia32@0.25.12': optional: true + '@esbuild/linux-ia32@0.28.0': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + '@esbuild/linux-loong64@0.25.12': optional: true + '@esbuild/linux-loong64@0.28.0': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + '@esbuild/linux-mips64el@0.25.12': optional: true + '@esbuild/linux-mips64el@0.28.0': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + '@esbuild/linux-ppc64@0.25.12': optional: true + '@esbuild/linux-ppc64@0.28.0': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + '@esbuild/linux-riscv64@0.25.12': optional: true + '@esbuild/linux-riscv64@0.28.0': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + '@esbuild/linux-s390x@0.25.12': optional: true + '@esbuild/linux-s390x@0.28.0': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + '@esbuild/linux-x64@0.25.12': optional: true + '@esbuild/linux-x64@0.28.0': + optional: true + '@esbuild/netbsd-arm64@0.25.12': optional: true + '@esbuild/netbsd-arm64@0.28.0': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + '@esbuild/netbsd-x64@0.25.12': optional: true + '@esbuild/netbsd-x64@0.28.0': + optional: true + '@esbuild/openbsd-arm64@0.25.12': optional: true + '@esbuild/openbsd-arm64@0.28.0': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + '@esbuild/openbsd-x64@0.25.12': optional: true + '@esbuild/openbsd-x64@0.28.0': + optional: true + '@esbuild/openharmony-arm64@0.25.12': optional: true + '@esbuild/openharmony-arm64@0.28.0': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + '@esbuild/sunos-x64@0.25.12': optional: true + '@esbuild/sunos-x64@0.28.0': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + '@esbuild/win32-arm64@0.25.12': optional: true + '@esbuild/win32-arm64@0.28.0': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + '@esbuild/win32-ia32@0.25.12': optional: true + '@esbuild/win32-ia32@0.28.0': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + '@esbuild/win32-x64@0.25.12': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.7.0))': + '@esbuild/win32-x64@0.28.0': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': dependencies: - eslint: 9.39.4(jiti@2.7.0) + eslint: 9.39.4 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -6452,10 +6906,6 @@ snapshots: '@js-sdsl/ordered-map@4.4.2': {} - '@js-temporal/polyfill@0.5.1': - dependencies: - jsbi: 4.3.2 - '@kwsites/file-exists@1.1.1': dependencies: debug: 4.4.3 @@ -7166,15 +7616,15 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.60.0(eslint@9.39.4)(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.60.0 - '@typescript-eslint/type-utils': 8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/utils': 8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/type-utils': 8.60.0(eslint@9.39.4)(typescript@6.0.3) + '@typescript-eslint/utils': 8.60.0(eslint@9.39.4)(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.60.0 - eslint: 9.39.4(jiti@2.7.0) + eslint: 9.39.4 ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -7182,14 +7632,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/parser@8.60.0(eslint@9.39.4)(typescript@6.0.3)': dependencies: '@typescript-eslint/scope-manager': 8.60.0 '@typescript-eslint/types': 8.60.0 '@typescript-eslint/typescript-estree': 8.60.0(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.60.0 debug: 4.4.3 - eslint: 9.39.4(jiti@2.7.0) + eslint: 9.39.4 typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -7212,13 +7662,13 @@ snapshots: dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.60.0(eslint@9.39.4)(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.60.0 '@typescript-eslint/typescript-estree': 8.60.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.60.0(eslint@9.39.4)(typescript@6.0.3) debug: 4.4.3 - eslint: 9.39.4(jiti@2.7.0) + eslint: 9.39.4 ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: @@ -7241,13 +7691,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/utils@8.60.0(eslint@9.39.4)(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) '@typescript-eslint/scope-manager': 8.60.0 '@typescript-eslint/types': 8.60.0 '@typescript-eslint/typescript-estree': 8.60.0(typescript@6.0.3) - eslint: 9.39.4(jiti@2.7.0) + eslint: 9.39.4 typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -8126,13 +8576,12 @@ snapshots: dotenv@17.4.1: {} - drizzle-kit@1.0.0-beta.22: + drizzle-kit@0.31.10: dependencies: - '@drizzle-team/brocli': 0.11.0 - '@js-temporal/polyfill': 0.5.1 + '@drizzle-team/brocli': 0.10.2 + '@esbuild-kit/esm-loader': 2.6.5 esbuild: 0.25.12 - get-tsconfig: 4.14.0 - jiti: 2.7.0 + tsx: 4.22.4 drizzle-orm@0.45.2(@types/pg@8.20.0)(pg@8.21.0): optionalDependencies: @@ -8210,6 +8659,31 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.4 + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + esbuild@0.25.12: optionalDependencies: '@esbuild/aix-ppc64': 0.25.12 @@ -8239,6 +8713,35 @@ snapshots: '@esbuild/win32-ia32': 0.25.12 '@esbuild/win32-x64': 0.25.12 + esbuild@0.28.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.0 + '@esbuild/android-arm': 0.28.0 + '@esbuild/android-arm64': 0.28.0 + '@esbuild/android-x64': 0.28.0 + '@esbuild/darwin-arm64': 0.28.0 + '@esbuild/darwin-x64': 0.28.0 + '@esbuild/freebsd-arm64': 0.28.0 + '@esbuild/freebsd-x64': 0.28.0 + '@esbuild/linux-arm': 0.28.0 + '@esbuild/linux-arm64': 0.28.0 + '@esbuild/linux-ia32': 0.28.0 + '@esbuild/linux-loong64': 0.28.0 + '@esbuild/linux-mips64el': 0.28.0 + '@esbuild/linux-ppc64': 0.28.0 + '@esbuild/linux-riscv64': 0.28.0 + '@esbuild/linux-s390x': 0.28.0 + '@esbuild/linux-x64': 0.28.0 + '@esbuild/netbsd-arm64': 0.28.0 + '@esbuild/netbsd-x64': 0.28.0 + '@esbuild/openbsd-arm64': 0.28.0 + '@esbuild/openbsd-x64': 0.28.0 + '@esbuild/openharmony-arm64': 0.28.0 + '@esbuild/sunos-x64': 0.28.0 + '@esbuild/win32-arm64': 0.28.0 + '@esbuild/win32-ia32': 0.28.0 + '@esbuild/win32-x64': 0.28.0 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -8251,19 +8754,19 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-config-prettier@10.1.8(eslint@9.39.4(jiti@2.7.0)): + eslint-config-prettier@10.1.8(eslint@9.39.4): dependencies: - eslint: 9.39.4(jiti@2.7.0) + eslint: 9.39.4 - eslint-plugin-prettier@5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0))(prettier@3.8.3): + eslint-plugin-prettier@5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.4))(eslint@9.39.4)(prettier@3.8.3): dependencies: - eslint: 9.39.4(jiti@2.7.0) + eslint: 9.39.4 prettier: 3.8.3 prettier-linter-helpers: 1.0.1 synckit: 0.11.13 optionalDependencies: '@types/eslint': 9.6.1 - eslint-config-prettier: 10.1.8(eslint@9.39.4(jiti@2.7.0)) + eslint-config-prettier: 10.1.8(eslint@9.39.4) eslint-scope@5.1.1: dependencies: @@ -8281,9 +8784,9 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@9.39.4(jiti@2.7.0): + eslint@9.39.4: dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.21.2 '@eslint/config-helpers': 0.4.2 @@ -8317,8 +8820,6 @@ snapshots: minimatch: 3.1.5 natural-compare: 1.4.0 optionator: 0.9.4 - optionalDependencies: - jiti: 2.7.0 transitivePeerDependencies: - supports-color @@ -9291,8 +9792,6 @@ snapshots: - supports-color - ts-node - jiti@2.7.0: {} - joycon@3.1.1: {} js-tokens@4.0.0: {} @@ -9306,8 +9805,6 @@ snapshots: dependencies: argparse: 2.0.1 - jsbi@4.3.2: {} - jsesc@3.1.0: {} json-bigint@1.0.0: @@ -10696,6 +11193,12 @@ snapshots: tslib@2.8.1: {} + tsx@4.22.4: + dependencies: + esbuild: 0.28.0 + optionalDependencies: + fsevents: 2.3.3 + tunnel@0.0.6: {} tweetnacl@0.14.5: {} @@ -10733,13 +11236,13 @@ snapshots: typedarray@0.0.6: {} - typescript-eslint@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3): + typescript-eslint@8.60.0(eslint@9.39.4)(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.60.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/parser': 8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.60.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4)(typescript@6.0.3) + '@typescript-eslint/parser': 8.60.0(eslint@9.39.4)(typescript@6.0.3) '@typescript-eslint/typescript-estree': 8.60.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) - eslint: 9.39.4(jiti@2.7.0) + '@typescript-eslint/utils': 8.60.0(eslint@9.39.4)(typescript@6.0.3) + eslint: 9.39.4 typescript: 6.0.3 transitivePeerDependencies: - supports-color From 73021502dc92f27e7167942e6183ce3168966df6 Mon Sep 17 00:00:00 2001 From: Gabrigas Date: Mon, 1 Jun 2026 19:46:13 -0300 Subject: [PATCH 2/6] feat(enrollment): add theme and sigaa validations --- drizzle/0010_concerned_the_liberteens.sql | 8 + drizzle/meta/0010_snapshot.json | 1363 +++++++++++++++++ drizzle/meta/_journal.json | 7 + frontend/package.json | 4 +- .../components/steps/step-sigaa.tsx | 72 +- .../components/steps/step-theme-selection.tsx | 385 +++++ .../enrollment/components/wizard-stepper.tsx | 4 +- .../enrollment/hooks/use-enrollment.ts | 22 + frontend/src/lib/api/enrollments.ts | 13 + frontend/src/lib/api/index.ts | 1 + frontend/src/routes/_app/enrollment/new.tsx | 14 +- src/database/schema/enrollments.ts | 7 + src/enrollment/domain/enrollment.ts | 2 + src/enrollment/dto/enrollment-response.dto.ts | 2 + .../dto/update-enrollment-themes.dto.ts | 12 + src/enrollment/enrollment.controller.ts | 17 + src/enrollment/enrollment.module.ts | 11 +- src/enrollment/enrollment.service.spec.ts | 146 ++ src/enrollment/enrollment.service.ts | 153 +- .../persistence/enrollment.repository.ts | 2 + 20 files changed, 2226 insertions(+), 19 deletions(-) create mode 100644 drizzle/0010_concerned_the_liberteens.sql create mode 100644 drizzle/meta/0010_snapshot.json create mode 100644 frontend/src/features/enrollment/components/steps/step-theme-selection.tsx create mode 100644 src/enrollment/dto/update-enrollment-themes.dto.ts diff --git a/drizzle/0010_concerned_the_liberteens.sql b/drizzle/0010_concerned_the_liberteens.sql new file mode 100644 index 0000000..5616987 --- /dev/null +++ b/drizzle/0010_concerned_the_liberteens.sql @@ -0,0 +1,8 @@ +ALTER TABLE "enrollments" ADD COLUMN "primary_theme_id" uuid;--> statement-breakpoint +ALTER TABLE "enrollments" ADD COLUMN "secondary_theme_id" uuid;--> statement-breakpoint +ALTER TABLE "courses" ADD COLUMN "search_vector" "tsvector" GENERATED ALWAYS AS (to_tsvector('simple', coalesce(name, ''))) STORED;--> statement-breakpoint +ALTER TABLE "universities" ADD COLUMN "search_vector" "tsvector" GENERATED ALWAYS AS (to_tsvector('simple', coalesce(name, '') || ' ' || coalesce(abbreviation, ''))) STORED;--> statement-breakpoint +ALTER TABLE "enrollments" ADD CONSTRAINT "enrollments_primary_theme_id_research_themes_id_fk" FOREIGN KEY ("primary_theme_id") REFERENCES "public"."research_themes"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "enrollments" ADD CONSTRAINT "enrollments_secondary_theme_id_research_themes_id_fk" FOREIGN KEY ("secondary_theme_id") REFERENCES "public"."research_themes"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "courses_search_vector_gin_idx" ON "courses" USING gin ("search_vector");--> statement-breakpoint +CREATE INDEX "universities_search_vector_gin_idx" ON "universities" USING gin ("search_vector"); \ No newline at end of file diff --git a/drizzle/meta/0010_snapshot.json b/drizzle/meta/0010_snapshot.json new file mode 100644 index 0000000..f958347 --- /dev/null +++ b/drizzle/meta/0010_snapshot.json @@ -0,0 +1,1363 @@ +{ + "id": "d363bd67-7880-413f-bfb6-979f8ba8720c", + "prevId": "d0651629-6e2c-4824-b5ef-5fd2704d78ea", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.candidates": { + "name": "candidates", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "university_of_origin": { + "name": "university_of_origin", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "ira": { + "name": "ira", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true + }, + "poscomp": { + "name": "poscomp", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "candidates_user_id_users_id_fk": { + "name": "candidates_user_id_users_id_fk", + "tableFrom": "candidates", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cv_items": { + "name": "cv_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "enrollment_id": { + "name": "enrollment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scoring_category_id": { + "name": "scoring_category_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "proof_file_id": { + "name": "proof_file_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "score": { + "name": "score", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "cv_items_enrollment_id_enrollments_id_fk": { + "name": "cv_items_enrollment_id_enrollments_id_fk", + "tableFrom": "cv_items", + "tableTo": "enrollments", + "columnsFrom": [ + "enrollment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cv_items_scoring_category_id_cv_scoring_categories_id_fk": { + "name": "cv_items_scoring_category_id_cv_scoring_categories_id_fk", + "tableFrom": "cv_items", + "tableTo": "cv_scoring_categories", + "columnsFrom": [ + "scoring_category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "cv_items_proof_file_id_files_id_fk": { + "name": "cv_items_proof_file_id_files_id_fk", + "tableFrom": "cv_items", + "tableTo": "files", + "columnsFrom": [ + "proof_file_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cv_scoring_categories": { + "name": "cv_scoring_categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "enrollment_period_id": { + "name": "enrollment_period_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "points_per_item": { + "name": "points_per_item", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true + }, + "max_points": { + "name": "max_points", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "enrollment_level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "cv_scoring_categories_enrollment_period_id_enrollment_periods_id_fk": { + "name": "cv_scoring_categories_enrollment_period_id_enrollment_periods_id_fk", + "tableFrom": "cv_scoring_categories", + "tableTo": "enrollment_periods", + "columnsFrom": [ + "enrollment_period_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.enrollment_periods": { + "name": "enrollment_periods", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "semester": { + "name": "semester", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "start_date": { + "name": "start_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "end_date": { + "name": "end_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "enrollment_period_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'scheduled'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.enrollments": { + "name": "enrollments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "candidate_id": { + "name": "candidate_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "enrollment_period_id": { + "name": "enrollment_period_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "enrollment_level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "enrollment_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "phone": { + "name": "phone", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "justification": { + "name": "justification", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sigaa_code": { + "name": "sigaa_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "sigaa_receipt_file_id": { + "name": "sigaa_receipt_file_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "declaration": { + "name": "declaration", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "primary_theme_id": { + "name": "primary_theme_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "secondary_theme_id": { + "name": "secondary_theme_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "poscomp": { + "name": "poscomp", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "masters_degrees": { + "name": "masters_degrees", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "score_draft": { + "name": "score_draft", + "type": "numeric(7, 2)", + "primaryKey": false, + "notNull": false + }, + "submitted_at": { + "name": "submitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "enrollments_candidate_period_unique": { + "name": "enrollments_candidate_period_unique", + "columns": [ + { + "expression": "candidate_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enrollment_period_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "enrollments_candidate_id_users_id_fk": { + "name": "enrollments_candidate_id_users_id_fk", + "tableFrom": "enrollments", + "tableTo": "users", + "columnsFrom": [ + "candidate_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "enrollments_enrollment_period_id_enrollment_periods_id_fk": { + "name": "enrollments_enrollment_period_id_enrollment_periods_id_fk", + "tableFrom": "enrollments", + "tableTo": "enrollment_periods", + "columnsFrom": [ + "enrollment_period_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "enrollments_primary_theme_id_research_themes_id_fk": { + "name": "enrollments_primary_theme_id_research_themes_id_fk", + "tableFrom": "enrollments", + "tableTo": "research_themes", + "columnsFrom": [ + "primary_theme_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "enrollments_secondary_theme_id_research_themes_id_fk": { + "name": "enrollments_secondary_theme_id_research_themes_id_fk", + "tableFrom": "enrollments", + "tableTo": "research_themes", + "columnsFrom": [ + "secondary_theme_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.files": { + "name": "files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "original_name": { + "name": "original_name", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "bucket": { + "name": "bucket", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar(1000)", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "purpose": { + "name": "purpose", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "files_uploaded_by_users_id_fk": { + "name": "files_uploaded_by_users_id_fk", + "tableFrom": "files", + "tableTo": "users", + "columnsFrom": [ + "uploaded_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.professor": { + "name": "professor", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "department": { + "name": "department", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "institution": { + "name": "institution", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "professor_user_id_users_id_fk": { + "name": "professor_user_id_users_id_fk", + "tableFrom": "professor", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.research_theme_professors": { + "name": "research_theme_professors", + "schema": "", + "columns": { + "research_theme_id": { + "name": "research_theme_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "professor_id": { + "name": "professor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "research_theme_professors_research_theme_id_idx": { + "name": "research_theme_professors_research_theme_id_idx", + "columns": [ + { + "expression": "research_theme_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "research_theme_professors_professor_id_idx": { + "name": "research_theme_professors_professor_id_idx", + "columns": [ + { + "expression": "professor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "research_theme_professors_research_theme_id_research_themes_id_fk": { + "name": "research_theme_professors_research_theme_id_research_themes_id_fk", + "tableFrom": "research_theme_professors", + "tableTo": "research_themes", + "columnsFrom": [ + "research_theme_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "research_theme_professors_professor_id_professor_user_id_fk": { + "name": "research_theme_professors_professor_id_professor_user_id_fk", + "tableFrom": "research_theme_professors", + "tableTo": "professor", + "columnsFrom": [ + "professor_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "research_theme_professors_research_theme_id_professor_id_pk": { + "name": "research_theme_professors_research_theme_id_professor_id_pk", + "columns": [ + "research_theme_id", + "professor_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.research_themes": { + "name": "research_themes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "professor_id": { + "name": "professor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vacancies": { + "name": "vacancies", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "research_theme_level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "references": { + "name": "references", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "research_themes_level_idx": { + "name": "research_themes_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "research_themes_professor_id_idx": { + "name": "research_themes_professor_id_idx", + "columns": [ + { + "expression": "professor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "research_themes_professor_id_professor_user_id_fk": { + "name": "research_themes_professor_id_professor_user_id_fk", + "tableFrom": "research_themes", + "tableTo": "professor", + "columnsFrom": [ + "professor_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "sid": { + "name": "sid", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "sess": { + "name": "sess", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "expire": { + "name": "expire", + "type": "timestamp (6)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "IDX_session_expire": { + "name": "IDX_session_expire", + "columns": [ + { + "expression": "expire", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.courses": { + "name": "courses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true + }, + "university_id": { + "name": "university_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "is_manual": { + "name": "is_manual", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "search_vector": { + "name": "search_vector", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('simple', coalesce(name, ''))", + "type": "stored" + } + } + }, + "indexes": { + "courses_search_vector_gin_idx": { + "name": "courses_search_vector_gin_idx", + "columns": [ + { + "expression": "search_vector", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "courses_university_id_universities_id_fk": { + "name": "courses_university_id_universities_id_fk", + "tableFrom": "courses", + "tableTo": "universities", + "columnsFrom": [ + "university_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.universities": { + "name": "universities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true + }, + "abbreviation": { + "name": "abbreviation", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "varchar(2)", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "is_manual": { + "name": "is_manual", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "search_vector": { + "name": "search_vector", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('simple', coalesce(name, '') || ' ' || coalesce(abbreviation, ''))", + "type": "stored" + } + } + }, + "indexes": { + "universities_search_vector_gin_idx": { + "name": "universities_search_vector_gin_idx", + "columns": [ + { + "expression": "search_vector", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "auth_provider": { + "name": "auth_provider", + "type": "auth_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'email'" + }, + "provider_subject": { + "name": "provider_subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "cpf": { + "name": "cpf", + "type": "varchar(11)", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'candidate'" + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'inactive'" + }, + "onboarding_completed": { + "name": "onboarding_completed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "must_change_password": { + "name": "must_change_password", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "bootstrap_password_expires_at": { + "name": "bootstrap_password_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "confirm_email_token_version": { + "name": "confirm_email_token_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "forgot_password_token_version": { + "name": "forgot_password_token_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "search_vector": { + "name": "search_vector", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector(\n 'simple',\n coalesce(firstName, '') || ' ' ||\n coalesce(lastName, '') || ' ' ||\n coalesce(email, '')\n )", + "type": "stored" + } + } + }, + "indexes": { + "users_auth_provider_subject_unique": { + "name": "users_auth_provider_subject_unique", + "columns": [ + { + "expression": "auth_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subject", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_search_vector_gin_idx": { + "name": "users_search_vector_gin_idx", + "columns": [ + { + "expression": "search_vector", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "users_cpf_unique": { + "name": "users_cpf_unique", + "nullsNotDistinct": false, + "columns": [ + "cpf" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.enrollment_level": { + "name": "enrollment_level", + "schema": "public", + "values": [ + "masters", + "doctoral" + ] + }, + "public.enrollment_period_status": { + "name": "enrollment_period_status", + "schema": "public", + "values": [ + "scheduled", + "open", + "closed" + ] + }, + "public.enrollment_status": { + "name": "enrollment_status", + "schema": "public", + "values": [ + "draft", + "submitted", + "closed", + "cancelled" + ] + }, + "public.research_theme_level": { + "name": "research_theme_level", + "schema": "public", + "values": [ + "masters", + "doctoral" + ] + }, + "public.auth_provider": { + "name": "auth_provider", + "schema": "public", + "values": [ + "email", + "google" + ] + }, + "public.role": { + "name": "role", + "schema": "public", + "values": [ + "professor", + "candidate", + "mdcc-secretary", + "post-graduate-coordinator", + "post-graduate-vice-coordinator" + ] + }, + "public.status": { + "name": "status", + "schema": "public", + "values": [ + "active", + "inactive", + "disabled" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 1075130..060ab8f 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -71,6 +71,13 @@ "when": 1780233375577, "tag": "0009_magical_karen_page", "breakpoints": true + }, + { + "idx": 10, + "version": "7", + "when": 1780351615091, + "tag": "0010_concerned_the_liberteens", + "breakpoints": true } ] } \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 3f175d4..0957201 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,8 +7,8 @@ "dev": "vite", "build": "vite build", "typecheck": "tsc -b", - "lint:check": "eslint .", - "lint:fix": "eslint . --fix", + "lint:check": "eslint --quiet . ", + "lint:fix": "eslint --quiet . --fix ", "format": "prettier --write \"src/**/*.{ts,tsx}\"", "format:check": "prettier --check \"src/**/*.{ts,tsx}\"", "preview": "vite preview" diff --git a/frontend/src/features/enrollment/components/steps/step-sigaa.tsx b/frontend/src/features/enrollment/components/steps/step-sigaa.tsx index 23d1be3..6da1d32 100644 --- a/frontend/src/features/enrollment/components/steps/step-sigaa.tsx +++ b/frontend/src/features/enrollment/components/steps/step-sigaa.tsx @@ -34,6 +34,8 @@ interface StepSigaaProps { onBack?: () => void; } +const SIGAA_URL = 'https://si3.ufc.br/sigaa/public/processo_seletivo/lista.jsf'; + // ── Component ──────────────────────────────────────────────────────── export function StepSigaa({ enrollment, onBack }: StepSigaaProps) { @@ -44,7 +46,9 @@ export function StepSigaa({ enrollment, onBack }: StepSigaaProps) { const [sigaaCode, setSigaaCode] = useState(enrollment?.sigaaCode ?? ''); const [receiptFile, setReceiptFile] = useState(null); + const [declaration, setDeclaration] = useState(enrollment?.declaration ?? false); const [errors, setErrors] = useState>({}); + const [submissionErrors, setSubmissionErrors] = useState([]); const [showSubmitDialog, setShowSubmitDialog] = useState(false); const uploadReceipt = useMutation({ @@ -70,6 +74,10 @@ export function StepSigaa({ enrollment, onBack }: StepSigaaProps) { newErrors.receipt = 'Comprovante de inscrição é obrigatório.'; } + if (!declaration) { + newErrors.declaration = 'Você deve aceitar a declaração de veracidade.'; + } + setErrors(newErrors); return Object.keys(newErrors).length === 0; } @@ -88,10 +96,12 @@ export function StepSigaa({ enrollment, onBack }: StepSigaaProps) { if (!enrollment) return; try { - // Save sigaaCode + setSubmissionErrors([]); + + // Save sigaaCode and declaration await updateEnrollment.mutateAsync({ id: enrollment.id, - payload: { sigaaCode: sigaaCode.trim() }, + payload: { sigaaCode: sigaaCode.trim(), declaration }, }); // Upload receipt file if selected @@ -110,7 +120,14 @@ export function StepSigaa({ enrollment, onBack }: StepSigaaProps) { navigate({ to: '/enrollment' }); }, onError: err => { - toast.error(err.message || 'Erro ao submeter inscrição.'); + toast.error('Não foi possível submeter a inscrição. Verifique as pendências.'); + if (err && typeof err === 'object' && 'message' in err) { + const rawMsg = (err as { message: string }).message; + if (typeof rawMsg === 'string') { + const list = rawMsg.split('. ').filter(Boolean); + setSubmissionErrors(list); + } + } setShowSubmitDialog(false); }, }); @@ -122,6 +139,8 @@ export function StepSigaa({ enrollment, onBack }: StepSigaaProps) { } const hasExistingReceipt = !!enrollment?.sigaaReceiptFileId; + const isFormComplete = + sigaaCode.trim() !== '' && (hasExistingReceipt || receiptFile !== null) && declaration; return (
@@ -134,6 +153,21 @@ export function StepSigaa({ enrollment, onBack }: StepSigaaProps) {

+ {/* ── Submission Errors ───────────────────────────────────── */} + {submissionErrors.length > 0 && ( +
+
+ + Existem pendências para a submissão de sua inscrição: +
+
    + {submissionErrors.map((errText, idx) => ( +
  • {errText}
  • + ))} +
+
+ )} + {/* ── Instructions ───────────────────────────────────────── */}
@@ -147,7 +181,7 @@ export function StepSigaa({ enrollment, onBack }: StepSigaaProps) {
  • Acesse o{' '} {errors.receipt}

    }
  • + {/* ── Declaration Checkbox ───────────────────────────────── */} +
    + { + setDeclaration(e.target.checked); + setErrors(prev => { + const { declaration: _, ...rest } = prev; + return rest; + }); + }} + className="mt-1 h-4.5 w-4.5 rounded border-slate-300 text-primary focus:ring-primary cursor-pointer" + /> + +
    + {errors.declaration &&

    {errors.declaration}

    } + {/* ── Navigation ─────────────────────────────────────────── */}
    {onBack ? ( @@ -215,8 +275,8 @@ export function StepSigaa({ enrollment, onBack }: StepSigaaProps) { +
    + +

    + {theme.description} +

    + + {isExpanded && ( +
    + )} + +
    + + +
    + + + ); + }) + ) : ( +

    + Nenhum tema encontrado com o termo de busca. +

    + )} +
    + + {/* ── Footer Navigation ──────────────────────────────────── */} +
    + {onBack && ( + + )} + +
    + + ); +} diff --git a/frontend/src/features/enrollment/components/wizard-stepper.tsx b/frontend/src/features/enrollment/components/wizard-stepper.tsx index 7d66172..e65c9d2 100644 --- a/frontend/src/features/enrollment/components/wizard-stepper.tsx +++ b/frontend/src/features/enrollment/components/wizard-stepper.tsx @@ -2,9 +2,9 @@ import { Check, FileCheck } from 'lucide-react'; import { cn } from '@/lib/utils'; -const STEP_LABELS = ['Nível', 'Acadêmico', 'POSCOMP', 'Currículo', 'SIGAA'] as const; +const STEP_LABELS = ['Nível', 'Acadêmico', 'POSCOMP', 'Currículo', 'Temas', 'SIGAA'] as const; -const STEP_ICONS = [null, null, null, null, FileCheck] as const; +const STEP_ICONS = [null, null, null, null, null, FileCheck] as const; interface WizardStepperProps { currentStep: number; diff --git a/frontend/src/features/enrollment/hooks/use-enrollment.ts b/frontend/src/features/enrollment/hooks/use-enrollment.ts index 32e7ed7..9ffb56c 100644 --- a/frontend/src/features/enrollment/hooks/use-enrollment.ts +++ b/frontend/src/features/enrollment/hooks/use-enrollment.ts @@ -1,6 +1,7 @@ import type { CreateEnrollmentPayload, UpdateEnrollmentPayload, + UpdateEnrollmentThemesPayload, UpdateMastersDegreesPayload, } from '@/lib/api'; import { api } from '@/lib/api'; @@ -100,3 +101,24 @@ export function useCancelEnrollment() { }, }); } + +export function useUpdateEnrollmentThemes() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ id, payload }: { id: string; payload: UpdateEnrollmentThemesPayload }) => + api.enrollments.updateThemes(id, payload), + onSuccess: (_data, variables) => { + queryClient.invalidateQueries({ queryKey: ['enrollments', variables.id] }); + queryClient.invalidateQueries({ queryKey: ['enrollments', 'me'] }); + }, + }); +} + +export function useResearchThemes(level?: 'masters' | 'doctoral', search?: string) { + return useQuery({ + queryKey: ['research-themes', { level, search }], + queryFn: () => api.researchThemes.findAll({ level, search, limit: 100 }), + enabled: !!level, + }); +} diff --git a/frontend/src/lib/api/enrollments.ts b/frontend/src/lib/api/enrollments.ts index 3ca9526..8cff437 100644 --- a/frontend/src/lib/api/enrollments.ts +++ b/frontend/src/lib/api/enrollments.ts @@ -30,6 +30,8 @@ export interface Enrollment { sigaaCode: string | null; sigaaReceiptFileId: string | null; declaration: boolean | null; + primaryThemeId: string | null; + secondaryThemeId: string | null; poscomp: PoscompData | null; mastersDegrees: MastersDegreeData[] | null; scoreDraft: string | null; @@ -55,6 +57,11 @@ export interface UpdateMastersDegreesPayload { mastersDegrees: MastersDegreeData[]; } +export interface UpdateEnrollmentThemesPayload { + primaryThemeId: string; + secondaryThemeId: string; +} + // ── Normalizers ────────────────────────────────────────────────────── function normalizeEnrollment(data: unknown): Enrollment { @@ -70,6 +77,8 @@ function normalizeEnrollment(data: unknown): Enrollment { sigaaCode: asNullableString(r.sigaaCode), sigaaReceiptFileId: asNullableString(r.sigaaReceiptFileId), declaration: r.declaration != null ? Boolean(r.declaration) : null, + primaryThemeId: asNullableString(r.primaryThemeId), + secondaryThemeId: asNullableString(r.secondaryThemeId), poscomp: (r.poscomp as PoscompData | null) ?? null, mastersDegrees: (r.mastersDegrees as MastersDegreeData[] | null) ?? null, scoreDraft: asNullableString(r.scoreDraft), @@ -112,6 +121,10 @@ export const enrollmentsApi = { ); }, + updateThemes: async (id: string, payload: UpdateEnrollmentThemesPayload): Promise => { + return normalizeEnrollment((await apiClient.put(`/enrollments/${id}/themes`, payload)).data); + }, + getMastersDegrees: async (id: string): Promise => { const { data } = await apiClient.get(`/enrollments/${id}/masters-degrees`); return (data as MastersDegreeData[] | null) ?? null; diff --git a/frontend/src/lib/api/index.ts b/frontend/src/lib/api/index.ts index 9d986a3..16133ad 100644 --- a/frontend/src/lib/api/index.ts +++ b/frontend/src/lib/api/index.ts @@ -26,6 +26,7 @@ export type { MastersDegreeData, PoscompData, UpdateEnrollmentPayload, + UpdateEnrollmentThemesPayload, UpdateMastersDegreesPayload, } from './enrollments'; export type { InviteProfessorPayload, PaginatedProfessors, ProfessorItem } from './professors'; diff --git a/frontend/src/routes/_app/enrollment/new.tsx b/frontend/src/routes/_app/enrollment/new.tsx index 4724265..8a80ddc 100644 --- a/frontend/src/routes/_app/enrollment/new.tsx +++ b/frontend/src/routes/_app/enrollment/new.tsx @@ -16,13 +16,14 @@ import { StepCvScoring } from '@/features/enrollment/components/steps/step-cv-sc import { StepLevelSelection } from '@/features/enrollment/components/steps/step-level-selection'; import { StepPoscomp } from '@/features/enrollment/components/steps/step-poscomp'; import { StepSigaa } from '@/features/enrollment/components/steps/step-sigaa'; +import { StepThemeSelection } from '@/features/enrollment/components/steps/step-theme-selection'; import { WizardStepper } from '@/features/enrollment/components/wizard-stepper'; import { useActivePeriod, useMyEnrollments } from '@/features/enrollment/hooks/use-enrollment'; import type { Enrollment } from '@/lib/api'; // ── Search params ──────────────────────────────────────────────────── -const TOTAL_STEPS = 5; +const TOTAL_STEPS = 6; function validateSearch(search: Record): { step?: number } { const step = Number(search.step); @@ -59,11 +60,16 @@ function detectCompletedSteps(enrollment: Enrollment | null): number[] { // Step 3 — CV scoring (checked via score or items — always accessible) // We can't determine CV items from the enrollment itself, so mark as accessible but not complete - // Step 4 — SIGAA - if (enrollment.sigaaCode) { + // Step 4 — Themes + if (enrollment.primaryThemeId && enrollment.secondaryThemeId) { completed.push(4); } + // Step 5 — SIGAA + if (enrollment.sigaaCode) { + completed.push(5); + } + return completed; } @@ -182,6 +188,8 @@ function EnrollmentWizardPage() { case 3: return ; case 4: + return ; + case 5: return ; default: return null; diff --git a/src/database/schema/enrollments.ts b/src/database/schema/enrollments.ts index 0b3d185..e815237 100644 --- a/src/database/schema/enrollments.ts +++ b/src/database/schema/enrollments.ts @@ -11,6 +11,7 @@ import { varchar, } from 'drizzle-orm/pg-core'; import { enrollmentLevelEnum, enrollmentPeriods } from './enrollment-periods'; +import { researchThemes } from './research-themes'; import { users } from './users'; export const enrollmentStatusEnum = pgEnum('enrollment_status', [ @@ -53,6 +54,12 @@ export const enrollments = pgTable( sigaaCode: varchar('sigaa_code', { length: 50 }), sigaaReceiptFileId: varchar('sigaa_receipt_file_id', { length: 255 }), declaration: boolean('declaration').default(false), + primaryThemeId: uuid('primary_theme_id').references(() => researchThemes.id, { + onDelete: 'restrict', + }), + secondaryThemeId: uuid('secondary_theme_id').references(() => researchThemes.id, { + onDelete: 'restrict', + }), poscomp: jsonb('poscomp').$type(), mastersDegrees: jsonb('masters_degrees').$type(), scoreDraft: numeric('score_draft', { precision: 7, scale: 2 }), diff --git a/src/enrollment/domain/enrollment.ts b/src/enrollment/domain/enrollment.ts index 3164c13..89ec8ef 100644 --- a/src/enrollment/domain/enrollment.ts +++ b/src/enrollment/domain/enrollment.ts @@ -11,6 +11,8 @@ export class Enrollment { sigaaCode!: string | null; sigaaReceiptFileId!: string | null; declaration!: boolean | null; + primaryThemeId!: string | null; + secondaryThemeId!: string | null; poscomp!: PoscompData | null; mastersDegrees!: MastersDegreeData[] | null; scoreDraft!: string | null; diff --git a/src/enrollment/dto/enrollment-response.dto.ts b/src/enrollment/dto/enrollment-response.dto.ts index 95cd5d9..2b689d4 100644 --- a/src/enrollment/dto/enrollment-response.dto.ts +++ b/src/enrollment/dto/enrollment-response.dto.ts @@ -12,6 +12,8 @@ export class EnrollmentResponseDto { @ApiPropertyOptional() sigaaCode!: string | null; @ApiPropertyOptional() sigaaReceiptFileId!: string | null; @ApiPropertyOptional() declaration!: boolean | null; + @ApiPropertyOptional() primaryThemeId!: string | null; + @ApiPropertyOptional() secondaryThemeId!: string | null; @ApiPropertyOptional() poscomp!: PoscompData | null; @ApiPropertyOptional() mastersDegrees!: MastersDegreeData[] | null; @ApiPropertyOptional() scoreDraft!: string | null; diff --git a/src/enrollment/dto/update-enrollment-themes.dto.ts b/src/enrollment/dto/update-enrollment-themes.dto.ts new file mode 100644 index 0000000..911c247 --- /dev/null +++ b/src/enrollment/dto/update-enrollment-themes.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsUUID } from 'class-validator'; + +export class UpdateEnrollmentThemesDto { + @ApiProperty() + @IsUUID() + primaryThemeId!: string; + + @ApiProperty() + @IsUUID() + secondaryThemeId!: string; +} diff --git a/src/enrollment/enrollment.controller.ts b/src/enrollment/enrollment.controller.ts index 9fb3cfb..0664b95 100644 --- a/src/enrollment/enrollment.controller.ts +++ b/src/enrollment/enrollment.controller.ts @@ -39,6 +39,7 @@ import { EnrollmentResponseDto } from './dto/enrollment-response.dto'; import { FindEnrollmentsDto } from './dto/find-enrollments.dto'; import { UpdateMastersDegreesDto } from './dto/masters-degree.dto'; import { UpdateEnrollmentStatusDto } from './dto/update-enrollment-status.dto'; +import { UpdateEnrollmentThemesDto } from './dto/update-enrollment-themes.dto'; import { UpdateEnrollmentDto } from './dto/update-enrollment.dto'; import { EnrollmentService } from './enrollment.service'; @@ -157,6 +158,22 @@ export class EnrollmentController { return this.enrollmentService.updateMastersDegrees(user.id, id, dto); } + @Put(':id/themes') + @Roles(RoleEnum.candidate) + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Update enrollment primary and secondary themes' }) + @ApiOkResponse({ type: EnrollmentResponseDto }) + @ApiUnauthorizedResponse({ description: 'No active session' }) + @ApiForbiddenResponse({ description: 'Insufficient permissions' }) + @ApiNotFoundResponse({ description: 'Enrollment not found' }) + updateThemes( + @CurrentUser() user: User, + @Param('id', new ParseUUIDPipe()) id: string, + @Body() dto: UpdateEnrollmentThemesDto, + ): Promise { + return this.enrollmentService.updateThemes(user.id, id, dto); + } + @Get(':id/masters-degrees') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: "Get master's degree info for an enrollment" }) diff --git a/src/enrollment/enrollment.module.ts b/src/enrollment/enrollment.module.ts index bf0cd26..1201c1a 100644 --- a/src/enrollment/enrollment.module.ts +++ b/src/enrollment/enrollment.module.ts @@ -4,6 +4,8 @@ import { SessionAuthGuard } from '../auth/guards/session-auth.guard'; import { SessionLifecycleGuard } from '../auth/guards/session-lifecycle.guard'; import { CandidateModule } from '../candidate/candidate.module'; import { FileStorageModule } from '../file-storage/file-storage.module'; +import { MailModule } from '../mail/mail.module'; +import { ResearchThemeModule } from '../research-theme/research-theme.module'; import { RolesGuard } from '../roles/roles.guard'; import { UsersModule } from '../users/users.module'; import { EnrollmentPeriodController } from './enrollment-period.controller'; @@ -14,7 +16,14 @@ import { EnrollmentService } from './enrollment.service'; import { EnrollmentDrizzlePersistenceModule } from './infrastructure/persistence/drizzle/drizzle-persistence.module'; @Module({ - imports: [UsersModule, CandidateModule, FileStorageModule, EnrollmentDrizzlePersistenceModule], + imports: [ + UsersModule, + CandidateModule, + FileStorageModule, + EnrollmentDrizzlePersistenceModule, + ResearchThemeModule, + MailModule, + ], controllers: [EnrollmentPeriodController, EnrollmentController], providers: [ EnrollmentPeriodService, diff --git a/src/enrollment/enrollment.service.spec.ts b/src/enrollment/enrollment.service.spec.ts index 73a963c..83c492a 100644 --- a/src/enrollment/enrollment.service.spec.ts +++ b/src/enrollment/enrollment.service.spec.ts @@ -7,6 +7,8 @@ import { import { Test } from '@nestjs/testing'; import { getLoggerToken } from 'nestjs-pino'; import { FileStorageService } from '../file-storage/file-storage.service'; +import { MailService } from '../mail/mail.service'; +import { ResearchThemeService } from '../research-theme/research-theme.service'; import { RoleEnum } from '../roles/roles.enum'; import { UsersService } from '../users/users.service'; import { EnrollmentLevel } from './dto/enrollment-level.enum'; @@ -21,6 +23,8 @@ describe('EnrollmentService', () => { let mockUsersService: Record; let mockEnrollmentPeriodService: Record; let mockFileStorageService: Record; + let mockResearchThemeService: Record; + let mockMailService: Record; const mockUser = { id: 'user-uuid', @@ -53,6 +57,8 @@ describe('EnrollmentService', () => { sigaaCode: null, sigaaReceiptFileId: null, declaration: false, + primaryThemeId: null, + secondaryThemeId: null, poscomp: null, mastersDegrees: null, scoreDraft: null, @@ -89,6 +95,18 @@ describe('EnrollmentService', () => { getSignedDownloadUrl: jest.fn().mockResolvedValue('https://signed-url.example.com'), }; + mockResearchThemeService = { + findById: jest.fn().mockResolvedValue({ + id: 'theme-uuid', + level: 'masters', + title: 'Tema de Teste', + }), + }; + + mockMailService = { + send: jest.fn().mockResolvedValue(undefined), + }; + const module = await Test.createTestingModule({ providers: [ EnrollmentService, @@ -102,6 +120,14 @@ describe('EnrollmentService', () => { provide: FileStorageService, useValue: mockFileStorageService, }, + { + provide: ResearchThemeService, + useValue: mockResearchThemeService, + }, + { + provide: MailService, + useValue: mockMailService, + }, { provide: getLoggerToken(EnrollmentService.name), useValue: { @@ -263,6 +289,11 @@ describe('EnrollmentService', () => { ...mockEnrollment, phone: '11999999999', justification: 'Minha justificativa', + declaration: true, + sigaaCode: 'sigaa-code', + sigaaReceiptFileId: 'file-uuid', + primaryThemeId: 'theme-uuid-1', + secondaryThemeId: 'theme-uuid-2', }; mockRepository.findById.mockResolvedValueOnce(readyEnrollment); @@ -272,6 +303,9 @@ describe('EnrollmentService', () => { submittedAt: new Date(), }; mockRepository.update.mockResolvedValueOnce(submittedEnrollment); + mockResearchThemeService.findById + .mockResolvedValueOnce({ id: 'theme-uuid-1', level: 'masters', title: 'Theme 1' }) + .mockResolvedValueOnce({ id: 'theme-uuid-2', level: 'masters', title: 'Theme 2' }); const result = await service.submit('user-uuid', 'enrollment-uuid'); @@ -279,11 +313,49 @@ describe('EnrollmentService', () => { expect(result.submittedAt).toBeDefined(); }); + it('triggers confirmation email on successful submission', async () => { + const readyEnrollment = { + ...mockEnrollment, + phone: '11999999999', + justification: 'Minha justificativa', + declaration: true, + sigaaCode: 'sigaa-code', + sigaaReceiptFileId: 'file-uuid', + primaryThemeId: 'theme-uuid-1', + secondaryThemeId: 'theme-uuid-2', + }; + mockRepository.findById.mockResolvedValueOnce(readyEnrollment); + + const submittedEnrollment = { + ...readyEnrollment, + status: 'submitted', + submittedAt: new Date(), + }; + mockRepository.update.mockResolvedValueOnce(submittedEnrollment); + mockResearchThemeService.findById + .mockResolvedValueOnce({ id: 'theme-uuid-1', level: 'masters', title: 'Theme 1' }) + .mockResolvedValueOnce({ id: 'theme-uuid-2', level: 'masters', title: 'Theme 2' }); + + await service.submit('user-uuid', 'enrollment-uuid'); + + expect(mockMailService.send).toHaveBeenCalledWith( + expect.objectContaining({ + to: mockUser.email, + title: 'Inscrição Submetida com Sucesso - MDCC', + }), + ); + }); + it('rejects submission with missing phone', async () => { const incompleteEnrollment = { ...mockEnrollment, phone: null, justification: 'Some justification', + declaration: true, + sigaaCode: 'sigaa-code', + sigaaReceiptFileId: 'file-uuid', + primaryThemeId: 'theme-uuid-1', + secondaryThemeId: 'theme-uuid-2', }; mockRepository.findById.mockResolvedValueOnce(incompleteEnrollment); @@ -297,6 +369,11 @@ describe('EnrollmentService', () => { ...mockEnrollment, phone: '11999999999', justification: null, + declaration: true, + sigaaCode: 'sigaa-code', + sigaaReceiptFileId: 'file-uuid', + primaryThemeId: 'theme-uuid-1', + secondaryThemeId: 'theme-uuid-2', }; mockRepository.findById.mockResolvedValueOnce(incompleteEnrollment); @@ -319,6 +396,11 @@ describe('EnrollmentService', () => { candidateId: 'other-user-uuid', phone: '11999999999', justification: 'test', + declaration: true, + sigaaCode: 'sigaa-code', + sigaaReceiptFileId: 'file-uuid', + primaryThemeId: 'theme-uuid-1', + secondaryThemeId: 'theme-uuid-2', }); await expect(service.submit('user-uuid', 'enrollment-uuid')).rejects.toThrow( @@ -327,6 +409,70 @@ describe('EnrollmentService', () => { }); }); + describe('updateThemes', () => { + it('updates primary and secondary themes', async () => { + const draftEnrollment = { + ...mockEnrollment, + status: 'draft', + level: 'masters', + }; + mockRepository.findById.mockResolvedValueOnce(draftEnrollment); + mockResearchThemeService.findById + .mockResolvedValueOnce({ id: 'theme-uuid-1', level: 'masters', title: 'Theme 1' }) + .mockResolvedValueOnce({ id: 'theme-uuid-2', level: 'masters', title: 'Theme 2' }); + + const updatedEnrollment = { + ...draftEnrollment, + primaryThemeId: 'theme-uuid-1', + secondaryThemeId: 'theme-uuid-2', + }; + mockRepository.update.mockResolvedValueOnce(updatedEnrollment); + + const result = await service.updateThemes('user-uuid', 'enrollment-uuid', { + primaryThemeId: 'theme-uuid-1', + secondaryThemeId: 'theme-uuid-2', + }); + + expect(result.primaryThemeId).toBe('theme-uuid-1'); + expect(result.secondaryThemeId).toBe('theme-uuid-2'); + }); + + it('rejects duplicate primary and secondary themes', async () => { + const draftEnrollment = { + ...mockEnrollment, + status: 'draft', + level: 'masters', + }; + mockRepository.findById.mockResolvedValueOnce(draftEnrollment); + + await expect( + service.updateThemes('user-uuid', 'enrollment-uuid', { + primaryThemeId: 'theme-uuid-1', + secondaryThemeId: 'theme-uuid-1', + }), + ).rejects.toThrow(BadRequestException); + }); + + it('rejects incompatible theme levels', async () => { + const draftEnrollment = { + ...mockEnrollment, + status: 'draft', + level: 'masters', + }; + mockRepository.findById.mockResolvedValueOnce(draftEnrollment); + mockResearchThemeService.findById + .mockResolvedValueOnce({ id: 'theme-uuid-1', level: 'doctoral', title: 'Theme 1' }) + .mockResolvedValueOnce({ id: 'theme-uuid-2', level: 'masters', title: 'Theme 2' }); + + await expect( + service.updateThemes('user-uuid', 'enrollment-uuid', { + primaryThemeId: 'theme-uuid-1', + secondaryThemeId: 'theme-uuid-2', + }), + ).rejects.toThrow(BadRequestException); + }); + }); + describe('updateStatus', () => { it('updates enrollment status', async () => { mockRepository.findById.mockResolvedValueOnce(mockEnrollment); diff --git a/src/enrollment/enrollment.service.ts b/src/enrollment/enrollment.service.ts index 40db1e2..4304666 100644 --- a/src/enrollment/enrollment.service.ts +++ b/src/enrollment/enrollment.service.ts @@ -9,6 +9,8 @@ import { import type { PaginatedResult } from '../common/dto/paginated-response.dto'; import { FileStorageService } from '../file-storage/file-storage.service'; +import { MailService } from '../mail/mail.service'; +import { ResearchThemeService } from '../research-theme/research-theme.service'; import { RoleEnum } from '../roles/roles.enum'; import { UsersService } from '../users/users.service'; import { ENROLLMENT_STATUS, PERIOD_STATUS } from './constants/enrollment-status'; @@ -17,6 +19,7 @@ import type { CreateEnrollmentDto } from './dto/create-enrollment.dto'; import type { FindEnrollmentsDto } from './dto/find-enrollments.dto'; import type { UpdateMastersDegreesDto } from './dto/masters-degree.dto'; import type { UpdateEnrollmentStatusDto } from './dto/update-enrollment-status.dto'; +import { UpdateEnrollmentThemesDto } from './dto/update-enrollment-themes.dto'; import type { UpdateEnrollmentDto } from './dto/update-enrollment.dto'; import { EnrollmentPeriodService } from './enrollment-period.service'; import { EnrollmentRepository } from './infrastructure/persistence/enrollment.repository'; @@ -30,6 +33,8 @@ export class EnrollmentService { private readonly usersService: UsersService, private readonly enrollmentPeriodService: EnrollmentPeriodService, private readonly fileStorageService: FileStorageService, + private readonly researchThemeService: ResearchThemeService, + private readonly mailService: MailService, ) {} async create(userId: string, dto: CreateEnrollmentDto): Promise { @@ -133,6 +138,57 @@ export class EnrollmentService { return updated; } + async updateThemes( + userId: string, + id: string, + dto: UpdateEnrollmentThemesDto, + ): Promise { + const enrollment = await this.findById(id); + + if (enrollment.candidateId !== userId) { + throw new ForbiddenException('Você não tem permissão para editar esta inscrição.'); + } + + if (enrollment.status !== ENROLLMENT_STATUS.DRAFT) { + throw new BadRequestException('Apenas inscrições em rascunho podem ser editadas.'); + } + + const period = await this.enrollmentPeriodService.findById(enrollment.enrollmentPeriodId); + if (period.status !== PERIOD_STATUS.OPEN) { + throw new BadRequestException('O período de inscrição não está mais aberto.'); + } + + if (dto.primaryThemeId === dto.secondaryThemeId) { + throw new BadRequestException('Os temas primário e secundário devem ser diferentes.'); + } + + const primaryTheme = await this.researchThemeService.findById(dto.primaryThemeId); + const secondaryTheme = await this.researchThemeService.findById(dto.secondaryThemeId); + + if ( + (primaryTheme.level as string) !== enrollment.level || + (secondaryTheme.level as string) !== enrollment.level + ) { + throw new BadRequestException( + 'Os temas selecionados devem ser compatíveis com o nível da inscrição.', + ); + } + + const now = new Date(); + const updated = await this.enrollmentRepository.update(id, { + primaryThemeId: dto.primaryThemeId, + secondaryThemeId: dto.secondaryThemeId, + updatedAt: now, + }); + + if (!updated) { + throw new NotFoundException('Inscrição não encontrada.'); + } + + this.logger.log(`Temas da inscrição atualizados: ${id}`); + return updated; + } + async submit(userId: string, id: string): Promise { const enrollment = await this.findById(id); @@ -149,14 +205,64 @@ export class EnrollmentService { throw new BadRequestException('O período de inscrição não está mais aberto.'); } + const errors: string[] = []; + if (!enrollment.phone) { - throw new BadRequestException('O campo telefone é obrigatório para submeter a inscrição.'); + errors.push('O campo telefone é obrigatório.'); } - if (!enrollment.justification) { - throw new BadRequestException( - 'O campo justificativa é obrigatório para submeter a inscrição.', - ); + errors.push('O campo justificativa é obrigatório.'); + } + if (!enrollment.declaration) { + errors.push('A declaração de veracidade é obrigatória.'); + } + if (!enrollment.sigaaCode) { + errors.push('O código SIGAA é obrigatório.'); + } + if (!enrollment.sigaaReceiptFileId) { + errors.push('O comprovante de inscrição do SIGAA é obrigatório.'); + } + if (!enrollment.primaryThemeId) { + errors.push('O tema primário é obrigatório.'); + } + if (!enrollment.secondaryThemeId) { + errors.push('O tema secundário é obrigatório.'); + } + + if (enrollment.level === 'doctoral') { + if (!enrollment.mastersDegrees || enrollment.mastersDegrees.length === 0) { + errors.push( + 'Pelo menos um curso de mestrado deve ser informado para inscrições de doutorado.', + ); + } else { + const primaryCount = enrollment.mastersDegrees.filter(d => d.isPrimary).length; + if (primaryCount !== 1) { + errors.push('Exatamente um curso de mestrado deve ser marcado como principal.'); + } + } + } + + if (enrollment.poscomp && enrollment.poscomp.hasPoscomp) { + const poscomp = enrollment.poscomp; + if (!poscomp.year) { + errors.push('O ano do POSCOMP é obrigatório.'); + } + if (poscomp.mathScore === undefined || poscomp.mathScore === null) { + errors.push('A nota de Matemática do POSCOMP é obrigatória.'); + } + if (poscomp.fundamentalsScore === undefined || poscomp.fundamentalsScore === null) { + errors.push('A nota de Fundamentos do POSCOMP é obrigatória.'); + } + if (poscomp.technologyScore === undefined || poscomp.technologyScore === null) { + errors.push('A nota de Tecnologia do POSCOMP é obrigatória.'); + } + if (!poscomp.receiptFileId) { + errors.push('O comprovante do POSCOMP é obrigatório.'); + } + } + + if (errors.length > 0) { + throw new BadRequestException(errors); } const now = new Date(); @@ -171,6 +277,43 @@ export class EnrollmentService { } this.logger.log(`Inscrição submetida: ${id} por ${userId}`); + + const user = await this.usersService.findById(userId); + if (user && user.email) { + try { + const primaryTheme = await this.researchThemeService.findById(updated.primaryThemeId!); + const secondaryTheme = await this.researchThemeService.findById(updated.secondaryThemeId!); + + const title = 'Inscrição Submetida com Sucesso - MDCC'; + const body = ` +

    Olá, ${user.firstName} ${user.lastName},

    +

    Sua inscrição no processo seletivo do MDCC foi submetida com sucesso!

    +

    Detalhes da inscrição:

    +
      +
    • ID da Inscrição: ${id}
    • +
    • Nível: ${updated.level === 'masters' ? 'Mestrado' : 'Doutorado'}
    • +
    • Código SIGAA: ${updated.sigaaCode}
    • +
    • Tema de Pesquisa Primário: ${primaryTheme.title}
    • +
    • Tema de Pesquisa Secundário: ${secondaryTheme.title}
    • +
    +
    +

    Atenciosamente,

    +

    Coordenação do MDCC / UFC

    + `; + await this.mailService.send({ + to: user.email, + title, + body, + }); + this.logger.log(`E-mail de confirmação enviado para ${user.email} para a inscrição ${id}`); + } catch (err) { + this.logger.error( + `Falha ao enviar e-mail de confirmação ou recuperar temas para a inscrição ${id}`, + err, + ); + } + } + return updated; } diff --git a/src/enrollment/infrastructure/persistence/enrollment.repository.ts b/src/enrollment/infrastructure/persistence/enrollment.repository.ts index 43a94c5..135c0ec 100644 --- a/src/enrollment/infrastructure/persistence/enrollment.repository.ts +++ b/src/enrollment/infrastructure/persistence/enrollment.repository.ts @@ -14,6 +14,8 @@ export interface UpdateEnrollmentData { sigaaCode?: string; declaration?: boolean; poscomp?: Record; + primaryThemeId?: string | null; + secondaryThemeId?: string | null; updatedAt: Date; } From 06a40087ce380afec2fabd096e277f90b8c6051f Mon Sep 17 00:00:00 2001 From: Gabrigas Date: Mon, 1 Jun 2026 19:46:53 -0300 Subject: [PATCH 3/6] docs: move skills to root dir --- .../skills/shadcn-tanstack-forms/SKILL.md | 0 .../references/field-types.md | 0 .../skills/shadcn/SKILL.md | 0 .../skills/shadcn/agents/openai.yml | 0 .../skills/shadcn/assets/shadcn-small.png | Bin .../skills/shadcn/assets/shadcn.png | Bin .../.agents => .agents}/skills/shadcn/cli.md | 0 .../skills/shadcn/customization.md | 0 .../skills/shadcn/evals/evals.json | 0 .../.agents => .agents}/skills/shadcn/mcp.md | 0 .../skills/shadcn/rules/base-vs-radix.md | 0 .../skills/shadcn/rules/composition.md | 0 .../skills/shadcn/rules/forms.md | 0 .../skills/shadcn/rules/icons.md | 0 .../skills/shadcn/rules/styling.md | 0 AGENTS.md | 99 ++++++++++++++++++ 16 files changed, 99 insertions(+) rename {frontend/.agents => .agents}/skills/shadcn-tanstack-forms/SKILL.md (100%) rename {frontend/.agents => .agents}/skills/shadcn-tanstack-forms/references/field-types.md (100%) rename {frontend/.agents => .agents}/skills/shadcn/SKILL.md (100%) rename {frontend/.agents => .agents}/skills/shadcn/agents/openai.yml (100%) rename {frontend/.agents => .agents}/skills/shadcn/assets/shadcn-small.png (100%) rename {frontend/.agents => .agents}/skills/shadcn/assets/shadcn.png (100%) rename {frontend/.agents => .agents}/skills/shadcn/cli.md (100%) rename {frontend/.agents => .agents}/skills/shadcn/customization.md (100%) rename {frontend/.agents => .agents}/skills/shadcn/evals/evals.json (100%) rename {frontend/.agents => .agents}/skills/shadcn/mcp.md (100%) rename {frontend/.agents => .agents}/skills/shadcn/rules/base-vs-radix.md (100%) rename {frontend/.agents => .agents}/skills/shadcn/rules/composition.md (100%) rename {frontend/.agents => .agents}/skills/shadcn/rules/forms.md (100%) rename {frontend/.agents => .agents}/skills/shadcn/rules/icons.md (100%) rename {frontend/.agents => .agents}/skills/shadcn/rules/styling.md (100%) diff --git a/frontend/.agents/skills/shadcn-tanstack-forms/SKILL.md b/.agents/skills/shadcn-tanstack-forms/SKILL.md similarity index 100% rename from frontend/.agents/skills/shadcn-tanstack-forms/SKILL.md rename to .agents/skills/shadcn-tanstack-forms/SKILL.md diff --git a/frontend/.agents/skills/shadcn-tanstack-forms/references/field-types.md b/.agents/skills/shadcn-tanstack-forms/references/field-types.md similarity index 100% rename from frontend/.agents/skills/shadcn-tanstack-forms/references/field-types.md rename to .agents/skills/shadcn-tanstack-forms/references/field-types.md diff --git a/frontend/.agents/skills/shadcn/SKILL.md b/.agents/skills/shadcn/SKILL.md similarity index 100% rename from frontend/.agents/skills/shadcn/SKILL.md rename to .agents/skills/shadcn/SKILL.md diff --git a/frontend/.agents/skills/shadcn/agents/openai.yml b/.agents/skills/shadcn/agents/openai.yml similarity index 100% rename from frontend/.agents/skills/shadcn/agents/openai.yml rename to .agents/skills/shadcn/agents/openai.yml diff --git a/frontend/.agents/skills/shadcn/assets/shadcn-small.png b/.agents/skills/shadcn/assets/shadcn-small.png similarity index 100% rename from frontend/.agents/skills/shadcn/assets/shadcn-small.png rename to .agents/skills/shadcn/assets/shadcn-small.png diff --git a/frontend/.agents/skills/shadcn/assets/shadcn.png b/.agents/skills/shadcn/assets/shadcn.png similarity index 100% rename from frontend/.agents/skills/shadcn/assets/shadcn.png rename to .agents/skills/shadcn/assets/shadcn.png diff --git a/frontend/.agents/skills/shadcn/cli.md b/.agents/skills/shadcn/cli.md similarity index 100% rename from frontend/.agents/skills/shadcn/cli.md rename to .agents/skills/shadcn/cli.md diff --git a/frontend/.agents/skills/shadcn/customization.md b/.agents/skills/shadcn/customization.md similarity index 100% rename from frontend/.agents/skills/shadcn/customization.md rename to .agents/skills/shadcn/customization.md diff --git a/frontend/.agents/skills/shadcn/evals/evals.json b/.agents/skills/shadcn/evals/evals.json similarity index 100% rename from frontend/.agents/skills/shadcn/evals/evals.json rename to .agents/skills/shadcn/evals/evals.json diff --git a/frontend/.agents/skills/shadcn/mcp.md b/.agents/skills/shadcn/mcp.md similarity index 100% rename from frontend/.agents/skills/shadcn/mcp.md rename to .agents/skills/shadcn/mcp.md diff --git a/frontend/.agents/skills/shadcn/rules/base-vs-radix.md b/.agents/skills/shadcn/rules/base-vs-radix.md similarity index 100% rename from frontend/.agents/skills/shadcn/rules/base-vs-radix.md rename to .agents/skills/shadcn/rules/base-vs-radix.md diff --git a/frontend/.agents/skills/shadcn/rules/composition.md b/.agents/skills/shadcn/rules/composition.md similarity index 100% rename from frontend/.agents/skills/shadcn/rules/composition.md rename to .agents/skills/shadcn/rules/composition.md diff --git a/frontend/.agents/skills/shadcn/rules/forms.md b/.agents/skills/shadcn/rules/forms.md similarity index 100% rename from frontend/.agents/skills/shadcn/rules/forms.md rename to .agents/skills/shadcn/rules/forms.md diff --git a/frontend/.agents/skills/shadcn/rules/icons.md b/.agents/skills/shadcn/rules/icons.md similarity index 100% rename from frontend/.agents/skills/shadcn/rules/icons.md rename to .agents/skills/shadcn/rules/icons.md diff --git a/frontend/.agents/skills/shadcn/rules/styling.md b/.agents/skills/shadcn/rules/styling.md similarity index 100% rename from frontend/.agents/skills/shadcn/rules/styling.md rename to .agents/skills/shadcn/rules/styling.md diff --git a/AGENTS.md b/AGENTS.md index d923e07..b2050bb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -182,3 +182,102 @@ For schema/repository changes, also run relevant integration tests. - Check `package.json`, `eslint.config.mjs` before assuming behavior - Preserve Portuguese user-facing messages - Don't reintroduce removed multi-provider linking + + + +## Long-term memory (ai-memory) + +This project uses [ai-memory](https://github.com/akitaonrails/ai-memory) +for cross-session continuity. **Lifecycle hooks already capture every +prompt + tool call automatically.** You never need to manually write +notes; the SessionStart hook auto-fetches pending handoffs and the +SessionEnd hook auto-consolidates. Just _use_ the read tools. + +### When to reach for each tool + +The user can express any of the intents below in plain English — +match the intent to the tool. They do not need to name the tool. + +| User says / situation | Tool | +| ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | +| "have we discussed X?" / "search memory for Y" / before proposing architecture | `memory_query` | +| "what's been going on" / "show recent activity" (light) | `memory_recent` | +| "is ai-memory healthy?" / "how big is the wiki?" | `memory_status` | +| "give me the stats" / structured snapshot for the agent to consume | `memory_briefing` | +| "catch me up" / "I've been away" / "what's important right now?" / open-ended exploration | `memory_explore` | +| "where did we leave off?" — and you see a `📥 ai-memory: pending handoff` block in your context | already done — answer from that block; do NOT re-call `memory_handoff_accept` | +| "where did we leave off?" — and no such block is visible | `memory_handoff_accept` (rare; the SessionStart hook usually got there first) | +| "save context for the next session" / wrapping up | `memory_handoff_begin` (terse summary; put detail in `open_questions` + `next_steps` bullets) | +| "consolidate this session" / "compile what we learned" (usually automatic) | `memory_consolidate` | +| "audit the wiki" / "find contradictions" / "what rules should we add?" | `memory_lint` | +| "prune old pages" / "memory cleanup" | `memory_forget_sweep` | + +`memory_explore` is the right default for the "I want to know what's +going on" use case — it returns a prose digest whose verbosity +scales automatically to how long it's been since the last activity +(< 1 h → one line; > 30 days → full catchup). + +### When you write a project rule, write it here + +If you're about to write a durable project rule ("always X", "never +Y", "all PRs must …"), this rules file (CLAUDE.md for Claude Code; +AGENTS.md for Codex / OpenCode / Cursor / Gemini CLI; whichever +convention your agent uses) is where it belongs. ai-memory's lint +pass surfaces the same hint automatically when a `kind: rule` page +lands in `_rules/`. + +### Refreshing this snippet + +This block is maintained by ai-memory. Two ways to refresh it with +the latest binary's recommended copy: + +- **From the agent** (no terminal needed): ask "refresh the ai-memory + routing in this project" — the agent calls + `memory_install_self_routing`, picks the right filename for itself + (Claude Code → `CLAUDE.md`; Codex / OpenCode / Cursor / Gemini → + `AGENTS.md`), and uses its Write / Edit tool to land the block. +- **From the CLI**: `ai-memory install-instructions` (defaults to + `CLAUDE.md`; pass `--target AGENTS.md` for non-Claude agents). + +Both are idempotent: re-runs replace the block bracketed by +`` / `` markers +without disturbing the rest of the file. + + + + + +# RTK - Rust Token Killer (Codex CLI) + +**Usage**: Token-optimized CLI proxy for shell commands. + +## Rule + +Always prefix shell commands with `rtk`. + +Examples: + +```bash +rtk git status +rtk cargo test +rtk npm run build +rtk pytest -q +``` + +## Meta Commands + +```bash +rtk gain # Token savings analytics +rtk gain --history # Recent command savings history +rtk proxy # Run raw command without filtering +``` + +## Verification + +```bash +rtk --version +rtk gain +which rtk +``` + + From 95d5d2fb2f90d83868b1b427ba4d2f2935eccdec Mon Sep 17 00:00:00 2001 From: Gabrigas Date: Mon, 1 Jun 2026 19:47:08 -0300 Subject: [PATCH 4/6] chore: update seed script --- package.json | 7 +- scripts/seed/main.ts | 557 +++++++++++++++++++++++++++++ scripts/seed/seed-cv-categories.ts | 98 ----- scripts/seed/seed-professors.ts | 192 ---------- scripts/seed/seed-themes.ts | 229 ------------ 5 files changed, 560 insertions(+), 523 deletions(-) create mode 100644 scripts/seed/main.ts delete mode 100644 scripts/seed/seed-cv-categories.ts delete mode 100644 scripts/seed/seed-professors.ts delete mode 100644 scripts/seed/seed-themes.ts diff --git a/package.json b/package.json index bfb9d0a..baa0a58 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", - "lint:check": "eslint \"{src,apps,libs,test}/**/*.ts\"", - "lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "lint:check": "eslint --quiet \"{src,apps,libs,test}/**/*.ts\" ", + "lint:fix": "eslint --quiet \"{src,apps,libs,test}/**/*.ts\" --fix", "typecheck": "tsc -p tsconfig.build.json --noEmit --pretty false", "test": "jest", "test:watch": "jest --watch", @@ -25,8 +25,7 @@ "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", "db:studio": "drizzle-kit studio", - "db:seed": "ts-node -r tsconfig-paths/register scripts/seed/seed-professors.ts", - "db:seed:themes": "ts-node -r tsconfig-paths/register scripts/seed/seed-themes.ts" + "db:seed": "ts-node -r tsconfig-paths/register scripts/seed/main.ts" }, "dependencies": { "@aws-sdk/client-s3": "^3.1057.0", diff --git a/scripts/seed/main.ts b/scripts/seed/main.ts new file mode 100644 index 0000000..c4b369c --- /dev/null +++ b/scripts/seed/main.ts @@ -0,0 +1,557 @@ +/* eslint-disable no-console */ +import * as bcrypt from 'bcrypt'; +import { eq } from 'drizzle-orm'; +import { drizzle, type NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { Pool } from 'pg'; +import { cvScoringCategories } from '../../src/database/schema/cv-scoring'; +import { enrollmentPeriods } from '../../src/database/schema/enrollment-periods'; +import { professors } from '../../src/database/schema/professor'; +import { researchThemeProfessors, researchThemes } from '../../src/database/schema/research-themes'; +import { users } from '../../src/database/schema/users'; + +// ========================================== +// Types & Interfaces +// ========================================== +interface RawMockProfessor { + nome: string; + cpf: string; + email: string; + institution: string; + department: string; + status: 'active' | 'inactive' | 'disabled'; +} + +interface GeneratedProfessorData { + email: string; + cpf: string; + firstName: string; + lastName: string | null; + institution: string; + department: string; + status: 'active' | 'inactive' | 'disabled'; +} + +interface DefaultUserData { + email: string; + role: + | 'candidate' + | 'professor' + | 'mdcc-secretary' + | 'post-graduate-coordinator' + | 'post-graduate-vice-coordinator'; + firstName: string; + lastName: string; +} + +// ========================================== +// Mock Data Constants +// ========================================== +const MOCK_PROFESSOR_BASE: RawMockProfessor[] = [ + { + nome: 'Dr. Ricardo Almeida', + cpf: '12345678909', + email: 'r.almeida@ufc.br', + institution: 'UFC', + department: 'Inteligencia Artificial', + status: 'active', + }, + { + nome: 'Dra. Ana Souza', + cpf: '39053344705', + email: 'ana.souza@mdcc.ufc.br', + institution: 'UFC', + department: 'Engenharia de Software', + status: 'inactive', + }, + { + nome: 'Dr. Carlos Mendes', + cpf: '11144477735', + email: 'c.mendes@ufc.br', + institution: 'UFC', + department: 'Sistemas Distribuidos', + status: 'active', + }, + { + nome: 'Dr. João Silveira', + cpf: '98765432100', + email: 'j.silveira@ufc.br', + institution: 'UFC', + department: 'Redes de Computadores', + status: 'inactive', + }, +]; + +const DEFAULT_USERS: DefaultUserData[] = [ + { + email: 'candidate@anubis.com', + role: 'candidate', + firstName: 'Candidato', + lastName: 'Padrão', + }, + { + email: 'professor@anubis.com', + role: 'professor', + firstName: 'Professor', + lastName: 'Padrão', + }, + { + email: 'secretary@anubis.com', + role: 'mdcc-secretary', + firstName: 'Secretário', + lastName: 'Padrão', + }, + { + email: 'coordinator@anubis.com', + role: 'post-graduate-coordinator', + firstName: 'Coordenador', + lastName: 'Padrão', + }, + { + email: 'vice@anubis.com', + role: 'post-graduate-vice-coordinator', + firstName: 'Vice', + lastName: 'Coordenador Padrão', + }, +]; + +const MASTERS_CATEGORIES = [ + { + name: 'Artigos publicados em periódicos', + description: 'Artigos completos em periódicos indexados (Qualis A1-B2)', + pointsPerItem: 1.0, + maxPoints: 3.0, + sortOrder: 1, + }, + { + name: 'Artigos publicados em conferências', + description: 'Artigos completos em anais de conferências nacionais e internacionais', + pointsPerItem: 0.75, + maxPoints: 2.25, + sortOrder: 2, + }, + { + name: 'Iniciação científica / TCC', + description: 'Participação em projetos de IC ou trabalho de conclusão de curso com orientador', + pointsPerItem: 0.5, + maxPoints: 1.5, + sortOrder: 3, + }, + { + name: 'Experiência profissional na área', + description: 'Atuação profissional comprovada na área de Ciência da Computação', + pointsPerItem: 0.5, + maxPoints: 1.0, + sortOrder: 4, + }, + { + name: 'Cursos e certificações', + description: 'Cursos de extensão, especializações e certificações técnicas relevantes', + pointsPerItem: 0.25, + maxPoints: 1.0, + sortOrder: 5, + }, + { + name: 'Participação em eventos', + description: 'Apresentação de pôsteres, palestras ou workshops em eventos acadêmicos', + pointsPerItem: 0.25, + maxPoints: 0.75, + sortOrder: 6, + }, + { + name: 'Premiações acadêmicas', + description: 'Prêmios, menções honrosas e reconhecimentos acadêmicos', + pointsPerItem: 0.5, + maxPoints: 1.0, + sortOrder: 7, + }, +]; + +const DOCTORAL_CATEGORIES = [ + { + name: 'Artigos publicados em periódicos', + description: 'Artigos completos em periódicos indexados (Qualis A1-B2)', + pointsPerItem: 1.5, + maxPoints: 6.0, + sortOrder: 1, + }, + { + name: 'Artigos publicados em conferências', + description: 'Artigos completos em anais de conferências nacionais e internacionais', + pointsPerItem: 1.0, + maxPoints: 4.0, + sortOrder: 2, + }, + { + name: 'Dissertação de mestrado', + description: 'Nota e relevância da dissertação de mestrado defendida', + pointsPerItem: 2.0, + maxPoints: 2.0, + sortOrder: 3, + }, + { + name: 'Participação em projetos de pesquisa', + description: 'Projetos de pesquisa com financiamento ou vinculação institutional', + pointsPerItem: 1.0, + maxPoints: 3.0, + sortOrder: 4, + }, + { + name: 'Experiência docente', + description: 'Atuação como professor ou monitor em nível superior', + pointsPerItem: 0.5, + maxPoints: 1.5, + sortOrder: 5, + }, + { + name: 'Patentes e softwares registrados', + description: 'Registros de propriedade intelectual na área', + pointsPerItem: 1.0, + maxPoints: 2.0, + sortOrder: 6, + }, + { + name: 'Premiações acadêmicas', + description: 'Prêmios, menções honrosas e reconhecimentos acadêmicos ou profissionais', + pointsPerItem: 0.5, + maxPoints: 1.5, + sortOrder: 7, + }, +]; + +// ========================================== +// Helper Functions +// ========================================== +function generateMockProfessors(count: number): GeneratedProfessorData[] { + return Array.from({ length: count }, (_, index) => { + const base = MOCK_PROFESSOR_BASE[index % MOCK_PROFESSOR_BASE.length]; + const cycle = Math.floor(index / MOCK_PROFESSOR_BASE.length) + 1; + + const emailParts = base.email.split('@'); + if (emailParts.length !== 2) { + throw new Error(`Malformed base email template encountered: ${base.email}`); + } + + const email = cycle === 1 ? base.email : `${emailParts[0]}+${cycle}@${emailParts[1]}`; + const splitNome = (cycle === 1 ? base.nome : `${base.nome} ${cycle}`).split(' '); + + const firstName = `${splitNome[0]} ${splitNome[1]}`; + const lastName = splitNome.slice(2).join(' ') || null; + const cpf = `${base.cpf.slice(0, 9)}${String(cycle).padStart(2, '0')}`.slice(0, 11); + + return { + email, + cpf, + firstName, + lastName, + institution: base.institution, + department: base.department, + status: base.status, + }; + }); +} + +function createDatabasePool(): Pool { + return new Pool({ + host: process.env.DATABASE_HOST || 'localhost', + port: Number(process.env.DATABASE_PORT) || 5432, + user: process.env.DATABASE_USER || 'postgres', + password: process.env.DATABASE_PASSWORD || 'postgres', + database: process.env.DATABASE_NAME || 'anubis', + ssl: process.env.DATABASE_SSL === 'true', + }); +} + +// ========================================== +// Seeding Phases +// ========================================== + +async function seedProfessors(db: NodePgDatabase): Promise { + const generatedProfessors = generateMockProfessors(14); + console.log(`[INFO] Starting insertion of ${generatedProfessors.length} mock professors.`); + + await db.transaction(async tx => { + for (const prof of generatedProfessors) { + // 1. Insert user if conflict on email do nothing + const rows = await tx + .insert(users) + .values({ + authProvider: 'email', + providerSubject: prof.email, + email: prof.email, + password: '$2a$12$lr9fx486D2ZJT1rmHu4xtOUOxRuapGfZwmdDhVKNzpBCNlHNXwvc.', // bcrypt hash for senha123 + cpf: prof.cpf, + firstName: prof.firstName, + lastName: prof.lastName, + role: 'professor', + status: prof.status, + onboardingCompleted: true, + mustChangePassword: true, + }) + .onConflictDoNothing({ target: users.email }) + .returning(); + + let userId = Array.isArray(rows) && rows[0] ? rows[0].id : null; + + // If user existed already, find their ID + if (!userId) { + const [existing] = await tx + .select({ id: users.id }) + .from(users) + .where(eq(users.email, prof.email)) + .limit(1); + if (existing) { + userId = existing.id; + } + } + + if (userId) { + await tx + .insert(professors) + .values({ + userId, + department: prof.department, + institution: prof.institution, + }) + .onConflictDoNothing({ target: professors.userId }); + } else { + console.warn(`[WARN] Failed to insert or find user for email: ${prof.email}`); + } + } + }); + + console.log('[SUCCESS] Professors seed completed.'); +} + +async function seedDefaultUsersAndThemes(db: NodePgDatabase): Promise { + console.log('[INFO] Seeding default users...'); + const userMap: Record = {}; + const hash = await bcrypt.hash('senha123', 10); + + for (const defaultUser of DEFAULT_USERS) { + const [existing] = await db + .select() + .from(users) + .where(eq(users.email, defaultUser.email)) + .limit(1); + + let userId: string; + if (existing) { + console.log(`[INFO] User ${defaultUser.email} already exists.`); + userId = existing.id; + userMap[defaultUser.email] = userId; + } else { + const [inserted] = await db + .insert(users) + .values({ + authProvider: 'email', + providerSubject: defaultUser.email, + email: defaultUser.email, + password: hash, + cpf: defaultUser.email.startsWith('candidate') + ? '00000000001' + : defaultUser.email.startsWith('professor') + ? '00000000002' + : defaultUser.email.startsWith('secretary') + ? '00000000003' + : defaultUser.email.startsWith('coordinator') + ? '00000000004' + : '00000000005', + firstName: defaultUser.firstName, + lastName: defaultUser.lastName, + role: defaultUser.role, + status: 'active', + onboardingCompleted: true, + mustChangePassword: false, + }) + .returning(); + + console.log(`[SUCCESS] Seeded default user: ${defaultUser.email}`); + userId = inserted.id; + userMap[defaultUser.email] = userId; + } + + if (defaultUser.role === 'professor') { + await db + .insert(professors) + .values({ + userId, + department: 'Metodologias e Técnicas de Computação', + institution: 'UFC', + }) + .onConflictDoNothing(); + } + } + + console.log('[INFO] Seeding research themes...'); + const defaultProfUserId = userMap['professor@anubis.com']; + if (!defaultProfUserId) { + throw new Error('Default professor ID not found.'); + } + + // Get other professors from the DB to use as co-advisors + const allProfs = await db + .select({ id: professors.userId }) + .from(professors) + .where(eq(professors.institution, 'UFC')) + .limit(5); + + const coadvisorIds = allProfs.map(p => p.id).filter(id => id !== defaultProfUserId); + + const mockThemes = [ + { + title: 'Inteligência Artificial na Saúde Pública', + description: + 'Pesquisa voltada ao uso de Redes Neurais Convolucionais e Processamento de Linguagem Natural para automatização de triagem de prontuários médicos e predição de surtos de dengue na região metropolitana de Fortaleza.', + vacancies: 2, + level: 'masters' as const, + references: [ + { name: 'AI in Medicine Handbook', url: 'https://example.com/handbook' }, + { name: 'Machine Learning for Health Spreads', url: 'https://example.com/health-ml' }, + ], + associatedProfessorIds: coadvisorIds.slice(0, 2), + }, + { + title: 'Segurança e Escalabilidade em Blockchains baseadas em Proof of Stake', + description: + 'Estudo analítico e prático de novos algoritmos de consenso para mitigar ataques de suborno de validadores e análise de técnicas de sharding para otimização do throughput transacional.', + vacancies: 1, + level: 'doctoral' as const, + references: [{ name: 'PoS Security Models', url: 'https://example.com/pos-sec' }], + associatedProfessorIds: coadvisorIds.slice(1, 3), + }, + { + title: 'Arquiteturas Serverless e Computação de Borda em IoT Industrial', + description: + 'Este tema visa projetar e validar uma infraestrutura serverless de baixíssima latência para execução de tarefas de detecção de anomalias em sensores de esteiras industriais na borda da rede.', + vacancies: 3, + level: 'masters' as const, + references: [], + associatedProfessorIds: [], + }, + ]; + + for (const mockTheme of mockThemes) { + const [existing] = await db + .select() + .from(researchThemes) + .where(eq(researchThemes.title, mockTheme.title)) + .limit(1); + + if (existing) { + console.log(`[INFO] Research theme "${mockTheme.title}" already exists.`); + continue; + } + + const [inserted] = await db + .insert(researchThemes) + .values({ + professorId: defaultProfUserId, + title: mockTheme.title, + description: mockTheme.description, + vacancies: mockTheme.vacancies, + level: mockTheme.level, + references: mockTheme.references, + }) + .returning(); + + console.log(`[SUCCESS] Seeded research theme: ${mockTheme.title}`); + + if (mockTheme.associatedProfessorIds.length > 0) { + await db + .insert(researchThemeProfessors) + .values( + mockTheme.associatedProfessorIds.map(coId => ({ + researchThemeId: inserted.id, + professorId: coId, + })), + ) + .onConflictDoNothing(); + console.log( + `[SUCCESS] Associated ${mockTheme.associatedProfessorIds.length} co-advisors to: ${mockTheme.title}`, + ); + } + } +} + +async function seedCvCategories(db: NodePgDatabase): Promise { + const openPeriods = await db + .select() + .from(enrollmentPeriods) + .where(eq(enrollmentPeriods.status, 'open')); + + if (openPeriods.length === 0) { + console.log('[INFO] No open enrollment periods found. Nothing to seed CV categories.'); + return; + } + + for (const period of openPeriods) { + const existing = await db + .select({ id: cvScoringCategories.id }) + .from(cvScoringCategories) + .where(eq(cvScoringCategories.enrollmentPeriodId, period.id)) + .limit(1); + + if (existing.length > 0) { + console.log(`[SKIP] Period "${period.name}" already has scoring categories.`); + continue; + } + + const allCategories = [ + { categories: MASTERS_CATEGORIES, level: 'masters' as const }, + { categories: DOCTORAL_CATEGORIES, level: 'doctoral' as const }, + ]; + + let totalSeeded = 0; + for (const { categories, level } of allCategories) { + for (const cat of categories) { + await db.insert(cvScoringCategories).values({ + enrollmentPeriodId: period.id, + name: cat.name, + description: cat.description, + pointsPerItem: cat.pointsPerItem.toString(), + maxPoints: cat.maxPoints.toString(), + level, + sortOrder: cat.sortOrder, + }); + } + totalSeeded += categories.length; + } + + console.log( + `[SUCCESS] Seeded ${totalSeeded} categories for "${period.name}" (masters + doctoral)`, + ); + } +} + +// ========================================== +// Main Execution +// ========================================== +async function main(): Promise { + console.log('[INFO] Starting database seeding process...'); + const pool = createDatabasePool(); + const db = drizzle(pool); + + try { + // 1. Seed Professors (needed for Co-Advisors association) + await seedProfessors(db); + + // 2. Seed Default Users & Themes + await seedDefaultUsersAndThemes(db); + + // 3. Seed CV Categories + await seedCvCategories(db); + + console.log('[SUCCESS] Database seeding completed successfully.'); + } catch (error) { + console.error('[ERROR] Seeding process failed:', error); + process.exit(1); + } finally { + await pool.end(); + console.log('[INFO] Database connection pool closed.'); + } +} + +main().catch(error => { + console.error('[ERROR] Uncaught exception during seeding:', error); + process.exit(1); +}); diff --git a/scripts/seed/seed-cv-categories.ts b/scripts/seed/seed-cv-categories.ts deleted file mode 100644 index 367256c..0000000 --- a/scripts/seed/seed-cv-categories.ts +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env ts-node -/** - * Seeds CV scoring categories for all open enrollment periods. - * Usage: npx ts-node -r tsconfig-paths/register scripts/seed/seed-cv-categories.ts - */ -import 'dotenv/config'; -import { drizzle } from 'drizzle-orm/node-postgres'; -import { eq } from 'drizzle-orm'; -import { Pool } from 'pg'; -import { enrollmentPeriods } from '../../src/database/schema/enrollment-periods'; -import { cvScoringCategories } from '../../src/database/schema/cv-scoring'; - -const pool = new Pool({ - user: process.env.DATABASE_USER, - password: process.env.DATABASE_PASSWORD, - host: process.env.DATABASE_HOST, - port: Number(process.env.DATABASE_PORT), - database: process.env.DATABASE_NAME, -}); - -const db = drizzle(pool); - -const MASTERS_CATEGORIES = [ - { name: 'Artigos publicados em periódicos', description: 'Artigos completos em periódicos indexados (Qualis A1-B2)', pointsPerItem: 1.0, maxPoints: 3.0, sortOrder: 1 }, - { name: 'Artigos publicados em conferências', description: 'Artigos completos em anais de conferências nacionais e internacionais', pointsPerItem: 0.75, maxPoints: 2.25, sortOrder: 2 }, - { name: 'Iniciação científica / TCC', description: 'Participação em projetos de IC ou trabalho de conclusão de curso com orientador', pointsPerItem: 0.5, maxPoints: 1.5, sortOrder: 3 }, - { name: 'Experiência profissional na área', description: 'Atuação profissional comprovada na área de Ciência da Computação', pointsPerItem: 0.5, maxPoints: 1.0, sortOrder: 4 }, - { name: 'Cursos e certificações', description: 'Cursos de extensão, especializações e certificações técnicas relevantes', pointsPerItem: 0.25, maxPoints: 1.0, sortOrder: 5 }, - { name: 'Participação em eventos', description: 'Apresentação de pôsteres, palestras ou workshops em eventos acadêmicos', pointsPerItem: 0.25, maxPoints: 0.75, sortOrder: 6 }, - { name: 'Premiações acadêmicas', description: 'Prêmios, menções honrosas e reconhecimentos acadêmicos', pointsPerItem: 0.5, maxPoints: 1.0, sortOrder: 7 }, -]; - -const DOCTORAL_CATEGORIES = [ - { name: 'Artigos publicados em periódicos', description: 'Artigos completos em periódicos indexados (Qualis A1-B2)', pointsPerItem: 1.5, maxPoints: 6.0, sortOrder: 1 }, - { name: 'Artigos publicados em conferências', description: 'Artigos completos em anais de conferências nacionais e internacionais', pointsPerItem: 1.0, maxPoints: 4.0, sortOrder: 2 }, - { name: 'Dissertação de mestrado', description: 'Nota e relevância da dissertação de mestrado defendida', pointsPerItem: 2.0, maxPoints: 2.0, sortOrder: 3 }, - { name: 'Participação em projetos de pesquisa', description: 'Projetos de pesquisa com financiamento ou vinculação institucional', pointsPerItem: 1.0, maxPoints: 3.0, sortOrder: 4 }, - { name: 'Experiência docente', description: 'Atuação como professor ou monitor em nível superior', pointsPerItem: 0.5, maxPoints: 1.5, sortOrder: 5 }, - { name: 'Patentes e softwares registrados', description: 'Registros de propriedade intelectual na área', pointsPerItem: 1.0, maxPoints: 2.0, sortOrder: 6 }, - { name: 'Premiações acadêmicas', description: 'Prêmios, menções honrosas e reconhecimentos acadêmicos ou profissionais', pointsPerItem: 0.5, maxPoints: 1.5, sortOrder: 7 }, -]; - -async function main() { - const openPeriods = await db - .select() - .from(enrollmentPeriods) - .where(eq(enrollmentPeriods.status, 'open')); - - if (openPeriods.length === 0) { - console.log('[INFO] No open enrollment periods found. Nothing to seed.'); - process.exit(0); - } - - for (const period of openPeriods) { - // Check if categories already exist - const existing = await db - .select({ id: cvScoringCategories.id }) - .from(cvScoringCategories) - .where(eq(cvScoringCategories.enrollmentPeriodId, period.id)) - .limit(1); - - if (existing.length > 0) { - console.log(`[SKIP] Period "${period.name}" already has scoring categories.`); - continue; - } - - const allCategories: { categories: typeof MASTERS_CATEGORIES; level: 'masters' | 'doctoral' }[] = [ - { categories: MASTERS_CATEGORIES, level: 'masters' }, - { categories: DOCTORAL_CATEGORIES, level: 'doctoral' }, - ]; - - let totalSeeded = 0; - for (const { categories, level } of allCategories) { - for (const cat of categories) { - await db.insert(cvScoringCategories).values({ - enrollmentPeriodId: period.id, - name: cat.name, - description: cat.description, - pointsPerItem: cat.pointsPerItem.toString(), - maxPoints: cat.maxPoints.toString(), - level, - sortOrder: cat.sortOrder, - }); - } - totalSeeded += categories.length; - } - - console.log(`[SUCCESS] Seeded ${totalSeeded} categories for "${period.name}" (masters + doctoral)`); - } - - await pool.end(); - console.log('[DONE] CV scoring categories seed completed.'); -} - -main().catch((err) => { - console.error('[ERROR]', err); - process.exit(1); -}); diff --git a/scripts/seed/seed-professors.ts b/scripts/seed/seed-professors.ts deleted file mode 100644 index 6aa9a1b..0000000 --- a/scripts/seed/seed-professors.ts +++ /dev/null @@ -1,192 +0,0 @@ -/* eslint-disable no-console */ -import { drizzle, type NodePgDatabase } from 'drizzle-orm/node-postgres'; -import { Pool } from 'pg'; -import { professors } from '../../src/database/schema/professor'; -import { users } from '../../src/database/schema/users'; - -interface RawMockProfessor { - nome: string; - cpf: string; - email: string; - institution: string; - department: string; - status: 'active' | 'inactive' | 'disabled'; -} - -interface GeneratedProfessorData { - email: string; - cpf: string; - firstName: string; - lastName: string | null; - institution: string; - department: string; - status: 'active' | 'inactive' | 'disabled'; -} - -const MOCK_PROFESSOR_BASE: RawMockProfessor[] = [ - { - nome: 'Dr. Ricardo Almeida', - cpf: '12345678909', - email: 'r.almeida@ufc.br', - institution: 'UFC', - department: 'Inteligencia Artificial', - status: 'active', - }, - { - nome: 'Dra. Ana Souza', - cpf: '39053344705', - email: 'ana.souza@mdcc.ufc.br', - institution: 'UFC', - department: 'Engenharia de Software', - status: 'inactive', - }, - { - nome: 'Dr. Carlos Mendes', - cpf: '11144477735', - email: 'c.mendes@ufc.br', - institution: 'UFC', - department: 'Sistemas Distribuidos', - status: 'active', - }, - { - nome: 'Dr. João Silveira', - cpf: '98765432100', - email: 'j.silveira@ufc.br', - institution: 'UFC', - department: 'Redes de Computadores', - status: 'inactive', - }, -]; - -/** - * Generates deterministic mock professor payloads scaled to a target size. - * - * @param count - Total number of records to generate. - * @returns Array of structured professor inputs. - * - * @example - * const records = generateMockProfessors(14); - */ -function generateMockProfessors(count: number): GeneratedProfessorData[] { - return Array.from({ length: count }, (_, index) => { - const base = MOCK_PROFESSOR_BASE[index % MOCK_PROFESSOR_BASE.length]; - const cycle = Math.floor(index / MOCK_PROFESSOR_BASE.length) + 1; - - const emailParts = base.email.split('@'); - if (emailParts.length !== 2) { - throw new Error(`Malformed base email template encountered: ${base.email}`); - } - - const email = cycle === 1 ? base.email : `${emailParts[0]}+${cycle}@${emailParts[1]}`; - const splitNome = (cycle === 1 ? base.nome : `${base.nome} ${cycle}`).split(' '); - - const firstName = `${splitNome[0]} ${splitNome[1]}`; - const lastName = splitNome.slice(2).join(' ') || null; - const cpf = `${base.cpf.slice(0, 9)}${String(cycle).padStart(2, '0')}`.slice(0, 11); - - return { - email, - cpf, - firstName, - lastName, - institution: base.institution, - department: base.department, - status: base.status, - }; - }); -} - -/** - * Initializes the Postgres database connection pool based on environment configurations. - */ -function createDatabasePool(): Pool { - return new Pool({ - host: process.env.DATABASE_HOST || 'localhost', - port: Number(process.env.DATABASE_PORT) || 5432, - user: process.env.DATABASE_USER || 'postgres', - password: process.env.DATABASE_PASSWORD || 'postgres', - database: process.env.DATABASE_NAME || 'anubis', - ssl: process.env.DATABASE_SSL === 'true', - }); -} - -/** - * Inserts a single professor and their corresponding user record safely. - */ -async function seedProfessor( - tx: Parameters[0]>[0], - prof: GeneratedProfessorData, -): Promise { - const rows = await tx - .insert(users) - .values({ - authProvider: 'email', - providerSubject: prof.email, - email: prof.email, - password: '$2a$12$lr9fx486D2ZJT1rmHu4xtOUOxRuapGfZwmdDhVKNzpBCNlHNXwvc.', // bcrypt hash for senha123 - cpf: prof.cpf, - firstName: prof.firstName, - lastName: prof.lastName, - role: 'professor', - status: prof.status, - onboardingCompleted: true, - mustChangePassword: true, - }) - .onConflictDoNothing({ target: users.email }) - .returning(); - - const userRow = Array.isArray(rows) ? rows[0] : null; - - if (!userRow) { - console.warn( - `[WARN] Skipping profile insertion: User conflict or omission occurred for email: ${prof.email}`, - ); - return; - } - - await tx - .insert(professors) - .values({ - userId: userRow.id, - department: prof.department, - institution: prof.institution, - }) - .onConflictDoNothing({ target: professors.userId }); -} - -/** - * Executes the database seed execution for application professors. - */ -async function seed(): Promise { - const pool = createDatabasePool(); - const db = drizzle(pool); - - try { - const generatedProfessors = generateMockProfessors(14); - console.log(`[INFO] Starting insertion of ${generatedProfessors.length} mock professors.`); - - await db.transaction(async tx => { - for (const prof of generatedProfessors) { - await seedProfessor(tx, prof); - } - }); - - console.log('[SUCCESS] Professors seed completed successfully.'); - } catch (error) { - console.error( - '[ERROR] Critical database seeding execution failure:', - error instanceof Error ? error.message : error, - ); - throw error; - } finally { - await pool.end(); - } -} - -seed().catch(error => { - console.error( - '[ERROR] Process terminated prematurely due to unhandled seeding exception:', - error, - ); - process.exit(1); -}); diff --git a/scripts/seed/seed-themes.ts b/scripts/seed/seed-themes.ts deleted file mode 100644 index 59f7df0..0000000 --- a/scripts/seed/seed-themes.ts +++ /dev/null @@ -1,229 +0,0 @@ -/* eslint-disable no-console */ -import { Pool } from 'pg'; -import { drizzle, type NodePgDatabase } from 'drizzle-orm/node-postgres'; -import { eq, and } from 'drizzle-orm'; -import * as bcrypt from 'bcrypt'; -import { users } from '../../src/database/schema/users'; -import { professors } from '../../src/database/schema/professor'; -import { researchThemes, researchThemeProfessors } from '../../src/database/schema/research-themes'; - -interface DefaultUserData { - email: string; - role: 'candidate' | 'professor' | 'mdcc-secretary' | 'post-graduate-coordinator' | 'post-graduate-vice-coordinator'; - firstName: string; - lastName: string; -} - -const DEFAULT_USERS: DefaultUserData[] = [ - { - email: 'candidate@anubis.com', - role: 'candidate', - firstName: 'Candidato', - lastName: 'Padrão', - }, - { - email: 'professor@anubis.com', - role: 'professor', - firstName: 'Professor', - lastName: 'Padrão', - }, - { - email: 'secretary@anubis.com', - role: 'mdcc-secretary', - firstName: 'Secretário', - lastName: 'Padrão', - }, - { - email: 'coordinator@anubis.com', - role: 'post-graduate-coordinator', - firstName: 'Coordenador', - lastName: 'Padrão', - }, - { - email: 'vice@anubis.com', - role: 'post-graduate-vice-coordinator', - firstName: 'Vice', - lastName: 'Coordenador Padrão', - }, -]; - -function createDatabasePool(): Pool { - return new Pool({ - host: process.env.DATABASE_HOST || 'localhost', - port: Number(process.env.DATABASE_PORT) || 5432, - user: process.env.DATABASE_USER || 'postgres', - password: process.env.DATABASE_PASSWORD || 'postgres', - database: process.env.DATABASE_NAME || 'anubis', - ssl: process.env.DATABASE_SSL === 'true', - }); -} - -async function seedDefaultUsers(db: NodePgDatabase, passwordHash: string): Promise> { - console.log('[INFO] Seeding default users...'); - const userMap: Record = {}; - - for (const defaultUser of DEFAULT_USERS) { - // Check if user already exists - const [existing] = await db - .select() - .from(users) - .where(eq(users.email, defaultUser.email)) - .limit(1); - - if (existing) { - console.log(`[INFO] User ${defaultUser.email} already exists.`); - userMap[defaultUser.email] = existing.id; - continue; - } - - // Insert user - const [inserted] = await db - .insert(users) - .values({ - authProvider: 'email', - providerSubject: defaultUser.email, - email: defaultUser.email, - password: passwordHash, - cpf: defaultUser.email.startsWith('candidate') ? '00000000001' : defaultUser.email.startsWith('professor') ? '00000000002' : defaultUser.email.startsWith('secretary') ? '00000000003' : defaultUser.email.startsWith('coordinator') ? '00000000004' : '00000000005', - firstName: defaultUser.firstName, - lastName: defaultUser.lastName, - role: defaultUser.role, - status: 'active', - onboardingCompleted: true, - mustChangePassword: false, - }) - .returning(); - - console.log(`[SUCCESS] Seeded default user: ${defaultUser.email}`); - userMap[defaultUser.email] = inserted.id; - - // If professor role, seed the professor details table - if (defaultUser.role === 'professor') { - await db - .insert(professors) - .values({ - userId: inserted.id, - department: 'Metodologias e Técnicas de Computação', - institution: 'UFC', - }) - .onConflictDoNothing(); - } - } - - return userMap; -} - -async function seedResearchThemes(db: NodePgDatabase, userMap: Record): Promise { - console.log('[INFO] Seeding research themes...'); - const professorId = userMap['professor@anubis.com']; - if (!professorId) { - throw new Error('Default professor ID not found.'); - } - - // Get other professors from the DB if they exist to use as co-advisors - const allProfs = await db - .select({ id: professors.userId }) - .from(professors) - .where(eq(professors.institution, 'UFC')) - .limit(5); - - const coadvisorIds = allProfs.map(p => p.id).filter(id => id !== professorId); - - const mockThemes = [ - { - title: 'Inteligência Artificial na Saúde Pública', - description: 'Pesquisa voltada ao uso de Redes Neurais Convolucionais e Processamento de Linguagem Natural para automatização de triagem de prontuários médicos e predição de surtos de dengue na região metropolitana de Fortaleza.', - vacancies: 2, - level: 'masters' as const, - references: [ - { name: 'AI in Medicine Handbook', url: 'https://example.com/handbook' }, - { name: 'Machine Learning for Health Spreads', url: 'https://example.com/health-ml' }, - ], - associatedProfessorIds: coadvisorIds.slice(0, 2), - }, - { - title: 'Segurança e Escalabilidade em Blockchains baseadas em Proof of Stake', - description: 'Estudo analítico e prático de novos algoritmos de consenso para mitigar ataques de suborno de validadores e análise de técnicas de sharding para otimização do throughput transacional.', - vacancies: 1, - level: 'doctoral' as const, - references: [ - { name: 'PoS Security Models', url: 'https://example.com/pos-sec' }, - ], - associatedProfessorIds: coadvisorIds.slice(1, 3), - }, - { - title: 'Arquiteturas Serverless e Computação de Borda em IoT Industrial', - description: 'Este tema visa projetar e validar uma infraestrutura serverless de baixíssima latência para execução de tarefas de detecção de anomalias em sensores de esteiras industriais na borda da rede.', - vacancies: 3, - level: 'masters' as const, - references: [], - associatedProfessorIds: [], - }, - ]; - - for (const mockTheme of mockThemes) { - // Check if theme with same title already exists - const [existing] = await db - .select() - .from(researchThemes) - .where(eq(researchThemes.title, mockTheme.title)) - .limit(1); - - if (existing) { - console.log(`[INFO] Research theme "${mockTheme.title}" already exists.`); - continue; - } - - // Insert theme - const [inserted] = await db - .insert(researchThemes) - .values({ - professorId, - title: mockTheme.title, - description: mockTheme.description, - vacancies: mockTheme.vacancies, - level: mockTheme.level, - references: mockTheme.references, - }) - .returning(); - - console.log(`[SUCCESS] Seeded research theme: ${mockTheme.title}`); - - // Insert associations - if (mockTheme.associatedProfessorIds.length > 0) { - await db - .insert(researchThemeProfessors) - .values( - mockTheme.associatedProfessorIds.map(coId => ({ - researchThemeId: inserted.id, - professorId: coId, - })), - ) - .onConflictDoNothing(); - console.log(`[SUCCESS] Associated ${mockTheme.associatedProfessorIds.length} co-advisors to: ${mockTheme.title}`); - } - } -} - -async function main(): Promise { - const pool = createDatabasePool(); - const db = drizzle(pool); - - try { - console.log('[INFO] Seeding database...'); - const hash = await bcrypt.hash('senha123', 10); - const userMap = await seedDefaultUsers(db, hash); - await seedResearchThemes(db, userMap); - console.log('[SUCCESS] Database seeding process completed successfully.'); - } catch (error) { - console.error('[ERROR] Seeding process failed:', error); - process.exit(1); - } finally { - await pool.end(); - } -} - -main().catch(error => { - console.error('[ERROR] Uncaught exception during seed:', error); - process.exit(1); -}); From ecdb73e218a3832b250bc020f5f1032f0e468dc8 Mon Sep 17 00:00:00 2001 From: Said Rodrigues Date: Thu, 4 Jun 2026 10:06:41 -0300 Subject: [PATCH 5/6] chore: use a better console for minio management --- docker-compose.yaml | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 989873b..ec3841d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -6,9 +6,9 @@ services: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres ports: - - "5432:5432" + - '5432:5432' healthcheck: - test: ["CMD-SHELL", "pg_isready -h localhost -U $$POSTGRES_USER"] + test: ['CMD-SHELL', 'pg_isready -h localhost -U $$POSTGRES_USER'] interval: 10s timeout: 5s retries: 5 @@ -22,7 +22,7 @@ services: context: . dockerfile: Dockerfile ports: - - "3000:3000" + - '3000:3000' image: ghcr.io/taldoflemis/anubis:latest env_file: - .env @@ -80,9 +80,9 @@ services: healthcheck: test: [ - "CMD", - "node", - "-e", + 'CMD', + 'node', + '-e', "fetch('http://localhost:3000/api/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))", ] interval: 15s @@ -93,13 +93,13 @@ services: mailpit: image: axllent/mailpit:latest ports: - - "1025:1025" - - "8025:8025" + - '1025:1025' + - '8025:8025' environment: - MP_SMTP_AUTH_ACCEPT_ANY=true - MP_SMTP_AUTH_ALLOW_INSECURE=true healthcheck: - test: ["CMD-SHELL", "wget http://localhost:8025 -O /dev/null"] + test: ['CMD-SHELL', 'wget http://localhost:8025 -O /dev/null'] interval: 10s timeout: 5s retries: 5 @@ -110,7 +110,7 @@ services: context: ./frontend/ image: ghcr.io/taldoflemis/anubis/frontend:latest ports: - - "5173:8080" + - '5173:8080' env_file: - ./frontend/.env @@ -134,22 +134,31 @@ services: minio: image: minio/minio:latest - command: server /data --console-address ":9001" + command: server /data ports: - - "9000:9000" - - "9001:9001" + - '9000:9000' environment: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: minioadmin volumes: - minio_data:/data healthcheck: - test: ["CMD", "mc", "ready", "local"] + test: ['CMD', 'mc', 'ready', 'local'] interval: 10s timeout: 5s retries: 5 start_period: 10s + minio_console: + image: ghcr.io/georgmangold/console + ports: + - '9001:9090' + environment: + CONSOLE_MINIO_SERVER: 'http://minio:9000' + depends_on: + createbuckets: + condition: service_completed_successfully + createbuckets: image: minio/mc:latest depends_on: From 43fdd9b49e04c62a834f30d57ba0b24a482b77d2 Mon Sep 17 00:00:00 2001 From: Said Rodrigues Date: Thu, 4 Jun 2026 10:32:20 -0300 Subject: [PATCH 6/6] feat: add user switcher --- frontend/src/components/dev/user-switcher.tsx | 214 ++++++++++++++++++ frontend/src/routes/__root.tsx | 2 + 2 files changed, 216 insertions(+) create mode 100644 frontend/src/components/dev/user-switcher.tsx diff --git a/frontend/src/components/dev/user-switcher.tsx b/frontend/src/components/dev/user-switcher.tsx new file mode 100644 index 0000000..5d3fabe --- /dev/null +++ b/frontend/src/components/dev/user-switcher.tsx @@ -0,0 +1,214 @@ +import { authQueryOptions } from '@/hooks/use-auth'; +import { api } from '@/lib/api'; +import { AUTH_SIGN_IN_ROUTE } from '@/lib/auth-flow'; +import { useQueryClient } from '@tanstack/react-query'; +import { useNavigate } from '@tanstack/react-router'; +import { LogOut, User } from 'lucide-react'; +import { useEffect, useRef, useState } from 'react'; + +const DEV_USERS = [ + { email: 'candidate@anubis.com', role: 'candidate', label: 'Candidato' }, + { email: 'professor@anubis.com', role: 'professor', label: 'Professor' }, + { email: 'secretary@anubis.com', role: 'mdcc-secretary', label: 'Secretário' }, + { email: 'coordinator@anubis.com', role: 'post-graduate-coordinator', label: 'Coordenador' }, + { email: 'vice@anubis.com', role: 'post-graduate-vice-coordinator', label: 'Vice-Coord.' }, +] as const; + +const PASSWORD = 'senha123'; + +type Position = + | 'bottom-center' + | 'bottom-left' + | 'bottom-right' + | 'top-center' + | 'top-left' + | 'top-right'; + +const POSITION_CLASSES: Record = { + 'bottom-center': 'bottom-4 left-1/2 -translate-x-1/2', + 'bottom-left': 'bottom-4 left-4', + 'bottom-right': 'bottom-4 right-4', + 'top-center': 'top-4 left-1/2 -translate-x-1/2', + 'top-left': 'top-4 left-4', + 'top-right': 'top-4 right-4', +}; + +const ROLE_DOT: Record = { + candidate: 'bg-blue-500', + professor: 'bg-emerald-500', + 'mdcc-secretary': 'bg-yellow-500', + 'post-graduate-coordinator': 'bg-purple-500', + 'post-graduate-vice-coordinator': 'bg-pink-500', +}; + +interface CurrentUser { + email: string | null; + firstName: string | null; + role: string; +} + +interface DevUserSwitcherProps { + position?: Position; +} + +export function DevUserSwitcher({ position = 'bottom-center' }: DevUserSwitcherProps) { + if (!import.meta.env.DEV) return null; + return ; +} + +function Inner({ position }: { position: Position }) { + const [open, setOpen] = useState(false); + const [switching, setSwitching] = useState(null); + const queryClient = useQueryClient(); + const navigate = useNavigate(); + const panelRef = useRef(null); + + const currentUser = queryClient.getQueryData(authQueryOptions.queryKey); + const dotColor = currentUser ? (ROLE_DOT[currentUser.role] ?? 'bg-zinc-500') : null; + + useEffect(() => { + if (!open) return; + function onMouseDown(e: MouseEvent) { + if (panelRef.current && !panelRef.current.contains(e.target as Node)) { + setOpen(false); + } + } + document.addEventListener('mousedown', onMouseDown); + return () => document.removeEventListener('mousedown', onMouseDown); + }, [open]); + + async function switchTo(email: string) { + if (switching) return; + setSwitching(email); + try { + await api.auth.logout().catch(() => {}); + await queryClient.cancelQueries(); + queryClient.clear(); + await api.auth.emailLogin({ email, password: PASSWORD }); + await queryClient.fetchQuery({ ...authQueryOptions, staleTime: 0 }); + navigate({ to: '/' }); + setOpen(false); + } catch (err) { + console.error('[DevUserSwitcher] switch failed:', err); + } finally { + setSwitching(null); + } + } + + async function logout() { + if (switching) return; + setSwitching('__logout__'); + try { + await api.auth.logout().catch(() => {}); + await queryClient.cancelQueries(); + queryClient.clear(); + navigate({ to: AUTH_SIGN_IN_ROUTE }); + setOpen(false); + } finally { + setSwitching(null); + } + } + + const posClass = POSITION_CLASSES[position]; + + if (!open) { + return ( +
    + +
    + ); + } + + return ( +
    +
    +
    + + dev · user switcher + + +
    + + {currentUser && ( +
    +

    logado como

    +
    + + {currentUser.email ?? '—'} +
    +

    {currentUser.role}

    +
    + )} + +
    + {DEV_USERS.map(u => { + const isActive = currentUser?.email === u.email; + const isLoading = switching === u.email; + return ( + + ); + })} + + {currentUser && ( + <> +
    + + + )} +
    +
    +
    + ); +} diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx index 5cfa818..465c1dd 100644 --- a/frontend/src/routes/__root.tsx +++ b/frontend/src/routes/__root.tsx @@ -1,3 +1,4 @@ +import { DevUserSwitcher } from '@/components/dev/user-switcher'; import type { QueryClient } from '@tanstack/react-query'; import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'; import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'; @@ -20,6 +21,7 @@ function RootComponent() { <> + ); }