From 475e0754df96c2fa712928c14ca827bb9099624f Mon Sep 17 00:00:00 2001 From: Sergio Ramirez Date: Mon, 16 Jun 2025 12:02:22 -0400 Subject: [PATCH] base con autenticacion, registro, modulo encuestas --- .commitlintrc.ts | 1 + .gitignore | 39 + .husky/commit-msg | 4 + .lintstagedrc | 3 + .npmrc | 0 .prettierignore | 7 + .prettierrc | 11 + LICENSE | 21 + README.md | 88 ++ apps/api/.env.example | 19 + apps/api/.gitignore | 56 + apps/api/.swcrc | 13 + apps/api/README.md | 1 + apps/api/eslint.config.mjs | 4 + apps/api/nest-cli.json | 10 + apps/api/package.json | 87 ++ apps/api/src/app.module.ts | 64 + apps/api/src/bootstrap.ts | 35 + apps/api/src/common/config/envs.ts | 58 + apps/api/src/common/constants/index.ts | 1 + apps/api/src/common/constants/role.ts | 6 + apps/api/src/common/decorators/index.ts | 3 + .../decorators/permissions.decorator.ts | 5 + .../src/common/decorators/public.decorator.ts | 4 + .../require-permissions.decorator.ts | 4 + .../src/common/decorators/roles.decorator.ts | 5 + .../src/common/decorators/user.decorator.ts | 8 + apps/api/src/common/dto/pagination.dto.ts | 35 + apps/api/src/common/guards/index.ts | 2 + apps/api/src/common/guards/jwt-auth.guard.ts | 81 + .../src/common/guards/jwt-refresh.guard.ts | 49 + apps/api/src/common/guards/roles.guard.ts | 47 + apps/api/src/common/interceptors/index.ts | 1 + .../interceptors/req-log.interceptor.ts | 36 + apps/api/src/common/middlewares/index.ts | 1 + .../common/middlewares/logger.middleware.ts | 15 + apps/api/src/common/modules/index.ts | 3 + apps/api/src/common/modules/logger.module.ts | 24 + .../src/common/modules/node-mailer.module.ts | 23 + .../api/src/common/modules/throttle.module.ts | 28 + .../common/pipes/file-size-validator.pipe.ts | 38 + .../common/pipes/file-type-validator.pipe.ts | 38 + apps/api/src/common/pipes/index.ts | 3 + .../src/common/pipes/zod-validator.pipe.ts | 17 + apps/api/src/common/utils/bcrypt.ts | 16 + apps/api/src/common/utils/dateTimeUtility.ts | 28 + apps/api/src/common/utils/index.ts | 16 + apps/api/src/common/utils/validateEnv.ts | 36 + apps/api/src/database/drizzle-provider.ts | 22 + apps/api/src/database/drizzle.module.ts | 10 + apps/api/src/database/index.ts | 5 + .../0000_abnormal_lethal_legion.sql | 173 +++ .../migrations/0001_massive_kylun.sql | 9 + .../migrations/meta/0000_snapshot.json | 1306 ++++++++++++++++ .../migrations/meta/0001_snapshot.json | 1358 +++++++++++++++++ .../database/migrations/meta/_journal.json | 20 + apps/api/src/database/schema/activity_logs.ts | 32 + apps/api/src/database/schema/auth.ts | 132 ++ apps/api/src/database/schema/enum.ts | 8 + apps/api/src/database/schema/general.ts | 114 ++ apps/api/src/database/schema/schemas.ts | 4 + apps/api/src/database/schema/surveys.ts | 57 + .../api/src/database/seeds/admin-role.seed.ts | 24 + apps/api/src/database/seeds/index.ts | 37 + .../src/database/seeds/municipalities.seed.ts | 25 + apps/api/src/database/seeds/parishes.seed.ts | 25 + apps/api/src/database/seeds/states.seed.ts | 24 + .../api/src/database/seeds/user-admin.seed.ts | 39 + apps/api/src/database/timestamps.ts | 8 + apps/api/src/drizzle.config.ts | 13 + apps/api/src/features/auth/auth.controller.ts | 61 + apps/api/src/features/auth/auth.module.ts | 12 + apps/api/src/features/auth/auth.service.ts | 368 +++++ .../features/auth/dto/change-password.dto.ts | 22 + .../features/auth/dto/confirm-email.dto.ts | 14 + .../src/features/auth/dto/create-user.dto.ts | 29 + .../features/auth/dto/forgot-password.dto.ts | 10 + .../features/auth/dto/refresh-token.dto.ts | 14 + .../features/auth/dto/reset-password.dto.ts | 22 + .../src/features/auth/dto/signIn-user.dto.ts | 16 + .../src/features/auth/dto/signOut-user.dto.ts | 10 + .../src/features/auth/dto/signUp-user.dto.ts | 45 + .../auth/dto/update-refresh-token.dto.ts | 6 + .../features/auth/dto/validate-user.dto.ts | 16 + .../auth/interfaces/auth-tokens.interface.ts | 6 + .../auth/interfaces/login-user.interface.ts | 25 + .../interfaces/refresh-token.interface.ts | 8 + .../auth/interfaces/session.interface.ts | 5 + .../category-types.controller.ts | 96 ++ .../category-types/category-types.module.ts | 12 + .../category-types/category-types.service.ts | 81 + .../dto/create-category-type.dto.ts | 21 + .../dto/update-category-type.dto.ts | 4 + .../entities/category-type.entity.ts | 33 + .../configurations/configurations.module.ts | 15 + .../dto/create-municipality.dto.ts | 20 + .../dto/update-municipality.dto.ts | 4 + .../entities/municipality.entity.ts | 39 + .../municipalities.controller.ts | 97 ++ .../municipalities/municipalities.module.ts | 13 + .../municipalities/municipalities.service.ts | 120 ++ .../parishes/dto/create-parish.dto.ts | 20 + .../parishes/dto/update-parish.dto.ts | 4 + .../parishes/entities/parish.entity.ts | 39 + .../parishes/parishes.controller.ts | 99 ++ .../parishes/parishes.module.ts | 13 + .../parishes/parishes.service.ts | 115 ++ .../states/dto/create-state.dto.ts | 12 + .../states/dto/update-state.dto.ts | 4 + .../states/entities/state.entity.ts | 27 + .../states/states.controller.ts | 68 + .../configurations/states/states.module.ts | 12 + .../configurations/states/states.service.ts | 67 + .../features/location/entities/user.entity.ts | 16 + .../features/location/location.controller.ts | 41 + .../src/features/location/location.module.ts | 11 + .../src/features/location/location.service.ts | 44 + apps/api/src/features/mail/mail.module.ts | 11 + .../src/features/mail/mail.service.spec.ts | 18 + apps/api/src/features/mail/mail.service.ts | 18 + .../mail/templates/change-password.mail.ts | 177 +++ .../mail/templates/confirm-email.mail.ts | 177 +++ .../mail/templates/forgot-password.mail.ts | 183 +++ apps/api/src/features/mail/templates/index.ts | 195 +++ .../features/mail/templates/register.mail.ts | 183 +++ .../features/mail/templates/sign-in.mail.ts | 177 +++ .../src/features/roles/dto/create-role.dto.ts | 9 + .../src/features/roles/dto/update-role.dto.ts | 4 + .../features/roles/entities/role.entity.ts | 6 + .../src/features/roles/roles.controller.ts | 59 + apps/api/src/features/roles/roles.module.ts | 12 + apps/api/src/features/roles/roles.service.ts | 67 + apps/api/src/features/surveys/Untitled-1.json | 76 + .../features/surveys/dto/create-survey.dto.ts | 73 + .../features/surveys/dto/find-for-user.dto.ts | 10 + .../surveys/dto/response-survey.dto.ts | 15 + .../surveys/dto/statistics-response.dto.ts | 63 + .../features/surveys/dto/update-survey.dto.ts | 5 + .../surveys/entities/survey.entity.ts | 30 + .../features/surveys/surveys.controller.ts | 125 ++ .../src/features/surveys/surveys.module.ts | 10 + .../src/features/surveys/surveys.service.ts | 535 +++++++ .../user-roles/dto/assign-role.dto.ts | 12 + .../user-roles/user-roles.controller.ts | 46 + .../features/user-roles/user-roles.module.ts | 12 + .../features/user-roles/user-roles.service.ts | 118 ++ .../src/features/users/dto/create-user.dto.ts | 33 + .../src/features/users/dto/update-user.dto.ts | 40 + .../features/users/entities/user.entity.ts | 25 + .../src/features/users/users.controller.ts | 81 + apps/api/src/features/users/users.module.ts | 11 + apps/api/src/features/users/users.service.ts | 288 ++++ apps/api/src/main.ts | 16 + apps/api/src/swagger.ts | 11 + apps/api/test/app.e2e-spec.ts | 24 + apps/api/test/jest-e2e.json | 9 + apps/api/tsconfig.build.json | 4 + apps/api/tsconfig.json | 13 + apps/api/uploads/.gitkeep | 0 apps/web/.env_template | 4 + apps/web/.gitignore | 36 + apps/web/README.md | 49 + apps/web/app/(auth)/page.tsx | 14 + apps/web/app/api/auth/[...nextauth]/route.ts | 2 + .../administracion/encuestas/crear/page.tsx | 13 + .../encuestas/editar/[id]/page.tsx | 13 + .../administracion/encuestas/page.tsx | 37 + .../dashboard/administracion/usuario/page.tsx | 37 + .../configuraciones/caja-ahorro/page.tsx | 19 + .../encuestas/[id]/responder/page.tsx | 24 + apps/web/app/dashboard/encuestas/page.tsx | 21 + .../dashboard/estadisticas/encuestas/page.tsx | 19 + apps/web/app/dashboard/inicio/page.tsx | 11 + apps/web/app/dashboard/layout.tsx | 31 + apps/web/app/dashboard/page.tsx | 12 + apps/web/app/dashboard/profile/page.tsx | 17 + apps/web/app/error.tsx | 37 + apps/web/app/fonts/GeistMonoVF.woff | Bin 0 -> 67864 bytes apps/web/app/fonts/GeistVF.woff | Bin 0 -> 66268 bytes apps/web/app/layout.tsx | 64 + apps/web/app/not-found.tsx | 29 + apps/web/app/og/mono.ttf | Bin 0 -> 277092 bytes apps/web/app/og/route.tsx | 62 + apps/web/app/opengraph-image.tsx | 48 + apps/web/app/register/page.tsx | 13 + apps/web/components.json | 20 + apps/web/components/breadcrumbs.tsx | 42 + apps/web/components/icons.tsx | 92 ++ .../layout/ThemeToggle/theme-provider.tsx | 13 + .../layout/ThemeToggle/theme-toggle.tsx | 37 + apps/web/components/layout/app-sidebar.tsx | 58 + apps/web/components/layout/header.tsx | 25 + apps/web/components/layout/page-container.tsx | 22 + apps/web/components/layout/providers.tsx | 48 + apps/web/components/layout/user-nav.tsx | 52 + apps/web/components/modal/alert-modal.tsx | 50 + apps/web/components/nav-main.tsx | 113 ++ apps/web/components/nav-projects.tsx | 111 ++ apps/web/components/team-switcher.tsx | 117 ++ apps/web/constants/data.ts | 74 + apps/web/eslint.config.js | 4 + .../feactures/auth/actions/login-action.ts | 17 + .../auth/actions/refresh-token-action.ts | 20 + apps/web/feactures/auth/actions/register.ts | 27 + .../feactures/auth/components/sigin-view.tsx | 35 + .../feactures/auth/components/signup-view.tsx | 32 + .../auth/components/user-auth-form.tsx | 139 ++ .../auth/components/user-register-form.tsx | 321 ++++ .../auth/hooks/use-mutation-users.ts | 14 + apps/web/feactures/auth/schemas/login.ts | 46 + .../feactures/auth/schemas/refreshToken.ts | 14 + apps/web/feactures/auth/schemas/register.ts | 35 + .../web/feactures/location/actions/actions.ts | 36 + .../location/hooks/use-query-location.ts | 16 + apps/web/feactures/location/schemas/users.ts | 94 ++ .../actions/surveys-statistics-actions.ts | 27 + .../statistics/components/survey-details.tsx | 127 ++ .../statistics/components/survey-overview.tsx | 82 + .../components/survey-responses.tsx | 78 + .../components/survey-statistics.tsx | 35 + .../statistics/hooks/use-query-statistics.ts | 8 + .../statistics/schemas/statistics-schema.ts | 59 + .../statistics/schemas/statistics.ts | 35 + .../surveys/actions/surveys-actions.ts | 216 +++ .../admin/question-config-modal.tsx | 245 +++ .../components/admin/question-toolbox.tsx | 88 ++ .../components/admin/survey-builder.tsx | 454 ++++++ .../components/admin/surveys-admin-list.tsx | 42 + .../components/admin/surveys-header.tsx | 24 + .../admin/surveys-tables/cell-action.tsx | 88 ++ .../admin/surveys-tables/columns.tsx | 29 + .../surveys-tables/survey-table-action.tsx | 36 + .../use-survey-table-filters.tsx | 59 + .../surveys/components/survey-list.tsx | 86 ++ .../surveys/components/survey-response.tsx | 252 +++ .../feactures/surveys/components/survey.tsx | 28 + .../surveys/hooks/use-mutation-surveys.ts | 47 + .../surveys/hooks/use-query-surveys.ts | 20 + apps/web/feactures/surveys/schemas/survey.ts | 212 +++ .../surveys/schemas/surveys-options.ts | 6 + .../web/feactures/surveys/utils/date-utils.ts | 11 + .../feactures/surveys/utils/searchparams.ts | 16 + apps/web/feactures/users/actions/actions.ts | 148 ++ .../components/admin/create-user-form.tsx | 222 +++ .../admin/surveys-tables/cell-action.tsx | 90 ++ .../admin/surveys-tables/columns.tsx | 37 + .../use-survey-table-filters.tsx | 59 + .../surveys-tables/users-table-action.tsx | 36 + .../components/admin/update-user-form.tsx | 227 +++ .../users/components/admin/user-modal.tsx | 69 + .../components/admin/users-admin-list.tsx | 44 + .../users/components/admin/users-header.tsx | 27 + .../users/components/modal-profile.tsx | 57 + .../feactures/users/components/selectList.tsx | 85 ++ .../web/feactures/users/components/survey.tsx | 28 + .../users/components/update-user-form.tsx | 268 ++++ .../users/components/user-profile.tsx | 75 + .../users/hooks/use-mutation-users.ts | 45 + .../users/hooks/use-query-surveys.ts | 12 + .../feactures/users/hooks/use-query-users.ts | 12 + .../users/schemas/account-plan-options.ts | 19 + .../users/schemas/account-plan.schema.ts | 83 + .../users/schemas/surveys-options.ts | 6 + apps/web/feactures/users/schemas/users.ts | 67 + apps/web/feactures/users/utils/date-utils.ts | 11 + .../web/feactures/users/utils/searchparams.ts | 16 + apps/web/hooks/use-breadcrumbs.tsx | 46 + apps/web/hooks/use-mobile.ts | 19 + apps/web/hooks/use-safe-query.ts | 13 + apps/web/lib/auth.config.ts | 120 ++ apps/web/lib/auth.ts | 17 + apps/web/lib/buildSearchParams.ts | 15 + apps/web/lib/env.ts | 12 + apps/web/lib/fetch.api.ts | 98 ++ apps/web/lib/formData.ts | 37 + apps/web/lib/index.ts | 5 + apps/web/lib/safeAction.ts | 3 + apps/web/middleware.ts | 42 + apps/web/next.config.js | 4 + apps/web/package.json | 57 + apps/web/postcss.config.mjs | 1 + apps/web/public/icon-2.png | Bin 0 -> 6206 bytes apps/web/public/icon.png | Bin 0 -> 6366 bytes apps/web/public/logo.png | Bin 0 -> 219713 bytes apps/web/public/og-bg.png | Bin 0 -> 8505 bytes apps/web/server/home.server.ts | 15 + apps/web/tsconfig.json | 21 + apps/web/types/index.ts | 36 + apps/web/types/next-auth.d.ts | 52 + apps/web/types/user.type.ts | 18 + assets/lifecycle.png | Bin 0 -> 135801 bytes assets/preview.png | Bin 0 -> 19660 bytes package.json | 49 + packages/eslint-config/README.md | 3 + packages/eslint-config/base.js | 32 + packages/eslint-config/nest.js | 43 + packages/eslint-config/next.js | 49 + packages/eslint-config/package.json | 28 + packages/eslint-config/react-internal.js | 39 + packages/shadcn/README.md | 1 + packages/shadcn/components.json | 20 + packages/shadcn/package.json | 72 + packages/shadcn/postcss.config.mjs | 8 + packages/shadcn/src/components/icon/index.tsx | 1 + .../shadcn/src/components/ui/accordion.tsx | 66 + .../shadcn/src/components/ui/alert-dialog.tsx | 157 ++ packages/shadcn/src/components/ui/avatar.tsx | 53 + .../src/components/ui/background-box.tsx | 79 + packages/shadcn/src/components/ui/badge.tsx | 46 + .../shadcn/src/components/ui/breadcrumb.tsx | 108 ++ .../shadcn/src/components/ui/button copy.tsx | 58 + packages/shadcn/src/components/ui/button.tsx | 59 + .../shadcn/src/components/ui/calendar.tsx | 75 + packages/shadcn/src/components/ui/card.tsx | 75 + .../shadcn/src/components/ui/checkbox.tsx | 30 + .../shadcn/src/components/ui/collapsible.tsx | 33 + packages/shadcn/src/components/ui/command.tsx | 177 +++ packages/shadcn/src/components/ui/dialog.tsx | 138 ++ .../src/components/ui/dropdown-menu.tsx | 257 ++++ .../src/components/ui/follow-cursor.tsx | 123 ++ packages/shadcn/src/components/ui/form.tsx | 167 ++ .../src/components/ui/grid-background.tsx | 23 + packages/shadcn/src/components/ui/heading.tsx | 13 + .../shadcn/src/components/ui/input copy.tsx | 21 + packages/shadcn/src/components/ui/input.tsx | 19 + packages/shadcn/src/components/ui/label.tsx | 24 + packages/shadcn/src/components/ui/modal.tsx | 42 + .../src/components/ui/mode-switcher.tsx | 69 + packages/shadcn/src/components/ui/popover.tsx | 48 + .../shadcn/src/components/ui/radio-group.tsx | 45 + .../shadcn/src/components/ui/retro-grid.tsx | 32 + .../shadcn/src/components/ui/scroll-area.tsx | 58 + .../src/components/ui/select-searchable.tsx | 86 ++ packages/shadcn/src/components/ui/select.tsx | 185 +++ .../shadcn/src/components/ui/separator.tsx | 28 + packages/shadcn/src/components/ui/sheet.tsx | 139 ++ packages/shadcn/src/components/ui/sidebar.tsx | 724 +++++++++ .../shadcn/src/components/ui/skeleton.tsx | 13 + packages/shadcn/src/components/ui/switch.tsx | 31 + packages/shadcn/src/components/ui/table.tsx | 116 ++ .../ui/table/data-table-filter-box.tsx | 167 ++ .../ui/table/data-table-reset-filter.tsx | 22 + .../components/ui/table/data-table-search.tsx | 64 + .../ui/table/data-table-skeleton.tsx | 95 ++ .../src/components/ui/table/data-table.tsx | 240 +++ packages/shadcn/src/components/ui/tabs.tsx | 66 + .../shadcn/src/components/ui/tag-input.tsx | 251 +++ .../shadcn/src/components/ui/textarea.tsx | 18 + .../src/components/ui/texture-button.tsx | 110 ++ .../src/components/ui/themes-provider.tsx | 9 + .../shadcn/src/components/ui/tooltip copy.tsx | 61 + packages/shadcn/src/components/ui/tooltip.tsx | 61 + .../shadcn/src/components/ui/true-focus.tsx | 154 ++ packages/shadcn/src/hooks/ui/accordion.tsx | 66 + packages/shadcn/src/hooks/ui/avatar.tsx | 53 + .../shadcn/src/hooks/ui/background-box.tsx | 79 + packages/shadcn/src/hooks/ui/badge.tsx | 46 + packages/shadcn/src/hooks/ui/breadcrumb.tsx | 108 ++ packages/shadcn/src/hooks/ui/button copy.tsx | 58 + packages/shadcn/src/hooks/ui/button.tsx | 58 + packages/shadcn/src/hooks/ui/card.tsx | 75 + packages/shadcn/src/hooks/ui/checkbox.tsx | 30 + packages/shadcn/src/hooks/ui/collapsible.tsx | 33 + packages/shadcn/src/hooks/ui/command.tsx | 177 +++ packages/shadcn/src/hooks/ui/dialog.tsx | 138 ++ .../shadcn/src/hooks/ui/dropdown-menu.tsx | 257 ++++ .../shadcn/src/hooks/ui/follow-cursor.tsx | 123 ++ packages/shadcn/src/hooks/ui/form.tsx | 167 ++ .../shadcn/src/hooks/ui/grid-background.tsx | 23 + packages/shadcn/src/hooks/ui/heading.tsx | 13 + packages/shadcn/src/hooks/ui/input copy.tsx | 21 + packages/shadcn/src/hooks/ui/input.tsx | 19 + packages/shadcn/src/hooks/ui/label.tsx | 24 + packages/shadcn/src/hooks/ui/modal.tsx | 42 + .../shadcn/src/hooks/ui/mode-switcher.tsx | 69 + packages/shadcn/src/hooks/ui/popover.tsx | 48 + packages/shadcn/src/hooks/ui/retro-grid.tsx | 32 + packages/shadcn/src/hooks/ui/scroll-area.tsx | 58 + packages/shadcn/src/hooks/ui/select.tsx | 185 +++ packages/shadcn/src/hooks/ui/separator.tsx | 28 + packages/shadcn/src/hooks/ui/sheet.tsx | 139 ++ packages/shadcn/src/hooks/ui/sidebar.tsx | 724 +++++++++ packages/shadcn/src/hooks/ui/skeleton.tsx | 13 + packages/shadcn/src/hooks/ui/table.tsx | 116 ++ .../hooks/ui/table/data-table-filter-box.tsx | 167 ++ .../ui/table/data-table-reset-filter.tsx | 22 + .../src/hooks/ui/table/data-table-search.tsx | 64 + .../hooks/ui/table/data-table-skeleton.tsx | 95 ++ .../shadcn/src/hooks/ui/table/data-table.tsx | 240 +++ packages/shadcn/src/hooks/ui/tag-input.tsx | 251 +++ packages/shadcn/src/hooks/ui/textarea.tsx | 18 + .../shadcn/src/hooks/ui/texture-button.tsx | 110 ++ .../shadcn/src/hooks/ui/themes-provider.tsx | 9 + packages/shadcn/src/hooks/ui/tooltip copy.tsx | 61 + packages/shadcn/src/hooks/ui/tooltip.tsx | 61 + packages/shadcn/src/hooks/ui/true-focus.tsx | 154 ++ .../shadcn/src/hooks/use-click-outside.tsx | 31 + packages/shadcn/src/hooks/use-mobile.ts | 19 + packages/shadcn/src/hooks/use-tags.tsx | 65 + packages/shadcn/src/lib/searchparams.ts | 17 + packages/shadcn/src/lib/utils.ts | 6 + packages/shadcn/src/shadcn.css | 184 +++ packages/shadcn/tsconfig.json | 11 + packages/ts-config/base.json | 19 + packages/ts-config/nestjs.json | 19 + packages/ts-config/nextjs.json | 12 + packages/ts-config/package.json | 9 + packages/ts-config/react-library.json | 7 + pnpm-workspace.yaml | 3 + renovate.json | 4 + turbo.json | 29 + 411 files changed, 26265 insertions(+) create mode 100644 .commitlintrc.ts create mode 100644 .gitignore create mode 100644 .husky/commit-msg create mode 100644 .lintstagedrc create mode 100644 .npmrc create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 LICENSE create mode 100644 README.md create mode 100644 apps/api/.env.example create mode 100644 apps/api/.gitignore create mode 100644 apps/api/.swcrc create mode 100644 apps/api/README.md create mode 100644 apps/api/eslint.config.mjs create mode 100644 apps/api/nest-cli.json create mode 100644 apps/api/package.json create mode 100644 apps/api/src/app.module.ts create mode 100644 apps/api/src/bootstrap.ts create mode 100644 apps/api/src/common/config/envs.ts create mode 100644 apps/api/src/common/constants/index.ts create mode 100644 apps/api/src/common/constants/role.ts create mode 100644 apps/api/src/common/decorators/index.ts create mode 100644 apps/api/src/common/decorators/permissions.decorator.ts create mode 100644 apps/api/src/common/decorators/public.decorator.ts create mode 100644 apps/api/src/common/decorators/require-permissions.decorator.ts create mode 100644 apps/api/src/common/decorators/roles.decorator.ts create mode 100644 apps/api/src/common/decorators/user.decorator.ts create mode 100644 apps/api/src/common/dto/pagination.dto.ts create mode 100644 apps/api/src/common/guards/index.ts create mode 100644 apps/api/src/common/guards/jwt-auth.guard.ts create mode 100644 apps/api/src/common/guards/jwt-refresh.guard.ts create mode 100644 apps/api/src/common/guards/roles.guard.ts create mode 100644 apps/api/src/common/interceptors/index.ts create mode 100644 apps/api/src/common/interceptors/req-log.interceptor.ts create mode 100644 apps/api/src/common/middlewares/index.ts create mode 100644 apps/api/src/common/middlewares/logger.middleware.ts create mode 100644 apps/api/src/common/modules/index.ts create mode 100644 apps/api/src/common/modules/logger.module.ts create mode 100644 apps/api/src/common/modules/node-mailer.module.ts create mode 100644 apps/api/src/common/modules/throttle.module.ts create mode 100644 apps/api/src/common/pipes/file-size-validator.pipe.ts create mode 100644 apps/api/src/common/pipes/file-type-validator.pipe.ts create mode 100644 apps/api/src/common/pipes/index.ts create mode 100644 apps/api/src/common/pipes/zod-validator.pipe.ts create mode 100644 apps/api/src/common/utils/bcrypt.ts create mode 100644 apps/api/src/common/utils/dateTimeUtility.ts create mode 100644 apps/api/src/common/utils/index.ts create mode 100644 apps/api/src/common/utils/validateEnv.ts create mode 100644 apps/api/src/database/drizzle-provider.ts create mode 100644 apps/api/src/database/drizzle.module.ts create mode 100644 apps/api/src/database/index.ts create mode 100644 apps/api/src/database/migrations/0000_abnormal_lethal_legion.sql create mode 100644 apps/api/src/database/migrations/0001_massive_kylun.sql create mode 100644 apps/api/src/database/migrations/meta/0000_snapshot.json create mode 100644 apps/api/src/database/migrations/meta/0001_snapshot.json create mode 100644 apps/api/src/database/migrations/meta/_journal.json create mode 100644 apps/api/src/database/schema/activity_logs.ts create mode 100644 apps/api/src/database/schema/auth.ts create mode 100644 apps/api/src/database/schema/enum.ts create mode 100644 apps/api/src/database/schema/general.ts create mode 100644 apps/api/src/database/schema/schemas.ts create mode 100644 apps/api/src/database/schema/surveys.ts create mode 100644 apps/api/src/database/seeds/admin-role.seed.ts create mode 100644 apps/api/src/database/seeds/index.ts create mode 100644 apps/api/src/database/seeds/municipalities.seed.ts create mode 100644 apps/api/src/database/seeds/parishes.seed.ts create mode 100644 apps/api/src/database/seeds/states.seed.ts create mode 100644 apps/api/src/database/seeds/user-admin.seed.ts create mode 100644 apps/api/src/database/timestamps.ts create mode 100644 apps/api/src/drizzle.config.ts create mode 100644 apps/api/src/features/auth/auth.controller.ts create mode 100644 apps/api/src/features/auth/auth.module.ts create mode 100644 apps/api/src/features/auth/auth.service.ts create mode 100644 apps/api/src/features/auth/dto/change-password.dto.ts create mode 100644 apps/api/src/features/auth/dto/confirm-email.dto.ts create mode 100644 apps/api/src/features/auth/dto/create-user.dto.ts create mode 100644 apps/api/src/features/auth/dto/forgot-password.dto.ts create mode 100644 apps/api/src/features/auth/dto/refresh-token.dto.ts create mode 100644 apps/api/src/features/auth/dto/reset-password.dto.ts create mode 100644 apps/api/src/features/auth/dto/signIn-user.dto.ts create mode 100644 apps/api/src/features/auth/dto/signOut-user.dto.ts create mode 100644 apps/api/src/features/auth/dto/signUp-user.dto.ts create mode 100644 apps/api/src/features/auth/dto/update-refresh-token.dto.ts create mode 100644 apps/api/src/features/auth/dto/validate-user.dto.ts create mode 100644 apps/api/src/features/auth/interfaces/auth-tokens.interface.ts create mode 100644 apps/api/src/features/auth/interfaces/login-user.interface.ts create mode 100644 apps/api/src/features/auth/interfaces/refresh-token.interface.ts create mode 100644 apps/api/src/features/auth/interfaces/session.interface.ts create mode 100644 apps/api/src/features/configurations/category-types/category-types.controller.ts create mode 100644 apps/api/src/features/configurations/category-types/category-types.module.ts create mode 100644 apps/api/src/features/configurations/category-types/category-types.service.ts create mode 100644 apps/api/src/features/configurations/category-types/dto/create-category-type.dto.ts create mode 100644 apps/api/src/features/configurations/category-types/dto/update-category-type.dto.ts create mode 100644 apps/api/src/features/configurations/category-types/entities/category-type.entity.ts create mode 100644 apps/api/src/features/configurations/configurations.module.ts create mode 100644 apps/api/src/features/configurations/municipalities/dto/create-municipality.dto.ts create mode 100644 apps/api/src/features/configurations/municipalities/dto/update-municipality.dto.ts create mode 100644 apps/api/src/features/configurations/municipalities/entities/municipality.entity.ts create mode 100644 apps/api/src/features/configurations/municipalities/municipalities.controller.ts create mode 100644 apps/api/src/features/configurations/municipalities/municipalities.module.ts create mode 100644 apps/api/src/features/configurations/municipalities/municipalities.service.ts create mode 100644 apps/api/src/features/configurations/parishes/dto/create-parish.dto.ts create mode 100644 apps/api/src/features/configurations/parishes/dto/update-parish.dto.ts create mode 100644 apps/api/src/features/configurations/parishes/entities/parish.entity.ts create mode 100644 apps/api/src/features/configurations/parishes/parishes.controller.ts create mode 100644 apps/api/src/features/configurations/parishes/parishes.module.ts create mode 100644 apps/api/src/features/configurations/parishes/parishes.service.ts create mode 100644 apps/api/src/features/configurations/states/dto/create-state.dto.ts create mode 100644 apps/api/src/features/configurations/states/dto/update-state.dto.ts create mode 100644 apps/api/src/features/configurations/states/entities/state.entity.ts create mode 100644 apps/api/src/features/configurations/states/states.controller.ts create mode 100644 apps/api/src/features/configurations/states/states.module.ts create mode 100644 apps/api/src/features/configurations/states/states.service.ts create mode 100644 apps/api/src/features/location/entities/user.entity.ts create mode 100644 apps/api/src/features/location/location.controller.ts create mode 100644 apps/api/src/features/location/location.module.ts create mode 100644 apps/api/src/features/location/location.service.ts create mode 100644 apps/api/src/features/mail/mail.module.ts create mode 100644 apps/api/src/features/mail/mail.service.spec.ts create mode 100644 apps/api/src/features/mail/mail.service.ts create mode 100644 apps/api/src/features/mail/templates/change-password.mail.ts create mode 100644 apps/api/src/features/mail/templates/confirm-email.mail.ts create mode 100644 apps/api/src/features/mail/templates/forgot-password.mail.ts create mode 100644 apps/api/src/features/mail/templates/index.ts create mode 100644 apps/api/src/features/mail/templates/register.mail.ts create mode 100644 apps/api/src/features/mail/templates/sign-in.mail.ts create mode 100644 apps/api/src/features/roles/dto/create-role.dto.ts create mode 100644 apps/api/src/features/roles/dto/update-role.dto.ts create mode 100644 apps/api/src/features/roles/entities/role.entity.ts create mode 100644 apps/api/src/features/roles/roles.controller.ts create mode 100644 apps/api/src/features/roles/roles.module.ts create mode 100644 apps/api/src/features/roles/roles.service.ts create mode 100644 apps/api/src/features/surveys/Untitled-1.json create mode 100644 apps/api/src/features/surveys/dto/create-survey.dto.ts create mode 100644 apps/api/src/features/surveys/dto/find-for-user.dto.ts create mode 100644 apps/api/src/features/surveys/dto/response-survey.dto.ts create mode 100644 apps/api/src/features/surveys/dto/statistics-response.dto.ts create mode 100644 apps/api/src/features/surveys/dto/update-survey.dto.ts create mode 100644 apps/api/src/features/surveys/entities/survey.entity.ts create mode 100644 apps/api/src/features/surveys/surveys.controller.ts create mode 100644 apps/api/src/features/surveys/surveys.module.ts create mode 100644 apps/api/src/features/surveys/surveys.service.ts create mode 100644 apps/api/src/features/user-roles/dto/assign-role.dto.ts create mode 100644 apps/api/src/features/user-roles/user-roles.controller.ts create mode 100644 apps/api/src/features/user-roles/user-roles.module.ts create mode 100644 apps/api/src/features/user-roles/user-roles.service.ts create mode 100644 apps/api/src/features/users/dto/create-user.dto.ts create mode 100644 apps/api/src/features/users/dto/update-user.dto.ts create mode 100644 apps/api/src/features/users/entities/user.entity.ts create mode 100644 apps/api/src/features/users/users.controller.ts create mode 100644 apps/api/src/features/users/users.module.ts create mode 100644 apps/api/src/features/users/users.service.ts create mode 100644 apps/api/src/main.ts create mode 100644 apps/api/src/swagger.ts create mode 100644 apps/api/test/app.e2e-spec.ts create mode 100644 apps/api/test/jest-e2e.json create mode 100644 apps/api/tsconfig.build.json create mode 100644 apps/api/tsconfig.json create mode 100644 apps/api/uploads/.gitkeep create mode 100644 apps/web/.env_template create mode 100644 apps/web/.gitignore create mode 100644 apps/web/README.md create mode 100644 apps/web/app/(auth)/page.tsx create mode 100644 apps/web/app/api/auth/[...nextauth]/route.ts create mode 100644 apps/web/app/dashboard/administracion/encuestas/crear/page.tsx create mode 100644 apps/web/app/dashboard/administracion/encuestas/editar/[id]/page.tsx create mode 100644 apps/web/app/dashboard/administracion/encuestas/page.tsx create mode 100644 apps/web/app/dashboard/administracion/usuario/page.tsx create mode 100644 apps/web/app/dashboard/configuraciones/caja-ahorro/page.tsx create mode 100644 apps/web/app/dashboard/encuestas/[id]/responder/page.tsx create mode 100644 apps/web/app/dashboard/encuestas/page.tsx create mode 100644 apps/web/app/dashboard/estadisticas/encuestas/page.tsx create mode 100644 apps/web/app/dashboard/inicio/page.tsx create mode 100644 apps/web/app/dashboard/layout.tsx create mode 100644 apps/web/app/dashboard/page.tsx create mode 100644 apps/web/app/dashboard/profile/page.tsx create mode 100644 apps/web/app/error.tsx create mode 100644 apps/web/app/fonts/GeistMonoVF.woff create mode 100644 apps/web/app/fonts/GeistVF.woff create mode 100644 apps/web/app/layout.tsx create mode 100644 apps/web/app/not-found.tsx create mode 100644 apps/web/app/og/mono.ttf create mode 100644 apps/web/app/og/route.tsx create mode 100644 apps/web/app/opengraph-image.tsx create mode 100644 apps/web/app/register/page.tsx create mode 100644 apps/web/components.json create mode 100644 apps/web/components/breadcrumbs.tsx create mode 100644 apps/web/components/icons.tsx create mode 100644 apps/web/components/layout/ThemeToggle/theme-provider.tsx create mode 100644 apps/web/components/layout/ThemeToggle/theme-toggle.tsx create mode 100644 apps/web/components/layout/app-sidebar.tsx create mode 100644 apps/web/components/layout/header.tsx create mode 100644 apps/web/components/layout/page-container.tsx create mode 100644 apps/web/components/layout/providers.tsx create mode 100644 apps/web/components/layout/user-nav.tsx create mode 100644 apps/web/components/modal/alert-modal.tsx create mode 100644 apps/web/components/nav-main.tsx create mode 100644 apps/web/components/nav-projects.tsx create mode 100644 apps/web/components/team-switcher.tsx create mode 100644 apps/web/constants/data.ts create mode 100644 apps/web/eslint.config.js create mode 100644 apps/web/feactures/auth/actions/login-action.ts create mode 100644 apps/web/feactures/auth/actions/refresh-token-action.ts create mode 100644 apps/web/feactures/auth/actions/register.ts create mode 100644 apps/web/feactures/auth/components/sigin-view.tsx create mode 100644 apps/web/feactures/auth/components/signup-view.tsx create mode 100644 apps/web/feactures/auth/components/user-auth-form.tsx create mode 100644 apps/web/feactures/auth/components/user-register-form.tsx create mode 100644 apps/web/feactures/auth/hooks/use-mutation-users.ts create mode 100644 apps/web/feactures/auth/schemas/login.ts create mode 100644 apps/web/feactures/auth/schemas/refreshToken.ts create mode 100644 apps/web/feactures/auth/schemas/register.ts create mode 100644 apps/web/feactures/location/actions/actions.ts create mode 100644 apps/web/feactures/location/hooks/use-query-location.ts create mode 100644 apps/web/feactures/location/schemas/users.ts create mode 100644 apps/web/feactures/statistics/actions/surveys-statistics-actions.ts create mode 100644 apps/web/feactures/statistics/components/survey-details.tsx create mode 100644 apps/web/feactures/statistics/components/survey-overview.tsx create mode 100644 apps/web/feactures/statistics/components/survey-responses.tsx create mode 100644 apps/web/feactures/statistics/components/survey-statistics.tsx create mode 100644 apps/web/feactures/statistics/hooks/use-query-statistics.ts create mode 100644 apps/web/feactures/statistics/schemas/statistics-schema.ts create mode 100644 apps/web/feactures/statistics/schemas/statistics.ts create mode 100644 apps/web/feactures/surveys/actions/surveys-actions.ts create mode 100644 apps/web/feactures/surveys/components/admin/question-config-modal.tsx create mode 100644 apps/web/feactures/surveys/components/admin/question-toolbox.tsx create mode 100644 apps/web/feactures/surveys/components/admin/survey-builder.tsx create mode 100644 apps/web/feactures/surveys/components/admin/surveys-admin-list.tsx create mode 100644 apps/web/feactures/surveys/components/admin/surveys-header.tsx create mode 100644 apps/web/feactures/surveys/components/admin/surveys-tables/cell-action.tsx create mode 100644 apps/web/feactures/surveys/components/admin/surveys-tables/columns.tsx create mode 100644 apps/web/feactures/surveys/components/admin/surveys-tables/survey-table-action.tsx create mode 100644 apps/web/feactures/surveys/components/admin/surveys-tables/use-survey-table-filters.tsx create mode 100644 apps/web/feactures/surveys/components/survey-list.tsx create mode 100644 apps/web/feactures/surveys/components/survey-response.tsx create mode 100644 apps/web/feactures/surveys/components/survey.tsx create mode 100644 apps/web/feactures/surveys/hooks/use-mutation-surveys.ts create mode 100644 apps/web/feactures/surveys/hooks/use-query-surveys.ts create mode 100644 apps/web/feactures/surveys/schemas/survey.ts create mode 100644 apps/web/feactures/surveys/schemas/surveys-options.ts create mode 100644 apps/web/feactures/surveys/utils/date-utils.ts create mode 100644 apps/web/feactures/surveys/utils/searchparams.ts create mode 100644 apps/web/feactures/users/actions/actions.ts create mode 100644 apps/web/feactures/users/components/admin/create-user-form.tsx create mode 100644 apps/web/feactures/users/components/admin/surveys-tables/cell-action.tsx create mode 100644 apps/web/feactures/users/components/admin/surveys-tables/columns.tsx create mode 100644 apps/web/feactures/users/components/admin/surveys-tables/use-survey-table-filters.tsx create mode 100644 apps/web/feactures/users/components/admin/surveys-tables/users-table-action.tsx create mode 100644 apps/web/feactures/users/components/admin/update-user-form.tsx create mode 100644 apps/web/feactures/users/components/admin/user-modal.tsx create mode 100644 apps/web/feactures/users/components/admin/users-admin-list.tsx create mode 100644 apps/web/feactures/users/components/admin/users-header.tsx create mode 100644 apps/web/feactures/users/components/modal-profile.tsx create mode 100644 apps/web/feactures/users/components/selectList.tsx create mode 100644 apps/web/feactures/users/components/survey.tsx create mode 100644 apps/web/feactures/users/components/update-user-form.tsx create mode 100644 apps/web/feactures/users/components/user-profile.tsx create mode 100644 apps/web/feactures/users/hooks/use-mutation-users.ts create mode 100644 apps/web/feactures/users/hooks/use-query-surveys.ts create mode 100644 apps/web/feactures/users/hooks/use-query-users.ts create mode 100644 apps/web/feactures/users/schemas/account-plan-options.ts create mode 100644 apps/web/feactures/users/schemas/account-plan.schema.ts create mode 100644 apps/web/feactures/users/schemas/surveys-options.ts create mode 100644 apps/web/feactures/users/schemas/users.ts create mode 100644 apps/web/feactures/users/utils/date-utils.ts create mode 100644 apps/web/feactures/users/utils/searchparams.ts create mode 100644 apps/web/hooks/use-breadcrumbs.tsx create mode 100644 apps/web/hooks/use-mobile.ts create mode 100644 apps/web/hooks/use-safe-query.ts create mode 100644 apps/web/lib/auth.config.ts create mode 100644 apps/web/lib/auth.ts create mode 100644 apps/web/lib/buildSearchParams.ts create mode 100644 apps/web/lib/env.ts create mode 100644 apps/web/lib/fetch.api.ts create mode 100644 apps/web/lib/formData.ts create mode 100644 apps/web/lib/index.ts create mode 100644 apps/web/lib/safeAction.ts create mode 100644 apps/web/middleware.ts create mode 100644 apps/web/next.config.js create mode 100644 apps/web/package.json create mode 100644 apps/web/postcss.config.mjs create mode 100644 apps/web/public/icon-2.png create mode 100644 apps/web/public/icon.png create mode 100644 apps/web/public/logo.png create mode 100644 apps/web/public/og-bg.png create mode 100644 apps/web/server/home.server.ts create mode 100644 apps/web/tsconfig.json create mode 100644 apps/web/types/index.ts create mode 100644 apps/web/types/next-auth.d.ts create mode 100644 apps/web/types/user.type.ts create mode 100644 assets/lifecycle.png create mode 100644 assets/preview.png create mode 100644 package.json create mode 100644 packages/eslint-config/README.md create mode 100644 packages/eslint-config/base.js create mode 100644 packages/eslint-config/nest.js create mode 100644 packages/eslint-config/next.js create mode 100644 packages/eslint-config/package.json create mode 100644 packages/eslint-config/react-internal.js create mode 100644 packages/shadcn/README.md create mode 100644 packages/shadcn/components.json create mode 100644 packages/shadcn/package.json create mode 100644 packages/shadcn/postcss.config.mjs create mode 100644 packages/shadcn/src/components/icon/index.tsx create mode 100644 packages/shadcn/src/components/ui/accordion.tsx create mode 100644 packages/shadcn/src/components/ui/alert-dialog.tsx create mode 100644 packages/shadcn/src/components/ui/avatar.tsx create mode 100644 packages/shadcn/src/components/ui/background-box.tsx create mode 100644 packages/shadcn/src/components/ui/badge.tsx create mode 100644 packages/shadcn/src/components/ui/breadcrumb.tsx create mode 100644 packages/shadcn/src/components/ui/button copy.tsx create mode 100644 packages/shadcn/src/components/ui/button.tsx create mode 100644 packages/shadcn/src/components/ui/calendar.tsx create mode 100644 packages/shadcn/src/components/ui/card.tsx create mode 100644 packages/shadcn/src/components/ui/checkbox.tsx create mode 100644 packages/shadcn/src/components/ui/collapsible.tsx create mode 100644 packages/shadcn/src/components/ui/command.tsx create mode 100644 packages/shadcn/src/components/ui/dialog.tsx create mode 100644 packages/shadcn/src/components/ui/dropdown-menu.tsx create mode 100644 packages/shadcn/src/components/ui/follow-cursor.tsx create mode 100644 packages/shadcn/src/components/ui/form.tsx create mode 100644 packages/shadcn/src/components/ui/grid-background.tsx create mode 100644 packages/shadcn/src/components/ui/heading.tsx create mode 100644 packages/shadcn/src/components/ui/input copy.tsx create mode 100644 packages/shadcn/src/components/ui/input.tsx create mode 100644 packages/shadcn/src/components/ui/label.tsx create mode 100644 packages/shadcn/src/components/ui/modal.tsx create mode 100644 packages/shadcn/src/components/ui/mode-switcher.tsx create mode 100644 packages/shadcn/src/components/ui/popover.tsx create mode 100644 packages/shadcn/src/components/ui/radio-group.tsx create mode 100644 packages/shadcn/src/components/ui/retro-grid.tsx create mode 100644 packages/shadcn/src/components/ui/scroll-area.tsx create mode 100644 packages/shadcn/src/components/ui/select-searchable.tsx create mode 100644 packages/shadcn/src/components/ui/select.tsx create mode 100644 packages/shadcn/src/components/ui/separator.tsx create mode 100644 packages/shadcn/src/components/ui/sheet.tsx create mode 100644 packages/shadcn/src/components/ui/sidebar.tsx create mode 100644 packages/shadcn/src/components/ui/skeleton.tsx create mode 100644 packages/shadcn/src/components/ui/switch.tsx create mode 100644 packages/shadcn/src/components/ui/table.tsx create mode 100644 packages/shadcn/src/components/ui/table/data-table-filter-box.tsx create mode 100644 packages/shadcn/src/components/ui/table/data-table-reset-filter.tsx create mode 100644 packages/shadcn/src/components/ui/table/data-table-search.tsx create mode 100644 packages/shadcn/src/components/ui/table/data-table-skeleton.tsx create mode 100644 packages/shadcn/src/components/ui/table/data-table.tsx create mode 100644 packages/shadcn/src/components/ui/tabs.tsx create mode 100644 packages/shadcn/src/components/ui/tag-input.tsx create mode 100644 packages/shadcn/src/components/ui/textarea.tsx create mode 100644 packages/shadcn/src/components/ui/texture-button.tsx create mode 100644 packages/shadcn/src/components/ui/themes-provider.tsx create mode 100644 packages/shadcn/src/components/ui/tooltip copy.tsx create mode 100644 packages/shadcn/src/components/ui/tooltip.tsx create mode 100644 packages/shadcn/src/components/ui/true-focus.tsx create mode 100644 packages/shadcn/src/hooks/ui/accordion.tsx create mode 100644 packages/shadcn/src/hooks/ui/avatar.tsx create mode 100644 packages/shadcn/src/hooks/ui/background-box.tsx create mode 100644 packages/shadcn/src/hooks/ui/badge.tsx create mode 100644 packages/shadcn/src/hooks/ui/breadcrumb.tsx create mode 100644 packages/shadcn/src/hooks/ui/button copy.tsx create mode 100644 packages/shadcn/src/hooks/ui/button.tsx create mode 100644 packages/shadcn/src/hooks/ui/card.tsx create mode 100644 packages/shadcn/src/hooks/ui/checkbox.tsx create mode 100644 packages/shadcn/src/hooks/ui/collapsible.tsx create mode 100644 packages/shadcn/src/hooks/ui/command.tsx create mode 100644 packages/shadcn/src/hooks/ui/dialog.tsx create mode 100644 packages/shadcn/src/hooks/ui/dropdown-menu.tsx create mode 100644 packages/shadcn/src/hooks/ui/follow-cursor.tsx create mode 100644 packages/shadcn/src/hooks/ui/form.tsx create mode 100644 packages/shadcn/src/hooks/ui/grid-background.tsx create mode 100644 packages/shadcn/src/hooks/ui/heading.tsx create mode 100644 packages/shadcn/src/hooks/ui/input copy.tsx create mode 100644 packages/shadcn/src/hooks/ui/input.tsx create mode 100644 packages/shadcn/src/hooks/ui/label.tsx create mode 100644 packages/shadcn/src/hooks/ui/modal.tsx create mode 100644 packages/shadcn/src/hooks/ui/mode-switcher.tsx create mode 100644 packages/shadcn/src/hooks/ui/popover.tsx create mode 100644 packages/shadcn/src/hooks/ui/retro-grid.tsx create mode 100644 packages/shadcn/src/hooks/ui/scroll-area.tsx create mode 100644 packages/shadcn/src/hooks/ui/select.tsx create mode 100644 packages/shadcn/src/hooks/ui/separator.tsx create mode 100644 packages/shadcn/src/hooks/ui/sheet.tsx create mode 100644 packages/shadcn/src/hooks/ui/sidebar.tsx create mode 100644 packages/shadcn/src/hooks/ui/skeleton.tsx create mode 100644 packages/shadcn/src/hooks/ui/table.tsx create mode 100644 packages/shadcn/src/hooks/ui/table/data-table-filter-box.tsx create mode 100644 packages/shadcn/src/hooks/ui/table/data-table-reset-filter.tsx create mode 100644 packages/shadcn/src/hooks/ui/table/data-table-search.tsx create mode 100644 packages/shadcn/src/hooks/ui/table/data-table-skeleton.tsx create mode 100644 packages/shadcn/src/hooks/ui/table/data-table.tsx create mode 100644 packages/shadcn/src/hooks/ui/tag-input.tsx create mode 100644 packages/shadcn/src/hooks/ui/textarea.tsx create mode 100644 packages/shadcn/src/hooks/ui/texture-button.tsx create mode 100644 packages/shadcn/src/hooks/ui/themes-provider.tsx create mode 100644 packages/shadcn/src/hooks/ui/tooltip copy.tsx create mode 100644 packages/shadcn/src/hooks/ui/tooltip.tsx create mode 100644 packages/shadcn/src/hooks/ui/true-focus.tsx create mode 100644 packages/shadcn/src/hooks/use-click-outside.tsx create mode 100644 packages/shadcn/src/hooks/use-mobile.ts create mode 100644 packages/shadcn/src/hooks/use-tags.tsx create mode 100644 packages/shadcn/src/lib/searchparams.ts create mode 100644 packages/shadcn/src/lib/utils.ts create mode 100644 packages/shadcn/src/shadcn.css create mode 100644 packages/shadcn/tsconfig.json create mode 100644 packages/ts-config/base.json create mode 100644 packages/ts-config/nestjs.json create mode 100644 packages/ts-config/nextjs.json create mode 100644 packages/ts-config/package.json create mode 100644 packages/ts-config/react-library.json create mode 100644 pnpm-workspace.yaml create mode 100644 renovate.json create mode 100644 turbo.json diff --git a/.commitlintrc.ts b/.commitlintrc.ts new file mode 100644 index 0000000..3f5e287 --- /dev/null +++ b/.commitlintrc.ts @@ -0,0 +1 @@ +export default { extends: ['@commitlint/config-conventional'] }; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cae925f --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +.idea +# Dependencies +node_modules +.pnp +.pnp.js +pnpm-lock.yaml + +# Local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Testing +coverage + +# Turbo +.turbo + +# Vercel +.vercel + +# Build Outputs +.next/ +out/ +build +dist + + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Misc +.DS_Store +*.pem diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 0000000..f976c0c --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,4 @@ +# #!/usr/bin/env sh +# . "$(dirname -- "$0")/_/husky.sh" + +# npx commitlint --edit $1 \ No newline at end of file diff --git a/.lintstagedrc b/.lintstagedrc new file mode 100644 index 0000000..00ad986 --- /dev/null +++ b/.lintstagedrc @@ -0,0 +1,3 @@ +{ + "*.{js,jsx,ts,tsx}": ["prettier . --write"] +} diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..e69de29 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..5d6d342 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +node_modules +pnpm-lock.yaml +.gitignore +.idea +.turbo +.changeset +pnpm-workspace.yaml diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..e1ff765 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "plugins": [ + "prettier-plugin-tailwindcss", + "prettier-plugin-css-order", + "prettier-plugin-organize-imports", + "prettier-plugin-packagejson" + ], + "tailwindFunctions": ["clsx", "cn", "twMerge"] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f701435 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Aung Pyae Phyo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2111535 --- /dev/null +++ b/README.md @@ -0,0 +1,88 @@ + +## NestJS & NextJS + +Repositorio Sistema Base Fondemi. + +### **Caracteristicas** + +- Backend `NestJS (v11)` +- Frontend `NextJS (v15)` +- `SWC` para una transpilación rápida de TypeScript y JavaScript +- `pnpm` para una gestión eficiente de dependencias +- Autenticación con token de acceso y token de actualización `JWT` para un acceso seguro a la API +- Base de datos `PostgreSQL` con Drizzle ORM +- `Nodemailer` para servicios de correo electrónico +- `Linting` y `Formatting` preconfigurados para la calidad del código +- Compatibilidad con `Micro-Frontend` con Turborepo +- Integración con `Shadcn/UI` para componentes con estilo +- Integración con `Tailwindcss(v4)` en `@repo/shadcn` + +### **Tabla de contenido** + +- Installation +- Getting Started +- Project Structure +- Scripts +- Contributing +- License + +### **Installation** + +Clona el repositorio: + +```shell +git clone https://git.fondemi.gob.ve/Fondemi/sistema_base.git +``` + +Clona las variables de entorno y reemplaz la informacion: + +```shell +cp .env.exmple .env +``` + + +Instala dependencias usando pnpm: + +```shell +pnpm install +``` + + +Migra la base de datos: + +```shell +pnpm db:migrate +``` + +Inicio +Inicio el servidor en desarrollo, run: + +```shell +pnpm dev +``` + + +Estructura del proyecto +El repositorio está organizado de la siguiente manera: + +```yaml +turborepo +├── .husky # Git hooks +├── apps +│ ├── api # NestJS application +│ └── web # NextJS application +├── packages +│ ├── shadcn # shadcn/UI component library +│ ├── ts-config # Shared typescript configuration files +│ ├── eslint-config # Shared eslint configuration files +└── turbo.json # Turborepo configuration +``` + +### Caracteristicas del sistema + +- Administración de encuestas +- Responder encuestas +- Registro usuario +- Login de usuario +- Estadisticas de encuestas + diff --git a/apps/api/.env.example b/apps/api/.env.example new file mode 100644 index 0000000..0e8021f --- /dev/null +++ b/apps/api/.env.example @@ -0,0 +1,19 @@ +# Server Configuration +HOST=localhost +PORT=8000 +ALLOW_CORS_URL=http://localhost:3000 +NODE_ENV='development' #development | production + +#Jwt Securtiy +ACCESS_TOKEN_SECRET=bc63d848ca6e651b3b848bd96ef1ad1eb9b31afc9cad67ed5953efd023d02ffe +ACCESS_TOKEN_EXPIRATION=2h +REFRESH_TOKEN_SECRET=bc63d848ca6e651b3b848bd96ef1ad1eb9b31afc9cad67ed5953efd023d02ffe +REFRESH_TOKEN_EXPIRATION=30d + +#Database Configuration +DATABASE_URL="postgresql://postgres:local**@localhost:5432/caja_ahorro" #url conexion a base de datos + +#Mail Configuration +MAIL_HOST=gmail +MAIL_USERNAME= +MAIL_PASSWORD= diff --git a/apps/api/.gitignore b/apps/api/.gitignore new file mode 100644 index 0000000..4b56acf --- /dev/null +++ b/apps/api/.gitignore @@ -0,0 +1,56 @@ +# compiled output +/dist +/node_modules +/build + +# Logs +logs +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# OS +.DS_Store + +# Tests +/coverage +/.nyc_output + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# temp directory +.temp +.tmp + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json diff --git a/apps/api/.swcrc b/apps/api/.swcrc new file mode 100644 index 0000000..09bf764 --- /dev/null +++ b/apps/api/.swcrc @@ -0,0 +1,13 @@ +{ + "$schema": "https://swc.rs/schema.json", + "sourceMaps": true, + "jsc": { + "parser": { + "syntax": "typescript", + "decorators": true, + "dynamicImport": true + }, + "baseUrl": "./" + }, + "minify": false +} diff --git a/apps/api/README.md b/apps/api/README.md new file mode 100644 index 0000000..55b6565 --- /dev/null +++ b/apps/api/README.md @@ -0,0 +1 @@ +### Backend diff --git a/apps/api/eslint.config.mjs b/apps/api/eslint.config.mjs new file mode 100644 index 0000000..2fec3a5 --- /dev/null +++ b/apps/api/eslint.config.mjs @@ -0,0 +1,4 @@ +import { nestJsConfig } from '@repo/eslint-config/nest-js'; + +/** @type {import("eslint").Linter.Config} */ +export default nestJsConfig; diff --git a/apps/api/nest-cli.json b/apps/api/nest-cli.json new file mode 100644 index 0000000..579ca35 --- /dev/null +++ b/apps/api/nest-cli.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true, + "builder": "swc", + "typeCheck": true + } +} diff --git a/apps/api/package.json b/apps/api/package.json new file mode 100644 index 0000000..cc08ae0 --- /dev/null +++ b/apps/api/package.json @@ -0,0 +1,87 @@ +{ + "name": "api", + "private": true, + "scripts": { + "build": "nest build", + "db:generate": "drizzle-kit generate --config ./src/drizzle.config.ts", + "db:migrate": "drizzle-kit migrate --config ./src/drizzle.config.ts", + "db:push": "drizzle-kit push --config ./src/drizzle.config.ts", + "db:seed": "ts-node -r tsconfig-paths/register ./src/database/seeds/index.ts", + "dev": "nest start -b swc -w", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "start": "node dist/main", + "start:debug": "nest start --debug --watch", + "start:dev": "nest start", + "test": "jest", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config ./test/jest-e2e.json", + "test:watch": "jest --watch" + }, + "jest": { + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], + "coverageDirectory": "../coverage", + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testEnvironment": "node", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } + }, + "dependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0", + "@nestjs/platform-express": "^11.0.0", + "drizzle-orm": "^0.40.0", + "joi": "^17.13.3", + "moment": "^2.30.1", + "path-to-regexp": "^8.2.0", + "pg": "^8.13.3", + "pino-pretty": "^13.0.0", + "reflect-metadata": "^0.2.0", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@nestjs-modules/mailer": "^2.0.2", + "@nestjs/cli": "^11.0.0", + "@nestjs/config": "^4.0.0", + "@nestjs/jwt": "^11.0.0", + "@nestjs/schematics": "^11.0.0", + "@nestjs/swagger": "^11.0.0", + "@nestjs/testing": "^11.0.0", + "@nestjs/throttler": "^6.3.0", + "@repo/eslint-config": "workspace:*", + "@repo/ts-config": "workspace:*", + "@swc/cli": "^0.6.0", + "@swc/core": "^1.10.14", + "@types/bcryptjs": "^2.4.6", + "@types/express": "^5.0.0", + "@types/jest": "^29.5.2", + "@types/multer": "^1.4.12", + "@types/pg": "^8.11.11", + "@types/supertest": "^6.0.0", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "bcryptjs": "^3.0.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "drizzle-kit": "^0.30.5", + "jest": "^29.5.0", + "nestjs-pino": "^4.1.0", + "source-map-support": "^0.5.21", + "supertest": "^7.0.0", + "ts-jest": "^29.1.0", + "ts-loader": "^9.4.3", + "ts-node": "^10.9.1", + "tsconfig-paths": "^4.2.0", + "tsx": "^4.19.3" + } +} diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts new file mode 100644 index 0000000..ac53a17 --- /dev/null +++ b/apps/api/src/app.module.ts @@ -0,0 +1,64 @@ +import { JwtAuthGuard, RolesGuard } from '@/common/guards'; +import { + LoggerModule, + NodeMailerModule, + ThrottleModule, +} from '@/common/modules'; +import { UsersModule } from '@/features/users/users.module'; +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { APP_GUARD } from '@nestjs/core'; +import { JwtModule } from '@nestjs/jwt'; +import { ThrottlerGuard } from '@nestjs/throttler'; +import { DrizzleModule } from './database/drizzle.module'; +import { AuthModule } from './features/auth/auth.module'; +import { ConfigurationsModule } from './features/configurations/configurations.module'; +import { LocationModule} from './features/location/location.module' +import { MailModule } from './features/mail/mail.module'; +import { RolesModule } from './features/roles/roles.module'; +import { UserRolesModule } from './features/user-roles/user-roles.module'; +import { SurveysModule } from './features/surveys/surveys.module'; + + +@Module({ + providers: [ + { + provide: APP_GUARD, + useClass: JwtAuthGuard, + }, + { + provide: APP_GUARD, + useClass: RolesGuard, + }, + // { + // provide: APP_GUARD, + // useClass: PermissionsGuard, + // }, + { + provide: APP_GUARD, + useClass: ThrottlerGuard, + }, + ], + imports: [ + JwtModule.register({ + global: true, + }), + ConfigModule.forRoot({ + isGlobal: true, + //validate: validateEnv, + }), + NodeMailerModule, + LoggerModule, + ThrottleModule, + UsersModule, + AuthModule, + MailModule, + DrizzleModule, + RolesModule, + UserRolesModule, + ConfigurationsModule, + SurveysModule, + LocationModule + ], +}) +export class AppModule {} diff --git a/apps/api/src/bootstrap.ts b/apps/api/src/bootstrap.ts new file mode 100644 index 0000000..f67d416 --- /dev/null +++ b/apps/api/src/bootstrap.ts @@ -0,0 +1,35 @@ +import { swagger } from '@/swagger'; +import { ValidationPipe } from '@nestjs/common'; +import { NestExpressApplication } from '@nestjs/platform-express'; +import { Logger } from 'nestjs-pino'; +import { envs } from './common/config/envs'; + +export const bootstrap = async (app: NestExpressApplication) => { + const logger = app.get(Logger); + // app.setGlobalPrefix('api'); + app.useStaticAssets('./uploads', { + prefix: '/assets', + }); + app.enableCors({ + credentials: true, + //origin: envs.allow_cors_url, + origin: ['*'], + methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], + }); + app.useLogger(logger); + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + forbidNonWhitelisted: true, + transform: true, + transformOptions: { + enableImplicitConversion: true, + }, + }), + ); + + await swagger(app); + await app.listen(envs.port!, () => { + logger.log(`This application started at ${envs.host}:${envs.port}`); + }); +}; diff --git a/apps/api/src/common/config/envs.ts b/apps/api/src/common/config/envs.ts new file mode 100644 index 0000000..511490d --- /dev/null +++ b/apps/api/src/common/config/envs.ts @@ -0,0 +1,58 @@ +import 'dotenv/config'; +import * as joi from 'joi'; + +interface EnvVars { + HOST: string; + ALLOW_CORS_URL: string; + PORT: number; + NODE_ENV: string; + DATABASE_URL: string; + ACCESS_TOKEN_SECRET: string; + ACCESS_TOKEN_EXPIRATION: string; + REFRESH_TOKEN_SECRET: string; + REFRESH_TOKEN_EXPIRATION: string; + MAIL_HOST: string; + MAIL_USERNAME: string; + MAIL_PASSWORD: string; + +} + +const envsSchema = joi + .object({ + HOST: joi.string().required(), + ALLOW_CORS_URL: joi.string().required(), + PORT: joi.number().required(), + NODE_ENV: joi.string().required(), + DATABASE_URL: joi.string().required(), + ACCESS_TOKEN_SECRET: joi.string().required(), + ACCESS_TOKEN_EXPIRATION: joi.string().required(), + REFRESH_TOKEN_SECRET: joi.string().required(), + REFRESH_TOKEN_EXPIRATION: joi.string().required(), + MAIL_HOST: joi.string(), + MAIL_USERNAME: joi.string(), + MAIL_PASSWORD: joi.string(), + }) + .unknown(true); + +const { error, value } = envsSchema.validate(process.env); + +if (error) { + throw new Error(`Config validation error: ${error.message}`); +} + +const envVars: EnvVars = value; + +export const envs = { + port: envVars.PORT, + dataBaseUrl: envVars.DATABASE_URL, + node_env: envVars.NODE_ENV, + host: envVars.HOST, + allow_cors_url: envVars.ALLOW_CORS_URL, + access_token_secret: envVars.ACCESS_TOKEN_SECRET, + access_token_expiration: envVars.ACCESS_TOKEN_EXPIRATION, + refresh_token_secret: envVars.REFRESH_TOKEN_SECRET, + refresh_token_expiration: envVars.REFRESH_TOKEN_EXPIRATION, + mail_host: envVars.MAIL_HOST, + mail_username: envVars.MAIL_USERNAME, + mail_password: envVars.MAIL_PASSWORD +}; diff --git a/apps/api/src/common/constants/index.ts b/apps/api/src/common/constants/index.ts new file mode 100644 index 0000000..efbebd0 --- /dev/null +++ b/apps/api/src/common/constants/index.ts @@ -0,0 +1 @@ +export * from './role'; diff --git a/apps/api/src/common/constants/role.ts b/apps/api/src/common/constants/role.ts new file mode 100644 index 0000000..ed80175 --- /dev/null +++ b/apps/api/src/common/constants/role.ts @@ -0,0 +1,6 @@ +import { z } from 'zod'; + +// Cambiamos de un enum estático a un tipo string para soportar roles dinámicos +export const roleSchema = z.string(); + +export type Role = z.infer; diff --git a/apps/api/src/common/decorators/index.ts b/apps/api/src/common/decorators/index.ts new file mode 100644 index 0000000..ab100fe --- /dev/null +++ b/apps/api/src/common/decorators/index.ts @@ -0,0 +1,3 @@ +export * from './public.decorator'; +export * from './roles.decorator'; +export * from './user.decorator'; diff --git a/apps/api/src/common/decorators/permissions.decorator.ts b/apps/api/src/common/decorators/permissions.decorator.ts new file mode 100644 index 0000000..9b78dd2 --- /dev/null +++ b/apps/api/src/common/decorators/permissions.decorator.ts @@ -0,0 +1,5 @@ +import { SetMetadata } from '@nestjs/common'; + +export const PERMISSIONS_KEY = 'permissions'; +export const RequirePermissions = (...permissions: string[]) => + SetMetadata(PERMISSIONS_KEY, permissions); \ No newline at end of file diff --git a/apps/api/src/common/decorators/public.decorator.ts b/apps/api/src/common/decorators/public.decorator.ts new file mode 100644 index 0000000..b3845e1 --- /dev/null +++ b/apps/api/src/common/decorators/public.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from '@nestjs/common'; + +export const IS_PUBLIC_KEY = 'isPublic'; +export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); diff --git a/apps/api/src/common/decorators/require-permissions.decorator.ts b/apps/api/src/common/decorators/require-permissions.decorator.ts new file mode 100644 index 0000000..a6d5d97 --- /dev/null +++ b/apps/api/src/common/decorators/require-permissions.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from '@nestjs/common'; + +export const PERMISSIONS_KEY = 'permissions'; +export const RequirePermissions = (...permissions: string[]) => SetMetadata(PERMISSIONS_KEY, permissions); \ No newline at end of file diff --git a/apps/api/src/common/decorators/roles.decorator.ts b/apps/api/src/common/decorators/roles.decorator.ts new file mode 100644 index 0000000..23aae81 --- /dev/null +++ b/apps/api/src/common/decorators/roles.decorator.ts @@ -0,0 +1,5 @@ +import { SetMetadata } from '@nestjs/common'; +import { Role } from '../constants'; + +export const ROLES_KEY = 'roles'; +export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles); diff --git a/apps/api/src/common/decorators/user.decorator.ts b/apps/api/src/common/decorators/user.decorator.ts new file mode 100644 index 0000000..69bdbaa --- /dev/null +++ b/apps/api/src/common/decorators/user.decorator.ts @@ -0,0 +1,8 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + +export const User = createParamDecorator( + (data: unknown, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + return request.user; + }, +); diff --git a/apps/api/src/common/dto/pagination.dto.ts b/apps/api/src/common/dto/pagination.dto.ts new file mode 100644 index 0000000..b204537 --- /dev/null +++ b/apps/api/src/common/dto/pagination.dto.ts @@ -0,0 +1,35 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsOptional, IsInt, Min, IsString, IsIn } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class PaginationDto { + @ApiPropertyOptional({ default: 1, description: 'Page number' }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + page?: number; + + @ApiPropertyOptional({ default: 10, description: 'Items per page' }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + limit?: number; + + @ApiPropertyOptional({ description: 'Search term' }) + @IsOptional() + @IsString() + search?: string; + + @ApiPropertyOptional({ default: 'id', description: 'Field to sort by' }) + @IsOptional() + @IsString() + sortBy?: string; + + @ApiPropertyOptional({ default: 'asc', enum: ['asc', 'desc'], description: 'Sort order' }) + @IsOptional() + @IsString() + @IsIn(['asc', 'desc']) + sortOrder?: 'asc' | 'desc'; +} \ No newline at end of file diff --git a/apps/api/src/common/guards/index.ts b/apps/api/src/common/guards/index.ts new file mode 100644 index 0000000..e174be2 --- /dev/null +++ b/apps/api/src/common/guards/index.ts @@ -0,0 +1,2 @@ +export * from './jwt-auth.guard'; +export * from './roles.guard'; diff --git a/apps/api/src/common/guards/jwt-auth.guard.ts b/apps/api/src/common/guards/jwt-auth.guard.ts new file mode 100644 index 0000000..279694b --- /dev/null +++ b/apps/api/src/common/guards/jwt-auth.guard.ts @@ -0,0 +1,81 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; + +import { Reflector } from '@nestjs/core'; +import { JwtService } from '@nestjs/jwt'; +import { Request } from 'express'; +import { IS_PUBLIC_KEY } from 'src/common/decorators'; +import { envs } from '../config/envs'; +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider'; +import { Inject } from '@nestjs/common'; +import { roles, usersRole } from 'src/database/schema/auth'; +import { eq } from 'drizzle-orm'; + +@Injectable() +export class JwtAuthGuard implements CanActivate { + constructor( + private jwtService: JwtService, + private reflector: Reflector, + @Inject(DRIZZLE_PROVIDER) + private readonly drizzle: NodePgDatabase, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ + context.getHandler(), + context.getClass(), + ]); + if (isPublic) { + return true; + } + const request = context.switchToHttp().getRequest(); + const token = this.extractTokenFromHeader(request); + if (!token) { + throw new UnauthorizedException(); + } + try { + const payload = await this.jwtService.verifyAsync(token, { + secret: envs.access_token_secret, + }); + + // Asegurarse de que el payload contiene el ID del usuario + if (!payload.sub && !payload.id) { + throw new UnauthorizedException('Invalid token payload'); + } + + const userId = payload.sub || payload.id; + + // Obtener los roles del usuario desde la base de datos + const userRoles = await this.drizzle + .select({ name: roles.name }) + .from(roles) + .innerJoin(usersRole, eq(usersRole.roleId, roles.id)) + .where(eq(usersRole.userId, userId)); + + // Verificar si el usuario tiene el rol SUPERADMIN + const isSuperAdmin = userRoles.some(role => role.name === 'superadmin'); + + // Adjuntar el usuario a la solicitud con el ID correcto y sus roles + request.user = { + id: userId, + username: payload.username, + email: payload.email, + roles: userRoles.map(role => role.name), + isSuperAdmin, // Añadir flag para indicar si es SUPERADMIN + }; + } catch (error) { + throw new UnauthorizedException('Invalid Access Token'); + } + return true; + } + + private extractTokenFromHeader(request: Request): string | undefined { + const [type, token] = request.headers.authorization?.split(' ') ?? []; + return type === 'Bearer' ? token : undefined; + } +} diff --git a/apps/api/src/common/guards/jwt-refresh.guard.ts b/apps/api/src/common/guards/jwt-refresh.guard.ts new file mode 100644 index 0000000..2a31486 --- /dev/null +++ b/apps/api/src/common/guards/jwt-refresh.guard.ts @@ -0,0 +1,49 @@ +import { DRIZZLE_PROVIDER } from '@/database/drizzle-provider'; +import { + CanActivate, + ExecutionContext, + Inject, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { eq } from 'drizzle-orm'; +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { Request } from 'express'; +import * as schema from 'src/database/index'; +import { envs } from '../config/envs'; + +@Injectable() +export class JwtRefreshGuard implements CanActivate { + constructor( + private jwtService: JwtService, + @Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const token = this.extractTokenFromHeader(request); + if (!token) { + throw new UnauthorizedException(); + } + try { + request.user = await this.jwtService.verifyAsync(token, { + secret: envs.refresh_token_secret, + }); + } catch { + const session = await this.drizzle + .select() + .from(schema.sessions) + .where(eq(schema.sessions, token)); + if (session.length === 0) { + throw new UnauthorizedException('Invalid Refresh Token'); + } + } + return true; + } + + private extractTokenFromHeader(request: Request): string | undefined { + const [type, token] = request.headers.authorization?.split(' ') ?? []; + return type === 'Bearer' ? token : undefined; + } +} diff --git a/apps/api/src/common/guards/roles.guard.ts b/apps/api/src/common/guards/roles.guard.ts new file mode 100644 index 0000000..474c655 --- /dev/null +++ b/apps/api/src/common/guards/roles.guard.ts @@ -0,0 +1,47 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { ROLES_KEY } from '../decorators/roles.decorator'; +import { IS_PUBLIC_KEY } from '../decorators'; + +@Injectable() +export class RolesGuard implements CanActivate { + constructor( + private reflector: Reflector, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ + context.getHandler(), + context.getClass(), + ]); + + if (isPublic) { + return true; + } + + const requiredRoles = this.reflector.getAllAndOverride(ROLES_KEY, [ + context.getHandler(), + context.getClass(), + ]); + + if (!requiredRoles || requiredRoles.length === 0) { + return true; + } + + const { user } = context.switchToHttp().getRequest(); + + if (!user) { + return false; + } + + // Si el usuario es SUPERADMIN, permitir acceso sin verificar más + if (user.isSuperAdmin) { + return true; + } + + // Verificar si el usuario tiene alguno de los roles requeridos + return requiredRoles.some(role => + user.roles.includes(role) + ); + } +} diff --git a/apps/api/src/common/interceptors/index.ts b/apps/api/src/common/interceptors/index.ts new file mode 100644 index 0000000..3cc9876 --- /dev/null +++ b/apps/api/src/common/interceptors/index.ts @@ -0,0 +1 @@ +export * from './req-log.interceptor'; diff --git a/apps/api/src/common/interceptors/req-log.interceptor.ts b/apps/api/src/common/interceptors/req-log.interceptor.ts new file mode 100644 index 0000000..17646ee --- /dev/null +++ b/apps/api/src/common/interceptors/req-log.interceptor.ts @@ -0,0 +1,36 @@ +import { concatStr } from '@/common/utils'; +import { + CallHandler, + ExecutionContext, + Injectable, + Logger, + NestInterceptor, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; + +@Injectable() +export class ReqLogInterceptor implements NestInterceptor { + private readonly logger: Logger; + constructor() { + this.logger = new Logger('REQUEST INTERCEPTOR', { timestamp: true }); + } + intercept(context: ExecutionContext, next: CallHandler): Observable { + const req = context.switchToHttp().getRequest(); + const res = context.switchToHttp().getResponse(); + /* * + * Before the request is handled, log the request details + * */ + this.logger.log(concatStr([req.method, req.originalUrl])); + return next.handle().pipe( + tap(() => + /* * + * After the request is handled, log the response details + * */ + this.logger.log( + concatStr([req.method, req.originalUrl, res.statusCode]), + ), + ), + ); + } +} diff --git a/apps/api/src/common/middlewares/index.ts b/apps/api/src/common/middlewares/index.ts new file mode 100644 index 0000000..6eda966 --- /dev/null +++ b/apps/api/src/common/middlewares/index.ts @@ -0,0 +1 @@ +export * from './logger.middleware'; diff --git a/apps/api/src/common/middlewares/logger.middleware.ts b/apps/api/src/common/middlewares/logger.middleware.ts new file mode 100644 index 0000000..9774457 --- /dev/null +++ b/apps/api/src/common/middlewares/logger.middleware.ts @@ -0,0 +1,15 @@ +import { concatStr } from '@/common/utils'; +import { Injectable, Logger, NestMiddleware } from '@nestjs/common'; +import { NextFunction, Request, Response } from 'express'; + +@Injectable() +export class LoggerMiddleware implements NestMiddleware { + constructor(private readonly logger: Logger) {} + use(req: Request, res: Response, next: NextFunction) { + this.logger.log( + concatStr([req.method, req.originalUrl, res.statusCode]), + 'Request', + ); + next(); + } +} diff --git a/apps/api/src/common/modules/index.ts b/apps/api/src/common/modules/index.ts new file mode 100644 index 0000000..523ba2b --- /dev/null +++ b/apps/api/src/common/modules/index.ts @@ -0,0 +1,3 @@ +export * from './logger.module'; +export * from './node-mailer.module'; +export * from './throttle.module'; diff --git a/apps/api/src/common/modules/logger.module.ts b/apps/api/src/common/modules/logger.module.ts new file mode 100644 index 0000000..8e195a7 --- /dev/null +++ b/apps/api/src/common/modules/logger.module.ts @@ -0,0 +1,24 @@ +import { Env } from '@/common/utils'; +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { LoggerModule as PinoModule } from 'nestjs-pino'; + +@Module({ + imports: [ + PinoModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + pinoHttp: { + quietReqLogger: false, + quietResLogger: false, + transport: { + target: + config.get('NODE_ENV') !== 'production' ? 'pino-pretty' : '', + }, + }, + }), + }), + ], +}) +export class LoggerModule {} diff --git a/apps/api/src/common/modules/node-mailer.module.ts b/apps/api/src/common/modules/node-mailer.module.ts new file mode 100644 index 0000000..8ac82d4 --- /dev/null +++ b/apps/api/src/common/modules/node-mailer.module.ts @@ -0,0 +1,23 @@ +import { Env } from '@/common/utils'; +import { MailerModule } from '@nestjs-modules/mailer'; +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; + +@Module({ + imports: [ + MailerModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + transport: { + service: config.get('MAIL_HOST'), + auth: { + user: config.get('MAIL_USERNAME'), + pass: config.get('MAIL_PASSWORD'), + }, + }, + }), + }), + ], +}) +export class NodeMailerModule {} diff --git a/apps/api/src/common/modules/throttle.module.ts b/apps/api/src/common/modules/throttle.module.ts new file mode 100644 index 0000000..092eebc --- /dev/null +++ b/apps/api/src/common/modules/throttle.module.ts @@ -0,0 +1,28 @@ +import { Module } from '@nestjs/common'; +import { ThrottlerModule } from '@nestjs/throttler'; + +@Module({ + imports: [ + ThrottlerModule.forRoot({ + throttlers: [ + { + name: 'short', + ttl: 1000, // 1 sec + limit: 2, + }, + { + name: 'medium', + ttl: 10000, // 10 sec + limit: 4, + }, + { + name: 'long', + ttl: 60000, // 1 min + limit: 10, + }, + ], + errorMessage: 'Too many requests, please try again later.', + }), + ], +}) +export class ThrottleModule {} diff --git a/apps/api/src/common/pipes/file-size-validator.pipe.ts b/apps/api/src/common/pipes/file-size-validator.pipe.ts new file mode 100644 index 0000000..cd351a4 --- /dev/null +++ b/apps/api/src/common/pipes/file-size-validator.pipe.ts @@ -0,0 +1,38 @@ +import { FileValidator } from '@nestjs/common'; +import { IFile } from '@nestjs/common/pipes/file/interfaces'; + +export interface FileSizeValidatorOptions { + fileSize: number; +} + +/** + * Defines the built-in FileType File Validator. It validates incoming files mime-type + * matching a string or a regular expression. Note that this validator uses a naive strategy + * to check the mime-type and could be fooled if the client provided a file with renamed extension. + * (for instance, renaming a 'malicious.bat' to 'malicious.jpeg'). To handle such security issues + * with more reliability, consider checking against the file's [magic-numbers](https://en.wikipedia.org/wiki/Magic_number_%28programming%29) + * + * @see [File Validators](https://docs.nestjs.com/techniques/file-upload#validators) + * + * @publicApi + */ +export class FileSizeValidatorPipe extends FileValidator< + FileSizeValidatorOptions, + IFile +> { + buildErrorMessage(): string { + return `Max file size is ${(this.validationOptions.fileSize * 0.000001).toFixed()} Mb`; + } + + isValid(file?: IFile): boolean { + if (!this.validationOptions) { + return true; + } + + return ( + !!file && + 'mimetype' in file && + +file.size < this.validationOptions.fileSize + ); + } +} diff --git a/apps/api/src/common/pipes/file-type-validator.pipe.ts b/apps/api/src/common/pipes/file-type-validator.pipe.ts new file mode 100644 index 0000000..99208a9 --- /dev/null +++ b/apps/api/src/common/pipes/file-type-validator.pipe.ts @@ -0,0 +1,38 @@ +import { FileValidator } from '@nestjs/common'; +import { IFile } from '@nestjs/common/pipes/file/interfaces'; + +export interface FileTypeValidatorOptions { + fileType: string[]; +} + +/** + * Defines the built-in FileType File Validator. It validates incoming files mime-type + * matching a string or a regular expression. Note that this validator uses a naive strategy + * to check the mime-type and could be fooled if the client provided a file with renamed extension. + * (for instance, renaming a 'malicious.bat' to 'malicious.jpeg'). To handle such security issues + * with more reliability, consider checking against the file's [magic-numbers](https://en.wikipedia.org/wiki/Magic_number_%28programming%29) + * + * @see [File Validators](https://docs.nestjs.com/techniques/file-upload#validators) + * + * @publicApi + */ +export class FileTypeValidatorPipe extends FileValidator< + FileTypeValidatorOptions, + IFile +> { + buildErrorMessage(): string { + return `File must be ${this.validationOptions.fileType}`; + } + + isValid(file?: IFile): boolean { + if (!this.validationOptions) { + return true; + } + + return ( + !!file && + 'mimetype' in file && + this.validationOptions.fileType.includes(file.mimetype) + ); + } +} diff --git a/apps/api/src/common/pipes/index.ts b/apps/api/src/common/pipes/index.ts new file mode 100644 index 0000000..a9959ba --- /dev/null +++ b/apps/api/src/common/pipes/index.ts @@ -0,0 +1,3 @@ +export * from './file-size-validator.pipe'; +export * from './file-type-validator.pipe'; +export * from './zod-validator.pipe'; diff --git a/apps/api/src/common/pipes/zod-validator.pipe.ts b/apps/api/src/common/pipes/zod-validator.pipe.ts new file mode 100644 index 0000000..7ca2bdd --- /dev/null +++ b/apps/api/src/common/pipes/zod-validator.pipe.ts @@ -0,0 +1,17 @@ +import { BadRequestException, PipeTransform } from '@nestjs/common'; +import { ZodSchema, z } from 'zod'; + +export class ZodValidatorPipe implements PipeTransform { + constructor(private schema: ZodSchema) {} + transform( + value: unknown, + // metadata: ArgumentMetadata, + ): z.infer { + const validateFields = this.schema.safeParse(value); + if (!validateFields.success) + throw new BadRequestException({ + errors: validateFields.error.flatten().fieldErrors, + }); + return validateFields.data; + } +} diff --git a/apps/api/src/common/utils/bcrypt.ts b/apps/api/src/common/utils/bcrypt.ts new file mode 100644 index 0000000..620b865 --- /dev/null +++ b/apps/api/src/common/utils/bcrypt.ts @@ -0,0 +1,16 @@ +import * as bcrypt from 'bcryptjs'; +import { compare, hash } from 'bcryptjs'; + +const hashString = async (password: string): Promise => { + const salt = await bcrypt.genSalt(10); + return hash(password, salt); +}; + +const validateString = async ( + plainPassword: string, + hashedPassword: string, +): Promise => { + return await compare(plainPassword, hashedPassword); +}; + +export { hashString, validateString }; diff --git a/apps/api/src/common/utils/dateTimeUtility.ts b/apps/api/src/common/utils/dateTimeUtility.ts new file mode 100644 index 0000000..0d5e68f --- /dev/null +++ b/apps/api/src/common/utils/dateTimeUtility.ts @@ -0,0 +1,28 @@ +/* eslint-disable prettier/prettier */ +import * as moment from 'moment'; + +export const getExpiry = (cant: number) => { + const createdAt = new Date(); + const expiresAt = moment(createdAt).add(cant, 'days').toDate(); + return expiresAt; +}; + +export const getExpiryCode = (cant: number) => { + const createdAt = new Date(); + const expiresAt = moment(createdAt).add(cant, 'seconds').toDate(); + return expiresAt; +}; + +export function isDateExpired(expiry: Date): boolean { + const expirationDate = new Date(expiry); + const currentDate = new Date(); + return expirationDate.getTime() <= currentDate.getTime(); +} + +export const expirationTimeInSeconds = (cant: number) => { + const currentTimeInMillis = Date.now(); + const iat = Math.floor(currentTimeInMillis / 1000); + const expirationTimeInSeconds = cant * 24 * 60 * 60; + const exp = iat + expirationTimeInSeconds; + return exp; +}; diff --git a/apps/api/src/common/utils/index.ts b/apps/api/src/common/utils/index.ts new file mode 100644 index 0000000..acbe6a8 --- /dev/null +++ b/apps/api/src/common/utils/index.ts @@ -0,0 +1,16 @@ +export const isEmptyObj = (obj: object) => + Object.keys(obj).length === 0 && obj.constructor === Object; + +export const concatStr = ( + strings: (number | string)[], + divider?: string, +): string => strings.join(divider ?? ' '); + +export const getRandomInt = (min: number, max: number) => { + const minCelled = Math.ceil(min), + maxFloored = Math.floor(max); + return Math.floor(Math.random() * (maxFloored - minCelled) + minCelled); // The maximum is exclusive and the minimum is inclusive +}; + +export * from './bcrypt'; +export * from './validateEnv'; diff --git a/apps/api/src/common/utils/validateEnv.ts b/apps/api/src/common/utils/validateEnv.ts new file mode 100644 index 0000000..2a08526 --- /dev/null +++ b/apps/api/src/common/utils/validateEnv.ts @@ -0,0 +1,36 @@ +import { z } from 'zod'; + +export const EnvSchema = z.object({ + HOST: z.string(), + NODE_ENV: z + .enum(['development', 'production', 'test', 'provision']) + .default('development'), + PORT: z + .string() + .default('8000') + .transform((data: any) => +data), + ALLOW_CORS_URL: z.string().url(), + ACCESS_TOKEN_SECRET: z.string().min(10).max(128), + ACCESS_TOKEN_EXPIRATION: z.string().min(1).max(60), + REFRESH_TOKEN_SECRET: z.string().min(10).max(128), + REFRESH_TOKEN_EXPIRATION: z.string().min(1).max(365), + DB_HOST: z.string(), + DB_PORT: z.string(), + DB_USERNAME: z.string(), + DB_PASSWORD: z.string(), + DB_NAME: z.string(), + MAIL_HOST: z.string(), + MAIL_USERNAME: z.string(), + MAIL_PASSWORD: z.string(), + DATABASE_URL: z.string(), +}); + +export type Env = z.infer; + +export const validateEnv = (config: Record): Env => { + const validate = EnvSchema.safeParse(config); + if (!validate.success) { + throw new Error(validate.error.message); + } + return validate.data; +}; diff --git a/apps/api/src/database/drizzle-provider.ts b/apps/api/src/database/drizzle-provider.ts new file mode 100644 index 0000000..2d7a601 --- /dev/null +++ b/apps/api/src/database/drizzle-provider.ts @@ -0,0 +1,22 @@ +import { Provider } from '@nestjs/common'; +import { drizzle, NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { Pool } from 'pg'; +import * as schema from './index'; +import { envs } from 'src/common/config/envs'; + +export const DRIZZLE_PROVIDER = 'DRIZZLE_PROVIDER'; + +export type DrizzleDatabase = NodePgDatabase; + +export const DrizzleProvider: Provider = { + provide: DRIZZLE_PROVIDER, + useFactory: () => { + const pool = new Pool({ + connectionString: envs.dataBaseUrl, + ssl: + envs.node_env === 'production' ? { rejectUnauthorized: false } : false, + }); + + return drizzle(pool, { schema }) as DrizzleDatabase; + }, +}; diff --git a/apps/api/src/database/drizzle.module.ts b/apps/api/src/database/drizzle.module.ts new file mode 100644 index 0000000..eed98e0 --- /dev/null +++ b/apps/api/src/database/drizzle.module.ts @@ -0,0 +1,10 @@ +import { Global, Module } from '@nestjs/common'; +import { DrizzleProvider } from './drizzle-provider'; + +@Global() +@Module({ + imports: [], + providers: [DrizzleProvider], + exports: [DrizzleProvider], +}) +export class DrizzleModule {} diff --git a/apps/api/src/database/index.ts b/apps/api/src/database/index.ts new file mode 100644 index 0000000..e6715c9 --- /dev/null +++ b/apps/api/src/database/index.ts @@ -0,0 +1,5 @@ + +export * from './schema/activity_logs'; +export * from './schema/auth'; +export * from './schema/general'; +export * from './schema/surveys' diff --git a/apps/api/src/database/migrations/0000_abnormal_lethal_legion.sql b/apps/api/src/database/migrations/0000_abnormal_lethal_legion.sql new file mode 100644 index 0000000..4fa199d --- /dev/null +++ b/apps/api/src/database/migrations/0000_abnormal_lethal_legion.sql @@ -0,0 +1,173 @@ +CREATE SCHEMA "auth"; +--> statement-breakpoint +CREATE TYPE "auth"."gender" AS ENUM('FEMENINO', 'MASCULINO');--> statement-breakpoint +CREATE TYPE "public"."nationality" AS ENUM('VENEZOLANO', 'EXTRANJERO');--> statement-breakpoint +CREATE TYPE "auth"."status" AS ENUM('ACTIVE', 'INACTIVE');--> statement-breakpoint +CREATE TABLE "activity_logs" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" integer, + "type" text NOT NULL, + "description" text NOT NULL, + "timestamp" timestamp DEFAULT now(), + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp (3) +); +--> statement-breakpoint +CREATE TABLE "auth"."roles" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp (3) +); +--> statement-breakpoint +CREATE TABLE "auth"."sessions" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" integer NOT NULL, + "session_token" text NOT NULL, + "expires_at" integer NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp (3) +); +--> statement-breakpoint +CREATE TABLE "auth"."users" ( + "id" serial PRIMARY KEY NOT NULL, + "username" text NOT NULL, + "email" text NOT NULL, + "fullname" text NOT NULL, + "phone" text, + "password" text NOT NULL, + "is_two_factor_enabled" boolean DEFAULT false NOT NULL, + "two_factor_secret" text, + "is_email_verified" boolean DEFAULT false NOT NULL, + "is_active" boolean DEFAULT true NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp (3), + CONSTRAINT "users_username_unique" UNIQUE("username"), + CONSTRAINT "users_email_unique" UNIQUE("email") +); +--> statement-breakpoint +CREATE TABLE "auth"."user_role" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" integer, + "role_id" integer, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp (3) +); +--> statement-breakpoint +CREATE TABLE "auth"."verificationToken" ( + "identifier" text NOT NULL, + "token" text NOT NULL, + "code" integer, + "expires" timestamp NOT NULL, + "ip_address" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE "category_type" ( + "id" serial PRIMARY KEY NOT NULL, + "group" varchar(100) NOT NULL, + "description" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp (3) +); +--> statement-breakpoint +CREATE TABLE "localities" ( + "id" serial PRIMARY KEY NOT NULL, + "state_id" integer NOT NULL, + "municipality_id" integer NOT NULL, + "parish_id" integer NOT NULL, + "name" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp (3), + CONSTRAINT "localities_name_unique" UNIQUE("name") +); +--> statement-breakpoint +CREATE TABLE "municipalities" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "state_id" integer NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp (3) +); +--> statement-breakpoint +CREATE TABLE "parishes" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "municipality_id" integer NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp (3) +); +--> statement-breakpoint +CREATE TABLE "states" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp (3) +); +--> statement-breakpoint +CREATE TABLE "answers_surveys" ( + "id" serial PRIMARY KEY NOT NULL, + "survey_id" integer, + "user_id" integer, + "answers" jsonb NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp (3) +); +--> statement-breakpoint +CREATE TABLE "surveys" ( + "id" serial PRIMARY KEY NOT NULL, + "title" text NOT NULL, + "description" text NOT NULL, + "target_audience" varchar(50) NOT NULL, + "closing_date" date, + "published" boolean NOT NULL, + "questions" jsonb NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp (3) +); +--> statement-breakpoint +ALTER TABLE "activity_logs" ADD CONSTRAINT "activity_logs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "auth"."sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "auth"."user_role" ADD CONSTRAINT "user_role_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "auth"."user_role" ADD CONSTRAINT "user_role_role_id_roles_id_fk" FOREIGN KEY ("role_id") REFERENCES "auth"."roles"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "localities" ADD CONSTRAINT "localities_state_id_states_id_fk" FOREIGN KEY ("state_id") REFERENCES "public"."states"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "localities" ADD CONSTRAINT "localities_municipality_id_municipalities_id_fk" FOREIGN KEY ("municipality_id") REFERENCES "public"."municipalities"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "localities" ADD CONSTRAINT "localities_parish_id_parishes_id_fk" FOREIGN KEY ("parish_id") REFERENCES "public"."parishes"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "municipalities" ADD CONSTRAINT "municipalities_state_id_states_id_fk" FOREIGN KEY ("state_id") REFERENCES "public"."states"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "parishes" ADD CONSTRAINT "parishes_municipality_id_municipalities_id_fk" FOREIGN KEY ("municipality_id") REFERENCES "public"."municipalities"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "answers_surveys" ADD CONSTRAINT "answers_surveys_survey_id_surveys_id_fk" FOREIGN KEY ("survey_id") REFERENCES "public"."surveys"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "answers_surveys" ADD CONSTRAINT "answers_surveys_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "activityLogs_idx" ON "activity_logs" USING btree ("type");--> statement-breakpoint +CREATE INDEX "roles_idx" ON "auth"."roles" USING btree ("name");--> statement-breakpoint +CREATE INDEX "sessions_idx" ON "auth"."sessions" USING btree ("session_token");--> statement-breakpoint +CREATE INDEX "users_idx" ON "auth"."users" USING btree ("username");--> statement-breakpoint +CREATE INDEX "user_role_idx" ON "auth"."user_role" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "category_typeIx0" ON "category_type" USING btree ("group");--> statement-breakpoint +CREATE INDEX "category_typeIx1" ON "category_type" USING btree ("description");--> statement-breakpoint +CREATE UNIQUE INDEX "localities_index_03" ON "localities" USING btree ("state_id","municipality_id","parish_id");--> statement-breakpoint +CREATE INDEX "localities_index_00" ON "localities" USING btree ("state_id");--> statement-breakpoint +CREATE INDEX "localities_index_01" ON "localities" USING btree ("municipality_id");--> statement-breakpoint +CREATE INDEX "localities_index_02" ON "localities" USING btree ("parish_id");--> statement-breakpoint +CREATE INDEX "municipalities_index_00" ON "municipalities" USING btree ("id","name","state_id");--> statement-breakpoint +CREATE INDEX "parishes_index_00" ON "parishes" USING btree ("id","name","municipality_id");--> statement-breakpoint +CREATE INDEX "states_index_00" ON "states" USING btree ("id","name");--> statement-breakpoint +CREATE INDEX "answers_index_00" ON "answers_surveys" USING btree ("answers");--> statement-breakpoint +CREATE INDEX "answers_index_01" ON "answers_surveys" USING btree ("survey_id");--> statement-breakpoint +CREATE INDEX "answers_index_02" ON "answers_surveys" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "surveys_index_00" ON "surveys" USING btree ("title");--> statement-breakpoint +CREATE VIEW "auth"."user_access_view" AS ( + SELECT + u.id AS user_id, + u.username, + u.email, + u.fullname, + r.id AS role_id, + r.name AS role_name +FROM + auth.users u +LEFT JOIN + auth.user_role ur ON u.id = ur.user_id +LEFT JOIN + auth.roles r ON ur.role_id = r.id);--> statement-breakpoint +CREATE VIEW "public"."v_surveys" AS (select s.id as survey_id, s.title, s.description, s.created_at, s.closing_date, s.target_audience, as2.user_id from surveys s +left join answers_surveys as2 on as2.survey_id = s.id +where s.published = true); \ No newline at end of file diff --git a/apps/api/src/database/migrations/0001_massive_kylun.sql b/apps/api/src/database/migrations/0001_massive_kylun.sql new file mode 100644 index 0000000..a9e65de --- /dev/null +++ b/apps/api/src/database/migrations/0001_massive_kylun.sql @@ -0,0 +1,9 @@ +DROP VIEW "public"."v_surveys";--> statement-breakpoint +ALTER TABLE "auth"."users" ADD COLUMN "state" integer;--> statement-breakpoint +ALTER TABLE "auth"."users" ADD COLUMN "municipality" integer;--> statement-breakpoint +ALTER TABLE "auth"."users" ADD COLUMN "parish" integer;--> statement-breakpoint +ALTER TABLE "auth"."users" ADD CONSTRAINT "users_state_states_id_fk" FOREIGN KEY ("state") REFERENCES "public"."states"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "auth"."users" ADD CONSTRAINT "users_municipality_municipalities_id_fk" FOREIGN KEY ("municipality") REFERENCES "public"."municipalities"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "auth"."users" ADD CONSTRAINT "users_parish_parishes_id_fk" FOREIGN KEY ("parish") REFERENCES "public"."parishes"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +CREATE VIEW "public"."v_surveys" AS (select id as survey_id, title, description, created_at, closing_date, target_audience from surveys +where published = true); \ No newline at end of file diff --git a/apps/api/src/database/migrations/meta/0000_snapshot.json b/apps/api/src/database/migrations/meta/0000_snapshot.json new file mode 100644 index 0000000..5e88c5a --- /dev/null +++ b/apps/api/src/database/migrations/meta/0000_snapshot.json @@ -0,0 +1,1306 @@ +{ + "id": "317d79fd-7de3-4657-8265-11ddb34b0189", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.activity_logs": { + "name": "activity_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "activityLogs_idx": { + "name": "activityLogs_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "activity_logs_user_id_users_id_fk": { + "name": "activity_logs_user_id_users_id_fk", + "tableFrom": "activity_logs", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.roles": { + "name": "roles", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "roles_idx": { + "name": "roles_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.sessions": { + "name": "sessions", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sessions_idx": { + "name": "sessions_idx", + "columns": [ + { + "expression": "session_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.users": { + "name": "users", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fullname": { + "name": "fullname", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_two_factor_enabled": { + "name": "is_two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "two_factor_secret": { + "name": "two_factor_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_email_verified": { + "name": "is_email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "users_idx": { + "name": "users_idx", + "columns": [ + { + "expression": "username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.user_role": { + "name": "user_role", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_role_idx": { + "name": "user_role_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_role_user_id_users_id_fk": { + "name": "user_role_user_id_users_id_fk", + "tableFrom": "user_role", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_role_role_id_roles_id_fk": { + "name": "user_role_role_id_roles_id_fk", + "tableFrom": "user_role", + "tableTo": "roles", + "schemaTo": "auth", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.verificationToken": { + "name": "verificationToken", + "schema": "auth", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.category_type": { + "name": "category_type", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "group": { + "name": "group", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "category_typeIx0": { + "name": "category_typeIx0", + "columns": [ + { + "expression": "group", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "category_typeIx1": { + "name": "category_typeIx1", + "columns": [ + { + "expression": "description", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.localities": { + "name": "localities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "state_id": { + "name": "state_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "municipality_id": { + "name": "municipality_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parish_id": { + "name": "parish_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "localities_index_03": { + "name": "localities_index_03", + "columns": [ + { + "expression": "state_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parish_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "localities_index_00": { + "name": "localities_index_00", + "columns": [ + { + "expression": "state_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "localities_index_01": { + "name": "localities_index_01", + "columns": [ + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "localities_index_02": { + "name": "localities_index_02", + "columns": [ + { + "expression": "parish_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "localities_state_id_states_id_fk": { + "name": "localities_state_id_states_id_fk", + "tableFrom": "localities", + "tableTo": "states", + "columnsFrom": [ + "state_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "localities_municipality_id_municipalities_id_fk": { + "name": "localities_municipality_id_municipalities_id_fk", + "tableFrom": "localities", + "tableTo": "municipalities", + "columnsFrom": [ + "municipality_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "localities_parish_id_parishes_id_fk": { + "name": "localities_parish_id_parishes_id_fk", + "tableFrom": "localities", + "tableTo": "parishes", + "columnsFrom": [ + "parish_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "localities_name_unique": { + "name": "localities_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.municipalities": { + "name": "municipalities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_id": { + "name": "state_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "municipalities_index_00": { + "name": "municipalities_index_00", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "municipalities_state_id_states_id_fk": { + "name": "municipalities_state_id_states_id_fk", + "tableFrom": "municipalities", + "tableTo": "states", + "columnsFrom": [ + "state_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.parishes": { + "name": "parishes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "municipality_id": { + "name": "municipality_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "parishes_index_00": { + "name": "parishes_index_00", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "parishes_municipality_id_municipalities_id_fk": { + "name": "parishes_municipality_id_municipalities_id_fk", + "tableFrom": "parishes", + "tableTo": "municipalities", + "columnsFrom": [ + "municipality_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.states": { + "name": "states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "states_index_00": { + "name": "states_index_00", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.answers_surveys": { + "name": "answers_surveys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "survey_id": { + "name": "survey_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "answers": { + "name": "answers", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "answers_index_00": { + "name": "answers_index_00", + "columns": [ + { + "expression": "answers", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "answers_index_01": { + "name": "answers_index_01", + "columns": [ + { + "expression": "survey_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "answers_index_02": { + "name": "answers_index_02", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "answers_surveys_survey_id_surveys_id_fk": { + "name": "answers_surveys_survey_id_surveys_id_fk", + "tableFrom": "answers_surveys", + "tableTo": "surveys", + "columnsFrom": [ + "survey_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "answers_surveys_user_id_users_id_fk": { + "name": "answers_surveys_user_id_users_id_fk", + "tableFrom": "answers_surveys", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.surveys": { + "name": "surveys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_audience": { + "name": "target_audience", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "closing_date": { + "name": "closing_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "published": { + "name": "published", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "questions": { + "name": "questions", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "surveys_index_00": { + "name": "surveys_index_00", + "columns": [ + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "auth.gender": { + "name": "gender", + "schema": "auth", + "values": [ + "FEMENINO", + "MASCULINO" + ] + }, + "public.nationality": { + "name": "nationality", + "schema": "public", + "values": [ + "VENEZOLANO", + "EXTRANJERO" + ] + }, + "auth.status": { + "name": "status", + "schema": "auth", + "values": [ + "ACTIVE", + "INACTIVE" + ] + } + }, + "schemas": { + "auth": "auth" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": { + "auth.user_access_view": { + "columns": { + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "role_name": { + "name": "role_name", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "definition": "\n SELECT\n u.id AS user_id,\n u.username,\n u.email,\n u.fullname,\n r.id AS role_id,\n r.name AS role_name\nFROM\n auth.users u\nLEFT JOIN\n auth.user_role ur ON u.id = ur.user_id \nLEFT JOIN\n auth.roles r ON ur.role_id = r.id", + "name": "user_access_view", + "schema": "auth", + "isExisting": false, + "materialized": false + }, + "public.v_surveys": { + "columns": { + "survey_id": { + "name": "survey_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "closing_date": { + "name": "closing_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "target_audience": { + "name": "target_audience", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "definition": "select s.id as survey_id, s.title, s.description, s.created_at, s.closing_date, s.target_audience, as2.user_id from surveys s\nleft join answers_surveys as2 on as2.survey_id = s.id\nwhere s.published = true", + "name": "v_surveys", + "schema": "public", + "isExisting": false, + "materialized": false + } + }, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/api/src/database/migrations/meta/0001_snapshot.json b/apps/api/src/database/migrations/meta/0001_snapshot.json new file mode 100644 index 0000000..8ea460c --- /dev/null +++ b/apps/api/src/database/migrations/meta/0001_snapshot.json @@ -0,0 +1,1358 @@ +{ + "id": "cdbb3495-688f-4b2d-ab8e-e62e42328fd5", + "prevId": "317d79fd-7de3-4657-8265-11ddb34b0189", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.activity_logs": { + "name": "activity_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "activityLogs_idx": { + "name": "activityLogs_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "activity_logs_user_id_users_id_fk": { + "name": "activity_logs_user_id_users_id_fk", + "tableFrom": "activity_logs", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.roles": { + "name": "roles", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "roles_idx": { + "name": "roles_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.sessions": { + "name": "sessions", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sessions_idx": { + "name": "sessions_idx", + "columns": [ + { + "expression": "session_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.users": { + "name": "users", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fullname": { + "name": "fullname", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "municipality": { + "name": "municipality", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parish": { + "name": "parish", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_two_factor_enabled": { + "name": "is_two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "two_factor_secret": { + "name": "two_factor_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_email_verified": { + "name": "is_email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "users_idx": { + "name": "users_idx", + "columns": [ + { + "expression": "username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_state_states_id_fk": { + "name": "users_state_states_id_fk", + "tableFrom": "users", + "tableTo": "states", + "columnsFrom": [ + "state" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "users_municipality_municipalities_id_fk": { + "name": "users_municipality_municipalities_id_fk", + "tableFrom": "users", + "tableTo": "municipalities", + "columnsFrom": [ + "municipality" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "users_parish_parishes_id_fk": { + "name": "users_parish_parishes_id_fk", + "tableFrom": "users", + "tableTo": "parishes", + "columnsFrom": [ + "parish" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.user_role": { + "name": "user_role", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_role_idx": { + "name": "user_role_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_role_user_id_users_id_fk": { + "name": "user_role_user_id_users_id_fk", + "tableFrom": "user_role", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_role_role_id_roles_id_fk": { + "name": "user_role_role_id_roles_id_fk", + "tableFrom": "user_role", + "tableTo": "roles", + "schemaTo": "auth", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.verificationToken": { + "name": "verificationToken", + "schema": "auth", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.category_type": { + "name": "category_type", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "group": { + "name": "group", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "category_typeIx0": { + "name": "category_typeIx0", + "columns": [ + { + "expression": "group", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "category_typeIx1": { + "name": "category_typeIx1", + "columns": [ + { + "expression": "description", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.localities": { + "name": "localities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "state_id": { + "name": "state_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "municipality_id": { + "name": "municipality_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parish_id": { + "name": "parish_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "localities_index_03": { + "name": "localities_index_03", + "columns": [ + { + "expression": "state_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parish_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "localities_index_00": { + "name": "localities_index_00", + "columns": [ + { + "expression": "state_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "localities_index_01": { + "name": "localities_index_01", + "columns": [ + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "localities_index_02": { + "name": "localities_index_02", + "columns": [ + { + "expression": "parish_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "localities_state_id_states_id_fk": { + "name": "localities_state_id_states_id_fk", + "tableFrom": "localities", + "tableTo": "states", + "columnsFrom": [ + "state_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "localities_municipality_id_municipalities_id_fk": { + "name": "localities_municipality_id_municipalities_id_fk", + "tableFrom": "localities", + "tableTo": "municipalities", + "columnsFrom": [ + "municipality_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "localities_parish_id_parishes_id_fk": { + "name": "localities_parish_id_parishes_id_fk", + "tableFrom": "localities", + "tableTo": "parishes", + "columnsFrom": [ + "parish_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "localities_name_unique": { + "name": "localities_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.municipalities": { + "name": "municipalities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_id": { + "name": "state_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "municipalities_index_00": { + "name": "municipalities_index_00", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "municipalities_state_id_states_id_fk": { + "name": "municipalities_state_id_states_id_fk", + "tableFrom": "municipalities", + "tableTo": "states", + "columnsFrom": [ + "state_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.parishes": { + "name": "parishes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "municipality_id": { + "name": "municipality_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "parishes_index_00": { + "name": "parishes_index_00", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "parishes_municipality_id_municipalities_id_fk": { + "name": "parishes_municipality_id_municipalities_id_fk", + "tableFrom": "parishes", + "tableTo": "municipalities", + "columnsFrom": [ + "municipality_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.states": { + "name": "states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "states_index_00": { + "name": "states_index_00", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.answers_surveys": { + "name": "answers_surveys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "survey_id": { + "name": "survey_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "answers": { + "name": "answers", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "answers_index_00": { + "name": "answers_index_00", + "columns": [ + { + "expression": "answers", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "answers_index_01": { + "name": "answers_index_01", + "columns": [ + { + "expression": "survey_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "answers_index_02": { + "name": "answers_index_02", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "answers_surveys_survey_id_surveys_id_fk": { + "name": "answers_surveys_survey_id_surveys_id_fk", + "tableFrom": "answers_surveys", + "tableTo": "surveys", + "columnsFrom": [ + "survey_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "answers_surveys_user_id_users_id_fk": { + "name": "answers_surveys_user_id_users_id_fk", + "tableFrom": "answers_surveys", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.surveys": { + "name": "surveys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_audience": { + "name": "target_audience", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "closing_date": { + "name": "closing_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "published": { + "name": "published", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "questions": { + "name": "questions", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "surveys_index_00": { + "name": "surveys_index_00", + "columns": [ + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "auth.gender": { + "name": "gender", + "schema": "auth", + "values": [ + "FEMENINO", + "MASCULINO" + ] + }, + "public.nationality": { + "name": "nationality", + "schema": "public", + "values": [ + "VENEZOLANO", + "EXTRANJERO" + ] + }, + "auth.status": { + "name": "status", + "schema": "auth", + "values": [ + "ACTIVE", + "INACTIVE" + ] + } + }, + "schemas": { + "auth": "auth" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": { + "auth.user_access_view": { + "columns": { + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "role_name": { + "name": "role_name", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "definition": "\n SELECT\n u.id AS user_id,\n u.username,\n u.email,\n u.fullname,\n r.id AS role_id,\n r.name AS role_name\nFROM\n auth.users u\nLEFT JOIN\n auth.user_role ur ON u.id = ur.user_id \nLEFT JOIN\n auth.roles r ON ur.role_id = r.id", + "name": "user_access_view", + "schema": "auth", + "isExisting": false, + "materialized": false + }, + "public.v_surveys": { + "columns": { + "survey_id": { + "name": "survey_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "closing_date": { + "name": "closing_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "target_audience": { + "name": "target_audience", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "definition": "select id as survey_id, title, description, created_at, closing_date, target_audience from surveys\nwhere published = true", + "name": "v_surveys", + "schema": "public", + "isExisting": false, + "materialized": false + } + }, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/api/src/database/migrations/meta/_journal.json b/apps/api/src/database/migrations/meta/_journal.json new file mode 100644 index 0000000..0f874c3 --- /dev/null +++ b/apps/api/src/database/migrations/meta/_journal.json @@ -0,0 +1,20 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1743783835462, + "tag": "0000_abnormal_lethal_legion", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1747665408016, + "tag": "0001_massive_kylun", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/apps/api/src/database/schema/activity_logs.ts b/apps/api/src/database/schema/activity_logs.ts new file mode 100644 index 0000000..24c1a23 --- /dev/null +++ b/apps/api/src/database/schema/activity_logs.ts @@ -0,0 +1,32 @@ +import * as t from 'drizzle-orm/pg-core'; +import { index, pgTable } from 'drizzle-orm/pg-core'; +import { sql } from 'drizzle-orm'; +import { users } from './auth'; + +const timestamps = { + created_at: t.timestamp('created_at').defaultNow().notNull(), + updated_at: t + .timestamp('updated_at', { mode: 'date', precision: 3 }) + .$onUpdate(() => new Date()), +}; + +// Tabla de Logs de Actividad +export const activityLogsSystem = pgTable( + 'activity_logs', + { + id: t + .uuid('id') + .primaryKey() + .default(sql`gen_random_uuid()`), + userId: t + .integer('user_id') + .references(() => users.id, { onDelete: 'cascade' }), + type: t.text('type').notNull(), // login, failed + description: t.text('description').notNull(), + timestamp: t.timestamp('timestamp').defaultNow(), + ...timestamps, + }, + (activityLogs) => ({ + activityLogsIdx: index('activityLogs_idx').on(activityLogs.type), + }), +); diff --git a/apps/api/src/database/schema/auth.ts b/apps/api/src/database/schema/auth.ts new file mode 100644 index 0000000..626050b --- /dev/null +++ b/apps/api/src/database/schema/auth.ts @@ -0,0 +1,132 @@ +import * as t from 'drizzle-orm/pg-core'; +import { sql } from 'drizzle-orm'; +import { authSchema } from './schemas'; +import { timestamps } from '../timestamps'; +import { states, municipalities, parishes } from './general'; + + +// Tabla de Usuarios sistema +export const users = authSchema.table( + 'users', + { + id: t.serial('id').primaryKey(), + username: t.text('username').unique().notNull(), + email: t.text('email').unique().notNull(), + fullname: t.text('fullname').notNull(), + phone: t.text('phone'), + password: t.text('password').notNull(), + state: t.integer('state').references(() => states.id, { onDelete: 'set null' }), + municipality: t.integer('municipality').references(() => municipalities.id, { onDelete: 'set null' }), + parish: t.integer('parish').references(() => parishes.id, { onDelete: 'set null' }), + isTwoFactorEnabled: t + .boolean('is_two_factor_enabled') + .notNull() + .default(false), + twoFactorSecret: t.text('two_factor_secret'), + isEmailVerified: t.boolean('is_email_verified').notNull().default(false), + isActive: t.boolean('is_active').notNull().default(true), + ...timestamps, + }, + (users) => ({ + usersIdx: t.index('users_idx').on(users.username), + }), +); + + +// Tabla de Roles +export const roles = authSchema.table( + 'roles', + { + id: t.serial('id').primaryKey(), + name: t.text('name').notNull(), + ...timestamps, + }, + (roles) => ({ + rolesIdx: t.index('roles_idx').on(roles.name), + }), +); + + + +//tabla User_roles +export const usersRole = authSchema.table( + 'user_role', + { + id: t.serial('id').primaryKey(), + userId: t + .integer('user_id') + .references(() => users.id, { onDelete: 'cascade' }), + roleId: t + .integer('role_id') + .references(() => roles.id, { onDelete: 'set null' }), + ...timestamps, + }, + (userRole) => ({ + userRoleIdx: t.index('user_role_idx').on(userRole.userId), + }), +); + +export const userAccessView = authSchema.view('user_access_view', { + userId: t.integer('userId').notNull(), + username: t.text('username').notNull(), + email: t.text('email').notNull(), + fullname: t.text('email').notNull(), + roleId: t.integer('role_id'), + roleName: t.text('role_name'), +}).as(sql` + SELECT + u.id AS user_id, + u.username, + u.email, + u.fullname, + r.id AS role_id, + r.name AS role_name +FROM + auth.users u +LEFT JOIN + auth.user_role ur ON u.id = ur.user_id +LEFT JOIN + auth.roles r ON ur.role_id = r.id`); + + +// Tabla de Sesiones +export const sessions = authSchema.table( + 'sessions', + { + id: t + .uuid('id') + .primaryKey() + .default(sql`gen_random_uuid()`), + userId: t + .integer('user_id') + .references(() => users.id, { onDelete: 'cascade' }) + .notNull(), + sessionToken: t.text('session_token').notNull(), + expiresAt: t.integer('expires_at').notNull(), + ...timestamps, + }, + (sessions) => ({ + sessionsIdx: t.index('sessions_idx').on(sessions.sessionToken), + }), +); + + + +//tabla de tokens de verificación +export const verificationTokens = authSchema.table( + 'verificationToken', + { + identifier: t.text('identifier').notNull(), + token: t.text('token').notNull(), + code: t.integer('code'), + expires: t.timestamp('expires', { mode: 'date' }).notNull(), + ipAddress: t.text('ip_address').notNull(), + }, + (verificationToken) => [ + { + compositePk: t.primaryKey({ + columns: [verificationToken.identifier, verificationToken.token], + }), + }, + ], +); diff --git a/apps/api/src/database/schema/enum.ts b/apps/api/src/database/schema/enum.ts new file mode 100644 index 0000000..2e701da --- /dev/null +++ b/apps/api/src/database/schema/enum.ts @@ -0,0 +1,8 @@ +import { pgEnum } from 'drizzle-orm/pg-core'; +import { authSchema } from './schemas'; +export const statusEnum = authSchema.enum('status', ['ACTIVE', 'INACTIVE']); +export const genderEnum = authSchema.enum('gender', ['FEMENINO', 'MASCULINO']); +export const nationalityEnum = pgEnum('nationality', [ + 'VENEZOLANO', + 'EXTRANJERO', +]); diff --git a/apps/api/src/database/schema/general.ts b/apps/api/src/database/schema/general.ts new file mode 100644 index 0000000..2acb580 --- /dev/null +++ b/apps/api/src/database/schema/general.ts @@ -0,0 +1,114 @@ +import * as t from 'drizzle-orm/pg-core'; +import { timestamps } from '../timestamps'; + + + +//Tabla de Tipo de categorias +export const categoryType = t.pgTable( + 'category_type', + { + id: t.serial('id').primaryKey(), + group: t.varchar('group', { length: 100 }).notNull(), // grupo pertenece + description: t.text('description').notNull(), // name + ...timestamps, + }, + (categoryType) => ({ + categoryTypeIdx0: t.index('category_typeIx0').on(categoryType.group), + categoryTypeIdx1: t.index('category_typeIx1').on(categoryType.description), + }), +); + +// Tabla States +export const states = t.pgTable( + 'states', + { + id: t.serial('id').primaryKey(), + name: t.text('name').notNull(), + ...timestamps, + }, + (states) => ({ + nameIndex: t.index('states_index_00').on(states.id, states.name), + }), +); + +// Tabla Municipalities +export const municipalities = t.pgTable( + 'municipalities', + { + id: t.serial('id').primaryKey(), + name: t.text('name').notNull(), + stateId: t + .integer('state_id') + .notNull() + .references(() => states.id, { onDelete: 'cascade' }), + ...timestamps, + }, + (municipalities) => ({ + nameStateIndex: t + .index('municipalities_index_00') + .on(municipalities.id, municipalities.name, municipalities.stateId), + }), +); + +// Tabla Parishes +export const parishes = t.pgTable( + 'parishes', + { + id: t.serial('id').primaryKey(), + name: t.text('name').notNull(), + municipalityId: t + .integer('municipality_id') + .notNull() + .references(() => municipalities.id, { onDelete: 'cascade' }), + ...timestamps, + }, + (parishes) => ({ + parishIndex: t + .index('parishes_index_00') + .on(parishes.id, parishes.name, parishes.municipalityId), + }), +); + +// Tabla Localities +export const localities = t.pgTable( + 'localities', + { + id: t.serial('id').primaryKey(), + stateId: t + .integer('state_id') + .notNull() + .references(() => states.id, { onDelete: 'cascade' }), + municipalityId: t + .integer('municipality_id') + .notNull() + .references(() => municipalities.id, { onDelete: 'cascade' }), + parishId: t + .integer('parish_id') + .notNull() + .references(() => parishes.id, { onDelete: 'cascade' }), + name: t.text('name').unique().notNull(), + ...timestamps, + }, + (localities) => ({ + uniqueLocalityIndex: t + .uniqueIndex('localities_index_03') + .on(localities.stateId, localities.municipalityId, localities.parishId), + stateIndex: t.index('localities_index_00').on(localities.stateId), + municipalityIndex: t + .index('localities_index_01') + .on(localities.municipalityId), + parishIndex: t.index('localities_index_02').on(localities.parishId), + }), +); + +// // Vista LocalitiesView +// export const localitiesView = t.pgView("localities_view", { +// id: t.integer("id").notNull(), +// stateId: t.integer("state_id"), +// state: t.text("state"), +// municipalityId: t.integer("municipality_id"), +// municipality: t.text("municipality"), +// parishId: t.integer("parish_id"), +// parish: t.text("parish"), +// fullLocation: t.text("full_location"), +// }); diff --git a/apps/api/src/database/schema/schemas.ts b/apps/api/src/database/schema/schemas.ts new file mode 100644 index 0000000..88b8bbd --- /dev/null +++ b/apps/api/src/database/schema/schemas.ts @@ -0,0 +1,4 @@ +//schemas +import * as t from 'drizzle-orm/pg-core'; +export const authSchema = t.pgSchema('auth'); //autenticacion y sessiones usuarios + diff --git a/apps/api/src/database/schema/surveys.ts b/apps/api/src/database/schema/surveys.ts new file mode 100644 index 0000000..ccbede3 --- /dev/null +++ b/apps/api/src/database/schema/surveys.ts @@ -0,0 +1,57 @@ +import * as t from 'drizzle-orm/pg-core'; +import { eq, lt, gte, ne, sql } from 'drizzle-orm'; +import { timestamps } from '../timestamps'; +import { users } from './auth'; + + +// Tabla surveys +export const surveys = t.pgTable( + 'surveys', + { + id: t.serial('id').primaryKey(), + title: t.text('title').notNull(), + description: t.text('description').notNull(), + targetAudience: t.varchar('target_audience', { length: 50 }).notNull(), + closingDate: t.date('closing_date'), + published: t.boolean('published').notNull(), + questions: t.jsonb('questions').notNull(), + ...timestamps, + }, + (surveys) => ({ + surveysIndex: t + .index('surveys_index_00') + .on(surveys.title), + }), +); + +export const answersSurveys = t.pgTable( + 'answers_surveys', + { + id: t.serial('id').primaryKey(), + surveyId: t + .integer('survey_id') + .references(() => surveys.id, { onDelete: 'cascade' }), + userId: t + .integer('user_id') + .references(() => users.id, { onDelete: 'cascade' }), + answers: t.jsonb('answers').notNull(), + ...timestamps, + }, + (answers) => ({ + answersIndex: t.index('answers_index_00').on(answers.answers), + answersIndex01: t.index('answers_index_01').on(answers.surveyId), + answersIndex02: t.index('answers_index_02').on(answers.userId), + }), +); + + + +export const viewSurveys = t.pgView('v_surveys', { + surverId: t.integer('survey_id'), + title: t.text('title'), + description: t.text('description'), + created_at: t.timestamp('created_at'), + closingDate: t.date('closing_date'), + targetAudience: t.varchar('target_audience') +}).as(sql`select id as survey_id, title, description, created_at, closing_date, target_audience from surveys +where published = true`); \ No newline at end of file diff --git a/apps/api/src/database/seeds/admin-role.seed.ts b/apps/api/src/database/seeds/admin-role.seed.ts new file mode 100644 index 0000000..16e3b2f --- /dev/null +++ b/apps/api/src/database/seeds/admin-role.seed.ts @@ -0,0 +1,24 @@ +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import * as schema from '../index'; +import { roles, } from '../index'; + + +export async function seedAdminRole(db: NodePgDatabase) { + console.log('Seeding admin role...'); + + // Insert roles + const roleNames = ['superadmin', 'admin', 'autoridad','manager','user','producers','organization']; + + for (const roleName of roleNames) { + try { + await db.insert(roles).values({ + name: roleName + }).onConflictDoNothing(); + console.log(`Role '${roleName}' created or already exists`); + } catch (error) { + console.error(`Error creating role '${roleName}':`, error); + } + } + + console.log('roles seeded successfully'); +} diff --git a/apps/api/src/database/seeds/index.ts b/apps/api/src/database/seeds/index.ts new file mode 100644 index 0000000..acbeab6 --- /dev/null +++ b/apps/api/src/database/seeds/index.ts @@ -0,0 +1,37 @@ +import { drizzle } from 'drizzle-orm/node-postgres'; +import { Pool } from 'pg'; +import { envs } from 'src/common/config/envs'; +import * as schema from '../index'; +import { seedAdminRole } from './admin-role.seed'; +import { seedUserAdmin } from './user-admin.seed'; +import { seedStates } from './states.seed'; +import { seedMunicipalities } from './municipalities.seed'; +import { seedParishes } from './parishes.seed'; + + +async function main() { + const pool = new Pool({ + connectionString: envs.dataBaseUrl, + ssl: + envs.node_env === 'production' ? { rejectUnauthorized: false } : false, + }); + + const db = drizzle(pool, { schema }); + + try { + // Run seeds in order + await seedStates(db); + await seedMunicipalities(db); + await seedParishes(db); + await seedAdminRole(db); + await seedUserAdmin(db) + + console.log('All seeds completed successfully'); + } catch (error) { + console.error('Error seeding database:', error); + } finally { + await pool.end(); + } +} + +main(); diff --git a/apps/api/src/database/seeds/municipalities.seed.ts b/apps/api/src/database/seeds/municipalities.seed.ts new file mode 100644 index 0000000..e1de996 --- /dev/null +++ b/apps/api/src/database/seeds/municipalities.seed.ts @@ -0,0 +1,25 @@ +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import * as schema from '../index'; +import { municipalities } from '../schema/general'; + + +export async function seedMunicipalities(db: NodePgDatabase) { + console.log('Seeding public municipalities...'); + + // Insert roles + const municipalitiesArray = [{name:'municipio1',stateId:1}, {name:'municipio2',stateId:1}, {name:'municipio3',stateId:2}]; + + for (const item of municipalitiesArray) { + try { + await db.insert(municipalities).values({ + name: item.name, + stateId: item.stateId + }).onConflictDoNothing(); + // console.log(`Municipality '${item}' created or already exists`); + } catch (error) { + console.error(`Error creating municipality '${item.name}':`, error); + } + } + + console.log('All municipalities seeded successfully'); +} diff --git a/apps/api/src/database/seeds/parishes.seed.ts b/apps/api/src/database/seeds/parishes.seed.ts new file mode 100644 index 0000000..8fd28d8 --- /dev/null +++ b/apps/api/src/database/seeds/parishes.seed.ts @@ -0,0 +1,25 @@ +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import * as schema from '../index'; +import { parishes } from '../schema/general'; + + +export async function seedParishes(db: NodePgDatabase) { + console.log('Seeding public parishes...'); + + // Insert roles + const parishesArray = [{name:'parroquia1',municipalityId:1}, {name:'parroquia2',municipalityId:1}, {name:'parroquia3',municipalityId:2}]; + + for (const item of parishesArray) { + try { + await db.insert(parishes).values({ + name: item.name, + municipalityId: item.municipalityId + }).onConflictDoNothing(); + // console.log(`Parish '${item}' created or already exists`); + } catch (error) { + console.error(`Error creating parish '${item.name}':`, error); + } + } + + console.log('All parishes seeded successfully'); +} diff --git a/apps/api/src/database/seeds/states.seed.ts b/apps/api/src/database/seeds/states.seed.ts new file mode 100644 index 0000000..79de885 --- /dev/null +++ b/apps/api/src/database/seeds/states.seed.ts @@ -0,0 +1,24 @@ +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import * as schema from '../index'; +import { states } from '../schema/general'; + + +export async function seedStates(db: NodePgDatabase) { + console.log('Seeding public state...'); + + // Insert roles + const statesArray = ['estado1', 'estado2', 'estado3']; + + for (const item of statesArray) { + try { + await db.insert(states).values({ + name: item + }).onConflictDoNothing(); + // console.log(`State '${item}' created or already exists`); + } catch (error) { + console.error(`Error creating state '${item}':`, error); + } + } + + console.log('All states seeded successfully'); +} diff --git a/apps/api/src/database/seeds/user-admin.seed.ts b/apps/api/src/database/seeds/user-admin.seed.ts new file mode 100644 index 0000000..dcc16de --- /dev/null +++ b/apps/api/src/database/seeds/user-admin.seed.ts @@ -0,0 +1,39 @@ +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import * as schema from '../index'; +import { users, usersRole } from '../index'; + +export async function seedUserAdmin(db: NodePgDatabase) { + + // Insert admin user + try { + // Password is already hashed in your SQL, but in a real application you might want to hash it here + // const hashedPassword = await hash('your_password', 10); + const hashedPassword = '$2b$10$6esl7d/BOINamScuReRoPuYFC8iSJgpk61LHm2X3PCU5hu/St8vHW'; + + const [adminUser] = await db.insert(users).values({ + username: 'superadmin', + email: 'admin@zonastart.com', + fullname: 'Super Administrador', + password: hashedPassword, + state: 1, + municipality: 1, + parish: 1, + isTwoFactorEnabled: false, + isEmailVerified: true, + isActive: true + }).returning({ id: users.id }).onConflictDoNothing(); + + if (adminUser) { + // Assign superadmin role to the user + await db.insert(usersRole).values({ + roleId: 1, // Assuming 'superadmin' has ID 1 based on the insert order + userId: adminUser.id + }).onConflictDoNothing(); + console.log('Admin user created and assigned superadmin role'); + } else { + console.log('Admin user already exists, skipping'); + } + } catch (error) { + console.error('Error creating admin user:', error); + } +} \ No newline at end of file diff --git a/apps/api/src/database/timestamps.ts b/apps/api/src/database/timestamps.ts new file mode 100644 index 0000000..2198878 --- /dev/null +++ b/apps/api/src/database/timestamps.ts @@ -0,0 +1,8 @@ +import * as t from 'drizzle-orm/pg-core'; + +export const timestamps = { + created_at: t.timestamp('created_at').defaultNow().notNull(), + updated_at: t + .timestamp('updated_at', { mode: 'date', precision: 3 }) + .$onUpdate(() => new Date()), +}; \ No newline at end of file diff --git a/apps/api/src/drizzle.config.ts b/apps/api/src/drizzle.config.ts new file mode 100644 index 0000000..1e34e64 --- /dev/null +++ b/apps/api/src/drizzle.config.ts @@ -0,0 +1,13 @@ +import type { Config } from 'drizzle-kit'; +import { envs } from './common/config/envs'; + + +export default { + schema: './src/database/schema/*', // Path to schema file + out: './src/database/migrations', // Path to output directory + dialect: 'postgresql', // Database dialect + schemaFilter: ["public", "auth"], + dbCredentials: { + url: envs.dataBaseUrl, + }, +} satisfies Config; diff --git a/apps/api/src/features/auth/auth.controller.ts b/apps/api/src/features/auth/auth.controller.ts new file mode 100644 index 0000000..b856470 --- /dev/null +++ b/apps/api/src/features/auth/auth.controller.ts @@ -0,0 +1,61 @@ +import { Public } from '@/common/decorators'; +import { JwtRefreshGuard } from '@/common/guards/jwt-refresh.guard'; +import { RefreshTokenDto } from '@/features/auth/dto/refresh-token.dto'; +import { SignInUserDto } from '@/features/auth/dto/signIn-user.dto'; +import { SingUpUserDto } from '@/features/auth/dto/signUp-user.dto'; +import { SignOutUserDto } from '@/features/auth/dto/signOut-user.dto'; +import { + Body, + Controller, + HttpCode, + Patch, + Post, + UseGuards, +} from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; + +@Controller('auth') +export class AuthController { + constructor(private readonly authService: AuthService) {} + + @Public() + @HttpCode(200) + @Post('sing-up') + // @ApiOperation({ summary: 'Create a new user' }) + // @ApiResponse({ status: 201, description: 'User created successfully.' }) + async singUp(@Body() payload: SingUpUserDto) { + const data = await this.authService.singUp(payload) + return { message: 'User created successfully', data}; + // return { message: 'User created successfully', data }; + } + + @Public() + @HttpCode(200) + @Post('sign-in') + async signIn(@Body() signInUserDto: SignInUserDto) { + return await this.authService.signIn(signInUserDto); + } + + @Post('sign-out') + //@RequirePermissions('auth:sign-out') + async signOut(@Body() signOutUserDto: SignOutUserDto) { + await this.authService.signOut(signOutUserDto); + return { message: 'User signed out successfully' }; + } + + // @Post('forgot-password') + // async forgotPassword(@Body() forgotPasswordDto: ForgotPasswordDto) { + // await this.authService.forgotPassword(forgotPasswordDto); + // return { message: 'Password reset link sent to your email' }; + // } + + @UseGuards(JwtRefreshGuard) + @Patch('refresh-token') + //@RequirePermissions('auth:refresh-token') + async refreshToken(@Body() refreshTokenDto: RefreshTokenDto) { + return await this.authService.refreshToken(refreshTokenDto); + } + + +} diff --git a/apps/api/src/features/auth/auth.module.ts b/apps/api/src/features/auth/auth.module.ts new file mode 100644 index 0000000..aa9357c --- /dev/null +++ b/apps/api/src/features/auth/auth.module.ts @@ -0,0 +1,12 @@ +import { MailModule } from '@/features/mail/mail.module'; +import { Module } from '@nestjs/common'; +import { AuthController } from './auth.controller'; +import { AuthService } from './auth.service'; +import { DrizzleModule } from '@/database/drizzle.module'; + +@Module({ + imports: [DrizzleModule, MailModule], + controllers: [AuthController], + providers: [AuthService], +}) +export class AuthModule {} diff --git a/apps/api/src/features/auth/auth.service.ts b/apps/api/src/features/auth/auth.service.ts new file mode 100644 index 0000000..622fd01 --- /dev/null +++ b/apps/api/src/features/auth/auth.service.ts @@ -0,0 +1,368 @@ +import { envs } from '@/common/config/envs'; +import { Env, validateString } from '@/common/utils'; +import { DRIZZLE_PROVIDER } from '@/database/drizzle-provider'; +import { RefreshTokenDto } from '@/features/auth/dto/refresh-token.dto'; +import { SignInUserDto } from '@/features/auth/dto/signIn-user.dto'; +import { SingUpUserDto } from '@/features/auth/dto/signUp-user.dto'; +import { SignOutUserDto } from '@/features/auth/dto/signOut-user.dto'; +import { ValidateUserDto } from '@/features/auth/dto/validate-user.dto'; +import AuthTokensInterface from '@/features/auth/interfaces/auth-tokens.interface'; +import { + LoginUserInterface, + Roles, +} from '@/features/auth/interfaces/login-user.interface'; +import RefreshTokenInterface from '@/features/auth/interfaces/refresh-token.interface'; +import { MailService } from '@/features/mail/mail.service'; +import { User } from '@/features/users/entities/user.entity'; +import { + HttpException, + HttpStatus, + Inject, + Injectable, + NotFoundException, + UnauthorizedException, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { JwtService } from '@nestjs/jwt'; +import crypto from 'crypto'; +import { and, eq, or } from 'drizzle-orm'; +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import * as schema from 'src/database/index'; +import { sessions, users, roles, usersRole } from 'src/database/index'; +import { Session } from './interfaces/session.interface'; +import * as bcrypt from 'bcryptjs'; + +@Injectable() +export class AuthService { + constructor( + private readonly jwtService: JwtService, + private readonly config: ConfigService, + @Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase, + private readonly mailService: MailService, + ) {} + + //Decode Tokens + // Método para decodificar el token y obtener los datos completos + private decodeToken(token: string): { + sub: number; + username?: string; + iat: number; + exp: number; + } { + try { + const decoded = this.jwtService.decode(token) as { + sub: number; + username?: string; + iat: number; + exp: number; + }; + + // Validar que contiene los datos esenciales + if (!decoded || !decoded.exp || !decoded.iat) { + throw new Error('Token lacks required fields'); + } + + return decoded; + } catch (error) { + // Manejo seguro del tipo unknown + let errorMessage = 'Failed to decode token'; + + if (error instanceof Error) { + errorMessage = error.message; + console.error('Error decoding token:', errorMessage); + } else { + console.error('Unknown error type:', error); + } + + throw new HttpException(errorMessage, HttpStatus.UNAUTHORIZED); + } + } + + //Generate Tokens + async generateTokens(user: User): Promise { + const [access_token, refresh_token] = await Promise.all([ + this.jwtService.signAsync( + { + sub: user.id, + username: user.username, + }, + { + secret: envs.access_token_secret, + expiresIn: envs.access_token_expiration, + }, + ), + this.jwtService.signAsync( + { + sub: user.id, + username: user.username, + }, + { + secret: envs.refresh_token_secret, + expiresIn: envs.refresh_token_expiration, + }, + ), + ]); + + return { + access_token, + refresh_token, + }; + } + + //Generate OTP Code For Email Confirmation + async generateOTP(length = 6): Promise { + return crypto + .randomInt(0, 10 ** length) + .toString() + .padStart(length, '0'); + } + + // metodo para crear una session + private async createSession(sessionInput: Session): Promise { + const { userId } = sessionInput; + const activeSessionsCount = await this.drizzle + .select() + .from(sessions) + .where(eq(sessions.userId, parseInt(userId))); + + if (activeSessionsCount.length !== 0) { + // Elimina sessiones viejsas + await this.drizzle + .delete(sessions) + .where(eq(sessions.userId, parseInt(userId))); + } + + const session = await this.drizzle.insert(sessions).values({ + sessionToken: sessionInput.sessionToken, + userId: parseInt(userId), + expiresAt: sessionInput.expiresAt, + }); + if (session.rowCount === 0) throw new HttpException('Failed to create session', HttpStatus.NOT_FOUND); + + return 'Session created successfully'; + } + + //Find User + async findUser(username: string): Promise { + const user = await this.drizzle + .select() + .from(users) + .where(eq(users.username, username)); + return user[0]; + } + + //Find User + async findUserById(id: number): Promise { + const user = await this.drizzle + .select() + .from(users) + .where(eq(users.id, id)); + return user[0]; + } + + //Check User Is Already Exists + async validateUser(dto: ValidateUserDto): Promise { + const user = await this.findUser(dto.username); + if (!user) throw new NotFoundException('User not found'); + const isValid = await validateString( + dto.password, + user?.password as string, + ); + if (!isValid) throw new UnauthorizedException('Invalid credentials'); + return user; + } + + //Find rol user + async findUserRol(id: number): Promise { + const roles = await this.drizzle + .select({ + id: schema.roles.id, + role: schema.roles.name, + }) + .from(schema.usersRole) + .leftJoin(schema.roles, eq(schema.roles.id, schema.usersRole.roleId)) + .where(eq(schema.usersRole.userId, id)); + + if (roles.length === 0) { + throw new NotFoundException('User not found'); + } + + // Aseguramos que no haya valores nulos + return roles.map((role) => ({ + id: role.id ?? 0, // Asignamos un valor por defecto (0) si es null + rol: role.role ?? '', // Asignamos un valor por defecto (cadena vacía) si es null + })); + } + + //Sign In User Account + async signIn(dto: SignInUserDto): Promise { + + const user = await this.validateUser(dto); + const tokens = await this.generateTokens(user); + const decodeAccess = this.decodeToken(tokens.access_token); + const decodeRefresh = this.decodeToken(tokens.refresh_token); + const rol = await this.findUserRol(user?.id as number); + + await this.createSession({ + userId: String(user?.id), // Convert number to string + sessionToken: tokens.refresh_token, + expiresAt: decodeRefresh.exp, + }); + + return { + message: 'User signed in successfully', + user: { + id: user?.id as number, + username: user?.username, + fullname: user?.fullname, + email: user?.email, + rol: rol, + }, + tokens: { + access_token: tokens.access_token, + access_expire_in: decodeAccess.exp, + refresh_token: tokens.refresh_token, + refresh_expire_in: decodeRefresh.exp, + }, + }; + } + + // //Forgot Password + // async forgotPassword(dto: ForgotPasswordDto): Promise { + // const user = await this.findUser(dto.username); + // if (!user) throw new NotFoundException('User not found'); + // const passwordResetToken = await this.generateOTP(); + // user.passwordResetToken = passwordResetToken; + // user.passwordResetTokenExpires = new Date( + // Date.now() + 1000 * 60 * 60 * 24, // 1 day + // ); + // await this.UserRepository.save(user); + // await this.mailService.sendEmail({ + // to: [user.email], + // subject: 'Reset Password', + // html: ForgotPasswordMail({ + // name: user.name, + // code: passwordResetToken, + // }), + // }); + // } + + //Sign Out User Account + async signOut(dto: SignOutUserDto): Promise { + const { user_id } = dto; + const user = await this.drizzle + .select() + .from(users) + .where(eq(users.id, parseInt(user_id))); + if (!user) throw new NotFoundException('User not found'); + await this.drizzle + .delete(sessions) + .where(eq(sessions.userId, parseInt(user_id))); + } + + //Refresh User Access Token + async refreshToken(dto: RefreshTokenDto): Promise { + const { user_id } = dto; + + const session = await this.drizzle + .select() + .from(sessions) + .where( + and( + eq(sessions.userId, user_id) && + eq(sessions.sessionToken, dto.refresh_token), + ), + ); + + if (session.length === 0) throw new NotFoundException('session not found'); + const user = await this.findUserById(dto.user_id); + if (!user) throw new NotFoundException('User not found'); + const tokens = await this.generateTokens(user); + const decodeAccess = this.decodeToken(tokens.access_token); + const decodeRefresh = this.decodeToken(tokens.refresh_token); + await this.drizzle + .update(sessions) + .set({ sessionToken: tokens.refresh_token, expiresAt: decodeRefresh.exp }) + .where(eq(sessions.userId, dto.user_id)); + + return { + access_token: tokens.access_token, + access_expire_in: decodeAccess.exp, + refresh_token: tokens.refresh_token, + refresh_expire_in: decodeRefresh.exp, + }; + } + + async singUp(createUserDto: SingUpUserDto): Promise { + // Check if username or email exists + const data = await this.drizzle + .select({ + id: users.id, + username: users.username, + email: users.email + }) + .from(users) + .where(or(eq(users.username, createUserDto.username), eq(users.email, createUserDto.email))); + + if (data.length > 0) { + if (data[0].username === createUserDto.username) { + throw new HttpException('Username already exists', HttpStatus.BAD_REQUEST); + } + if (data[0].email === createUserDto.email) { + throw new HttpException('Email already exists', HttpStatus.BAD_REQUEST); + } + } + + // Hash the password + const hashedPassword = await bcrypt.hash(createUserDto.password, 10); + + // Start a transaction + return await this.drizzle.transaction(async (tx) => { + // Create the user + const [newUser] = await tx + .insert(users) + .values({ + username: createUserDto.username, + email: createUserDto.email, + password: hashedPassword, + fullname: createUserDto.fullname, + isActive: true, + state: createUserDto.state, + municipality: createUserDto.municipality, + parish: createUserDto.parish, + phone: createUserDto.phone, + isEmailVerified: false, + isTwoFactorEnabled: false, + }) + .returning(); + + // check if user role is admin + const role = createUserDto.role <= 2 ? 5 : createUserDto.role; + + // Assign role to user + await tx.insert(usersRole).values({ + userId: newUser.id, + roleId: role, + }); + + // Return the created user with role + const [userWithRole] = await tx + .select({ + id: users.id, + username: users.username, + email: users.email, + fullname: users.fullname, + phone: users.phone, + isActive: users.isActive, + role: roles.name, + }) + .from(users) + .leftJoin(usersRole, eq(usersRole.userId, users.id)) + .leftJoin(roles, eq(roles.id, usersRole.roleId)) + .where(eq(users.id, newUser.id)); + + return userWithRole; + }) + + } +} diff --git a/apps/api/src/features/auth/dto/change-password.dto.ts b/apps/api/src/features/auth/dto/change-password.dto.ts new file mode 100644 index 0000000..f4eac52 --- /dev/null +++ b/apps/api/src/features/auth/dto/change-password.dto.ts @@ -0,0 +1,22 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; + +export class ChangePasswordDto { + @ApiProperty() + @IsString({ + message: 'Identifier must be a string', + }) + username: string; + + @ApiProperty() + @IsString({ + message: 'Password must be a string', + }) + password: string; + + @ApiProperty() + @IsString({ + message: 'New password must be a string', + }) + newPassword: string; +} diff --git a/apps/api/src/features/auth/dto/confirm-email.dto.ts b/apps/api/src/features/auth/dto/confirm-email.dto.ts new file mode 100644 index 0000000..467d704 --- /dev/null +++ b/apps/api/src/features/auth/dto/confirm-email.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsString, MaxLength, MinLength } from 'class-validator'; + +export class ConfirmEmailDto { + @ApiProperty() + @IsString() + @MaxLength(6) + @MinLength(6) + code: string; + + @ApiProperty() + @IsEmail() + email: string; +} diff --git a/apps/api/src/features/auth/dto/create-user.dto.ts b/apps/api/src/features/auth/dto/create-user.dto.ts new file mode 100644 index 0000000..0c70d6d --- /dev/null +++ b/apps/api/src/features/auth/dto/create-user.dto.ts @@ -0,0 +1,29 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsOptional, IsString } from 'class-validator'; + +export class CreateUserDto { + @ApiProperty() + @IsEmail() + username: string; + + @ApiProperty() + @IsEmail() + email: string; + + @ApiProperty() + @IsEmail() + fullname: string; + + @ApiProperty() + @IsString({ + message: 'Phone must be a string', + }) + @IsOptional() + phone: string; + + @ApiProperty() + @IsString({ + message: 'Password must be a string', + }) + password: string; +} diff --git a/apps/api/src/features/auth/dto/forgot-password.dto.ts b/apps/api/src/features/auth/dto/forgot-password.dto.ts new file mode 100644 index 0000000..62a583d --- /dev/null +++ b/apps/api/src/features/auth/dto/forgot-password.dto.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; + +export class ForgotPasswordDto { + @ApiProperty() + @IsString({ + message: 'Identifier must be a string', + }) + username: string; +} diff --git a/apps/api/src/features/auth/dto/refresh-token.dto.ts b/apps/api/src/features/auth/dto/refresh-token.dto.ts new file mode 100644 index 0000000..dd2e8c1 --- /dev/null +++ b/apps/api/src/features/auth/dto/refresh-token.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNumber, IsString } from 'class-validator'; + +export class RefreshTokenDto { + @ApiProperty() + @IsString({ + message: 'Refresh token must be a string', + }) + refresh_token: string; + + @ApiProperty() + @IsNumber() + user_id: number; +} diff --git a/apps/api/src/features/auth/dto/reset-password.dto.ts b/apps/api/src/features/auth/dto/reset-password.dto.ts new file mode 100644 index 0000000..9c1a38d --- /dev/null +++ b/apps/api/src/features/auth/dto/reset-password.dto.ts @@ -0,0 +1,22 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; + +export class ResetPasswordDto { + @ApiProperty() + @IsString({ + message: 'Identifier must be a string', + }) + username: string; + + @ApiProperty() + @IsString({ + message: 'Reset Token must be a string', + }) + resetToken: string; + + @ApiProperty() + @IsString({ + message: 'New password must be a string', + }) + newPassword: string; +} diff --git a/apps/api/src/features/auth/dto/signIn-user.dto.ts b/apps/api/src/features/auth/dto/signIn-user.dto.ts new file mode 100644 index 0000000..ea186c0 --- /dev/null +++ b/apps/api/src/features/auth/dto/signIn-user.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; + +export class SignInUserDto { + @ApiProperty() + @IsString({ + message: 'Identifier must be a string', + }) + username: string; + + @ApiProperty() + @IsString({ + message: 'Password must be a string', + }) + password: string; +} diff --git a/apps/api/src/features/auth/dto/signOut-user.dto.ts b/apps/api/src/features/auth/dto/signOut-user.dto.ts new file mode 100644 index 0000000..a1b6d38 --- /dev/null +++ b/apps/api/src/features/auth/dto/signOut-user.dto.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; + +export class SignOutUserDto { + @ApiProperty() + @IsString({ + message: 'User Id must be a string', + }) + user_id: string; +} diff --git a/apps/api/src/features/auth/dto/signUp-user.dto.ts b/apps/api/src/features/auth/dto/signUp-user.dto.ts new file mode 100644 index 0000000..20f9c5f --- /dev/null +++ b/apps/api/src/features/auth/dto/signUp-user.dto.ts @@ -0,0 +1,45 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsInt, IsOptional, IsString } from 'class-validator'; + +export class SingUpUserDto { + @ApiProperty() + @IsString() + username: string; + + @ApiProperty() + @IsEmail() + email: string; + + @ApiProperty() + @IsString() + fullname: string; + + @ApiProperty() + @IsString({ + message: 'Phone must be a string', + }) + @IsOptional() + phone: string; + + @ApiProperty() + @IsString({ + message: 'Password must be a string', + }) + password: string; + + @ApiProperty() + @IsInt() + state: number; + + @ApiProperty() + @IsInt() + municipality: number; + + @ApiProperty() + @IsInt() + parish: number; + + @ApiProperty() + @IsInt() + role: number; +} diff --git a/apps/api/src/features/auth/dto/update-refresh-token.dto.ts b/apps/api/src/features/auth/dto/update-refresh-token.dto.ts new file mode 100644 index 0000000..7e9e435 --- /dev/null +++ b/apps/api/src/features/auth/dto/update-refresh-token.dto.ts @@ -0,0 +1,6 @@ +import { User } from '@/features/users/entities/user.entity'; + +export class UpdateRefreshTokenDto { + user: User; + refresh_token: string; +} diff --git a/apps/api/src/features/auth/dto/validate-user.dto.ts b/apps/api/src/features/auth/dto/validate-user.dto.ts new file mode 100644 index 0000000..7233789 --- /dev/null +++ b/apps/api/src/features/auth/dto/validate-user.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; + +export class ValidateUserDto { + @ApiProperty() + @IsString({ + message: 'First name must be a string', + }) + username: string; + + @ApiProperty() + @IsString({ + message: 'Password must be a string', + }) + password: string; +} diff --git a/apps/api/src/features/auth/interfaces/auth-tokens.interface.ts b/apps/api/src/features/auth/interfaces/auth-tokens.interface.ts new file mode 100644 index 0000000..f5f5b17 --- /dev/null +++ b/apps/api/src/features/auth/interfaces/auth-tokens.interface.ts @@ -0,0 +1,6 @@ +interface AuthTokensInterface { + access_token: string; + refresh_token: string; +} + +export default AuthTokensInterface; diff --git a/apps/api/src/features/auth/interfaces/login-user.interface.ts b/apps/api/src/features/auth/interfaces/login-user.interface.ts new file mode 100644 index 0000000..79c9622 --- /dev/null +++ b/apps/api/src/features/auth/interfaces/login-user.interface.ts @@ -0,0 +1,25 @@ +export interface LoginUserInterface { + message: string; + user: User; + tokens: Tokens; +} + +interface User { + id: number; + username: string; + fullname: string; + email?: string; + rol: Roles[]; +} + +export interface Roles { + id: number; + rol: string; +} + +interface Tokens { + access_token: string; + access_expire_in: number; + refresh_token: string; + refresh_expire_in: number; +} diff --git a/apps/api/src/features/auth/interfaces/refresh-token.interface.ts b/apps/api/src/features/auth/interfaces/refresh-token.interface.ts new file mode 100644 index 0000000..9afeb6a --- /dev/null +++ b/apps/api/src/features/auth/interfaces/refresh-token.interface.ts @@ -0,0 +1,8 @@ +interface RefreshTokenInterface { + access_token: string; + access_expire_in: number; + refresh_token: string; + refresh_expire_in: number; +} + +export default RefreshTokenInterface; diff --git a/apps/api/src/features/auth/interfaces/session.interface.ts b/apps/api/src/features/auth/interfaces/session.interface.ts new file mode 100644 index 0000000..d571c92 --- /dev/null +++ b/apps/api/src/features/auth/interfaces/session.interface.ts @@ -0,0 +1,5 @@ +export interface Session { + userId: string; + sessionToken: string; + expiresAt: number; +} diff --git a/apps/api/src/features/configurations/category-types/category-types.controller.ts b/apps/api/src/features/configurations/category-types/category-types.controller.ts new file mode 100644 index 0000000..9c42b99 --- /dev/null +++ b/apps/api/src/features/configurations/category-types/category-types.controller.ts @@ -0,0 +1,96 @@ +import { Roles } from '@/common/decorators'; +import { + Body, + Controller, + Delete, + Get, + Param, + ParseIntPipe, + Patch, + Post, +} from '@nestjs/common'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { CategoryTypesService } from './category-types.service'; +import { CreateCategoryTypeDto } from './dto/create-category-type.dto'; +import { UpdateCategoryTypeDto } from './dto/update-category-type.dto'; +import { CategoryType } from './entities/category-type.entity'; + +@ApiTags('Category Types') +@Controller('configurations/category-types') +export class CategoryTypesController { + constructor(private readonly categoryTypesService: CategoryTypesService) {} + + @Get() + @Roles('admin', 'user') + @ApiOperation({ summary: 'Get all category types' }) + @ApiResponse({ + status: 200, + description: 'Return all category types', + type: [CategoryType], + }) + findAll() { + return this.categoryTypesService.findAll(); + } + + @Get(':id') + @Roles('admin', 'user') + @ApiOperation({ summary: 'Get a category type by id' }) + @ApiResponse({ + status: 200, + description: 'Return a category type', + type: CategoryType, + }) + @ApiResponse({ status: 404, description: 'Category type not found' }) + findOne(@Param('id', ParseIntPipe) id: number) { + return this.categoryTypesService.findOne(id); + } + + @Get('group/:group') + @Roles('admin', 'user') + @ApiOperation({ summary: 'Get category types by group' }) + @ApiResponse({ + status: 200, + description: 'Return category types by group', + type: [CategoryType], + }) + findByGroup(@Param('group') group: string) { + return this.categoryTypesService.findByGroup(group); + } + + @Post() + @Roles('admin') + @ApiOperation({ summary: 'Create a new category type' }) + @ApiResponse({ + status: 201, + description: 'Category type created', + type: CategoryType, + }) + create(@Body() createCategoryTypeDto: CreateCategoryTypeDto) { + return this.categoryTypesService.create(createCategoryTypeDto); + } + + @Patch(':id') + @Roles('admin') + @ApiOperation({ summary: 'Update a category type' }) + @ApiResponse({ + status: 200, + description: 'Category type updated', + type: CategoryType, + }) + @ApiResponse({ status: 404, description: 'Category type not found' }) + update( + @Param('id', ParseIntPipe) id: number, + @Body() updateCategoryTypeDto: UpdateCategoryTypeDto, + ) { + return this.categoryTypesService.update(id, updateCategoryTypeDto); + } + + @Delete(':id') + @Roles('admin') + @ApiOperation({ summary: 'Delete a category type' }) + @ApiResponse({ status: 200, description: 'Category type deleted' }) + @ApiResponse({ status: 404, description: 'Category type not found' }) + remove(@Param('id', ParseIntPipe) id: number) { + return this.categoryTypesService.remove(id); + } +} diff --git a/apps/api/src/features/configurations/category-types/category-types.module.ts b/apps/api/src/features/configurations/category-types/category-types.module.ts new file mode 100644 index 0000000..2a1127a --- /dev/null +++ b/apps/api/src/features/configurations/category-types/category-types.module.ts @@ -0,0 +1,12 @@ +import { DrizzleModule } from '@/database/drizzle.module'; +import { Module } from '@nestjs/common'; +import { CategoryTypesController } from './category-types.controller'; +import { CategoryTypesService } from './category-types.service'; + +@Module({ + imports: [DrizzleModule], + controllers: [CategoryTypesController], + providers: [CategoryTypesService], + exports: [CategoryTypesService], +}) +export class CategoryTypesModule {} diff --git a/apps/api/src/features/configurations/category-types/category-types.service.ts b/apps/api/src/features/configurations/category-types/category-types.service.ts new file mode 100644 index 0000000..f114d08 --- /dev/null +++ b/apps/api/src/features/configurations/category-types/category-types.service.ts @@ -0,0 +1,81 @@ +import { DRIZZLE_PROVIDER } from '@/database/drizzle-provider'; +import * as schema from '@/database/index'; +import { categoryType } from '@/database/schema/general'; +import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { eq } from 'drizzle-orm'; +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { CreateCategoryTypeDto } from './dto/create-category-type.dto'; +import { UpdateCategoryTypeDto } from './dto/update-category-type.dto'; +import { CategoryType } from './entities/category-type.entity'; + +@Injectable() +export class CategoryTypesService { + constructor( + @Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase, + ) {} + + async findAll(): Promise { + return await this.drizzle.select().from(categoryType); + } + + async findOne(id: number): Promise { + const category = await this.drizzle + .select() + .from(categoryType) + .where(eq(categoryType.id, id)); + + if (category.length === 0) { + throw new HttpException('Category type not found', HttpStatus.NOT_FOUND); + } + + return category[0]; + } + + async findByGroup(group: string): Promise { + return await this.drizzle + .select() + .from(categoryType) + .where(eq(categoryType.group, group)); + } + + async create( + createCategoryTypeDto: CreateCategoryTypeDto, + ): Promise { + const [category] = await this.drizzle + .insert(categoryType) + .values({ + group: createCategoryTypeDto.group, + description: createCategoryTypeDto.description, + }) + .returning(); + + return category; + } + + async update( + id: number, + updateCategoryTypeDto: UpdateCategoryTypeDto, + ): Promise { + // Check if category type exists + await this.findOne(id); + + await this.drizzle + .update(categoryType) + .set({ + group: updateCategoryTypeDto.group, + description: updateCategoryTypeDto.description, + }) + .where(eq(categoryType.id, id)); + + return this.findOne(id); + } + + async remove(id: number): Promise<{ message: string }> { + // Check if category type exists + await this.findOne(id); + + await this.drizzle.delete(categoryType).where(eq(categoryType.id, id)); + + return { message: 'Category type deleted successfully' }; + } +} diff --git a/apps/api/src/features/configurations/category-types/dto/create-category-type.dto.ts b/apps/api/src/features/configurations/category-types/dto/create-category-type.dto.ts new file mode 100644 index 0000000..c9c06aa --- /dev/null +++ b/apps/api/src/features/configurations/category-types/dto/create-category-type.dto.ts @@ -0,0 +1,21 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'; + +export class CreateCategoryTypeDto { + @ApiProperty({ + description: 'The group of the category type', + example: 'PAYROLL_TYPE', + }) + @IsNotEmpty() + @IsString() + group: string; + + @ApiProperty({ + description: 'The description of the category type', + example: 'Quincenal', + }) + @IsNotEmpty() + @IsString() + description: string; + +} \ No newline at end of file diff --git a/apps/api/src/features/configurations/category-types/dto/update-category-type.dto.ts b/apps/api/src/features/configurations/category-types/dto/update-category-type.dto.ts new file mode 100644 index 0000000..0b9d2aa --- /dev/null +++ b/apps/api/src/features/configurations/category-types/dto/update-category-type.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateCategoryTypeDto } from './create-category-type.dto'; + +export class UpdateCategoryTypeDto extends PartialType(CreateCategoryTypeDto) {} \ No newline at end of file diff --git a/apps/api/src/features/configurations/category-types/entities/category-type.entity.ts b/apps/api/src/features/configurations/category-types/entities/category-type.entity.ts new file mode 100644 index 0000000..2d9b268 --- /dev/null +++ b/apps/api/src/features/configurations/category-types/entities/category-type.entity.ts @@ -0,0 +1,33 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class CategoryType { + @ApiProperty({ + description: 'The unique identifier of the category type', + example: 1, + }) + id: number; + + @ApiProperty({ + description: 'The group of the category type', + example: 'PAYROLL_TYPE', + }) + group: string; + + @ApiProperty({ + description: 'The description of the category type', + example: 'Quincenal', + }) + description: string; + + @ApiProperty({ + description: 'The date when the category type was created', + example: '2023-01-01T00:00:00.000Z', + }) + created_at?: Date; + + @ApiProperty({ + description: 'The date when the category type was last updated', + example: '2023-01-01T00:00:00.000Z', + }) + updated_at?: Date | null; +} diff --git a/apps/api/src/features/configurations/configurations.module.ts b/apps/api/src/features/configurations/configurations.module.ts new file mode 100644 index 0000000..62db91b --- /dev/null +++ b/apps/api/src/features/configurations/configurations.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { CategoryTypesModule } from './category-types/category-types.module'; +import { MunicipalitiesModule } from './municipalities/municipalities.module'; +import { ParishesModule } from './parishes/parishes.module'; +import { StatesModule } from './states/states.module'; + +@Module({ + imports: [ + StatesModule, + MunicipalitiesModule, + ParishesModule, + CategoryTypesModule, + ], +}) +export class ConfigurationsModule {} diff --git a/apps/api/src/features/configurations/municipalities/dto/create-municipality.dto.ts b/apps/api/src/features/configurations/municipalities/dto/create-municipality.dto.ts new file mode 100644 index 0000000..707206b --- /dev/null +++ b/apps/api/src/features/configurations/municipalities/dto/create-municipality.dto.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsNumber, IsString } from 'class-validator'; + +export class CreateMunicipalityDto { + @ApiProperty({ + description: 'The name of the municipality', + example: 'Los Angeles', + }) + @IsNotEmpty() + @IsString() + name: string; + + @ApiProperty({ + description: 'The ID of the state this municipality belongs to', + example: 1, + }) + @IsNotEmpty() + @IsNumber() + stateId: number; +} \ No newline at end of file diff --git a/apps/api/src/features/configurations/municipalities/dto/update-municipality.dto.ts b/apps/api/src/features/configurations/municipalities/dto/update-municipality.dto.ts new file mode 100644 index 0000000..459d32c --- /dev/null +++ b/apps/api/src/features/configurations/municipalities/dto/update-municipality.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateMunicipalityDto } from './create-municipality.dto'; + +export class UpdateMunicipalityDto extends PartialType(CreateMunicipalityDto) {} \ No newline at end of file diff --git a/apps/api/src/features/configurations/municipalities/entities/municipality.entity.ts b/apps/api/src/features/configurations/municipalities/entities/municipality.entity.ts new file mode 100644 index 0000000..2b547be --- /dev/null +++ b/apps/api/src/features/configurations/municipalities/entities/municipality.entity.ts @@ -0,0 +1,39 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class Municipality { + @ApiProperty({ + description: 'The unique identifier of the municipality', + example: 1, + }) + id: number; + + @ApiProperty({ + description: 'The name of the municipality', + example: 'Los Angeles', + }) + name: string; + + @ApiProperty({ + description: 'The ID of the state this municipality belongs to', + example: 1, + }) + stateId: number; + + @ApiProperty({ + description: 'The name of the state this municipality belongs to', + example: 'California', + }) + stateName?: string | null; + + @ApiProperty({ + description: 'The date when the municipality was created', + example: '2023-01-01T00:00:00.000Z', + }) + created_at?: Date | null; + + @ApiProperty({ + description: 'The date when the municipality was last updated', + example: '2023-01-01T00:00:00.000Z', + }) + updated_at?: Date | null; +} diff --git a/apps/api/src/features/configurations/municipalities/municipalities.controller.ts b/apps/api/src/features/configurations/municipalities/municipalities.controller.ts new file mode 100644 index 0000000..3a0e02f --- /dev/null +++ b/apps/api/src/features/configurations/municipalities/municipalities.controller.ts @@ -0,0 +1,97 @@ +import { Roles } from '@/common/decorators'; +import { + Body, + Controller, + Delete, + Get, + Param, + ParseIntPipe, + Patch, + Post, +} from '@nestjs/common'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { CreateMunicipalityDto } from './dto/create-municipality.dto'; +import { UpdateMunicipalityDto } from './dto/update-municipality.dto'; +import { Municipality } from './entities/municipality.entity'; +import { MunicipalitiesService } from './municipalities.service'; + +@ApiTags('Municipalities') +@Controller('configurations/municipalities') +export class MunicipalitiesController { + constructor(private readonly municipalitiesService: MunicipalitiesService) {} + + @Get() + @Roles('admin', 'user') + @ApiOperation({ summary: 'Get all municipalities' }) + @ApiResponse({ + status: 200, + description: 'Return all municipalities', + type: [Municipality], + }) + findAll() { + return this.municipalitiesService.findAll(); + } + + @Get(':id') + @Roles('admin', 'user') + @ApiOperation({ summary: 'Get a municipality by id' }) + @ApiResponse({ + status: 200, + description: 'Return a municipality', + type: Municipality, + }) + @ApiResponse({ status: 404, description: 'Municipality not found' }) + findOne(@Param('id', ParseIntPipe) id: number) { + return this.municipalitiesService.findOne(id); + } + + @Get('state/:stateId') + @Roles('admin', 'user') + @ApiOperation({ summary: 'Get municipalities by state id' }) + @ApiResponse({ + status: 200, + description: 'Return municipalities by state', + type: [Municipality], + }) + @ApiResponse({ status: 404, description: 'State not found' }) + findByState(@Param('stateId', ParseIntPipe) stateId: number) { + return this.municipalitiesService.findByState(stateId); + } + + @Post() + @Roles('ADMIN') + @ApiOperation({ summary: 'Create a new municipality' }) + @ApiResponse({ + status: 201, + description: 'Municipality created', + type: Municipality, + }) + create(@Body() createMunicipalityDto: CreateMunicipalityDto) { + return this.municipalitiesService.create(createMunicipalityDto); + } + + @Patch(':id') + @Roles('ADMIN') + @ApiOperation({ summary: 'Update a municipality' }) + @ApiResponse({ + status: 200, + description: 'Municipality updated', + type: Municipality, + }) + @ApiResponse({ status: 404, description: 'Municipality not found' }) + update( + @Param('id', ParseIntPipe) id: number, + @Body() updateMunicipalityDto: UpdateMunicipalityDto, + ) { + return this.municipalitiesService.update(id, updateMunicipalityDto); + } + + @Delete(':id') + @Roles('ADMIN') + @ApiOperation({ summary: 'Delete a municipality' }) + @ApiResponse({ status: 200, description: 'Municipality deleted' }) + @ApiResponse({ status: 404, description: 'Municipality not found' }) + remove(@Param('id', ParseIntPipe) id: number) { + return this.municipalitiesService.remove(id); + } +} diff --git a/apps/api/src/features/configurations/municipalities/municipalities.module.ts b/apps/api/src/features/configurations/municipalities/municipalities.module.ts new file mode 100644 index 0000000..77048b3 --- /dev/null +++ b/apps/api/src/features/configurations/municipalities/municipalities.module.ts @@ -0,0 +1,13 @@ +import { DrizzleModule } from '@/database/drizzle.module'; +import { Module } from '@nestjs/common'; +import { StatesModule } from '../states/states.module'; +import { MunicipalitiesController } from './municipalities.controller'; +import { MunicipalitiesService } from './municipalities.service'; + +@Module({ + imports: [DrizzleModule, StatesModule], + controllers: [MunicipalitiesController], + providers: [MunicipalitiesService], + exports: [MunicipalitiesService], +}) +export class MunicipalitiesModule {} diff --git a/apps/api/src/features/configurations/municipalities/municipalities.service.ts b/apps/api/src/features/configurations/municipalities/municipalities.service.ts new file mode 100644 index 0000000..ff14acd --- /dev/null +++ b/apps/api/src/features/configurations/municipalities/municipalities.service.ts @@ -0,0 +1,120 @@ +import { DRIZZLE_PROVIDER } from '@/database/drizzle-provider'; +import * as schema from '@/database/index'; +import { municipalities, states } from '@/database/schema/general'; +import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { eq } from 'drizzle-orm'; +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { StatesService } from '../states/states.service'; +import { CreateMunicipalityDto } from './dto/create-municipality.dto'; +import { UpdateMunicipalityDto } from './dto/update-municipality.dto'; +import { Municipality } from './entities/municipality.entity'; + +@Injectable() +export class MunicipalitiesService { + constructor( + @Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase, + private statesService: StatesService, + ) {} + + async findAll(): Promise { + return await this.drizzle + .select({ + id: municipalities.id, + name: municipalities.name, + stateId: municipalities.stateId, + stateName: states.name, + created_at: municipalities.created_at, + updated_at: municipalities.updated_at, + }) + .from(municipalities) + .leftJoin(states, eq(municipalities.stateId, states.id)); + } + + async findOne(id: number): Promise { + const municipality = await this.drizzle + .select({ + id: municipalities.id, + name: municipalities.name, + stateId: municipalities.stateId, + stateName: states.name, + created_at: municipalities.created_at, + updated_at: municipalities.updated_at, + }) + .from(municipalities) + .leftJoin(states, eq(municipalities.stateId, states.id)) + .where(eq(municipalities.id, id)); + + if (municipality.length === 0) { + throw new HttpException('Municipality not found', HttpStatus.NOT_FOUND); + } + + return municipality[0]; + } + + async findByState(stateId: number): Promise { + // Verify state exists + await this.statesService.findOne(stateId); + + return await this.drizzle + .select({ + id: municipalities.id, + name: municipalities.name, + stateId: municipalities.stateId, + stateName: states.name, + created_at: municipalities.created_at, + updated_at: municipalities.updated_at, + }) + .from(municipalities) + .leftJoin(states, eq(municipalities.stateId, states.id)) + .where(eq(municipalities.stateId, stateId)); + } + + async create( + createMunicipalityDto: CreateMunicipalityDto, + ): Promise { + // Verify state exists + await this.statesService.findOne(createMunicipalityDto.stateId); + + const [municipality] = await this.drizzle + .insert(municipalities) + .values({ + name: createMunicipalityDto.name, + stateId: createMunicipalityDto.stateId, + }) + .returning(); + + return this.findOne(municipality.id); + } + + async update( + id: number, + updateMunicipalityDto: UpdateMunicipalityDto, + ): Promise { + // Check if municipality exists + await this.findOne(id); + + // If stateId is provided, verify it exists + if (updateMunicipalityDto.stateId) { + await this.statesService.findOne(updateMunicipalityDto.stateId); + } + + await this.drizzle + .update(municipalities) + .set({ + name: updateMunicipalityDto.name, + stateId: updateMunicipalityDto.stateId, + }) + .where(eq(municipalities.id, id)); + + return this.findOne(id); + } + + async remove(id: number): Promise<{ message: string }> { + // Check if municipality exists + await this.findOne(id); + + await this.drizzle.delete(municipalities).where(eq(municipalities.id, id)); + + return { message: 'Municipality deleted successfully' }; + } +} diff --git a/apps/api/src/features/configurations/parishes/dto/create-parish.dto.ts b/apps/api/src/features/configurations/parishes/dto/create-parish.dto.ts new file mode 100644 index 0000000..e122d76 --- /dev/null +++ b/apps/api/src/features/configurations/parishes/dto/create-parish.dto.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsNumber, IsString } from 'class-validator'; + +export class CreateParishDto { + @ApiProperty({ + description: 'The name of the parish', + example: 'Downtown', + }) + @IsNotEmpty() + @IsString() + name: string; + + @ApiProperty({ + description: 'The ID of the municipality this parish belongs to', + example: 1, + }) + @IsNotEmpty() + @IsNumber() + municipalityId: number; +} \ No newline at end of file diff --git a/apps/api/src/features/configurations/parishes/dto/update-parish.dto.ts b/apps/api/src/features/configurations/parishes/dto/update-parish.dto.ts new file mode 100644 index 0000000..9d680ab --- /dev/null +++ b/apps/api/src/features/configurations/parishes/dto/update-parish.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateParishDto } from './create-parish.dto'; + +export class UpdateParishDto extends PartialType(CreateParishDto) {} \ No newline at end of file diff --git a/apps/api/src/features/configurations/parishes/entities/parish.entity.ts b/apps/api/src/features/configurations/parishes/entities/parish.entity.ts new file mode 100644 index 0000000..06c524c --- /dev/null +++ b/apps/api/src/features/configurations/parishes/entities/parish.entity.ts @@ -0,0 +1,39 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class Parish { + @ApiProperty({ + description: 'The unique identifier of the parish', + example: 1, + }) + id: number; + + @ApiProperty({ + description: 'The name of the parish', + example: 'Downtown', + }) + name: string; + + @ApiProperty({ + description: 'The ID of the municipality this parish belongs to', + example: 1, + }) + municipalityId: number; + + @ApiProperty({ + description: 'The name of the municipality this parish belongs to', + example: 'Los Angeles', + }) + municipalityName?: string | null; + + @ApiProperty({ + description: 'The date when the parish was created', + example: '2023-01-01T00:00:00.000Z', + }) + created_at?: Date | null; + + @ApiProperty({ + description: 'The date when the parish was last updated', + example: '2023-01-01T00:00:00.000Z', + }) + updated_at: Date | null; +} diff --git a/apps/api/src/features/configurations/parishes/parishes.controller.ts b/apps/api/src/features/configurations/parishes/parishes.controller.ts new file mode 100644 index 0000000..4f690b3 --- /dev/null +++ b/apps/api/src/features/configurations/parishes/parishes.controller.ts @@ -0,0 +1,99 @@ +import { Roles } from '@/common/decorators'; +import { + Body, + Controller, + Delete, + Get, + Param, + ParseIntPipe, + Patch, + Post, +} from '@nestjs/common'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { CreateParishDto } from './dto/create-parish.dto'; +import { UpdateParishDto } from './dto/update-parish.dto'; +import { Parish } from './entities/parish.entity'; +import { ParishesService } from './parishes.service'; + +@ApiTags('Parishes') +@Controller('configurations/parishes') +export class ParishesController { + constructor(private readonly parishesService: ParishesService) {} + + @Get() + @Roles('admin', 'user') + @ApiOperation({ summary: 'Get all parishes' }) + @ApiResponse({ + status: 200, + description: 'Return all parishes', + type: [Parish], + }) + findAll() { + return this.parishesService.findAll(); + } + + @Get(':id') + @Roles('admin', 'user') + @ApiOperation({ summary: 'Get a parish by id' }) + @ApiResponse({ + status: 200, + description: 'Return a parish', + type: Parish, + }) + @ApiResponse({ status: 404, description: 'Parish not found' }) + findOne(@Param('id', ParseIntPipe) id: number) { + return this.parishesService.findOne(id); + } + + @Get('municipality/:municipalityId') + @Roles('admin', 'user') + @ApiOperation({ summary: 'Get parishes by municipality id' }) + @ApiResponse({ + status: 200, + description: 'Return parishes by municipality', + type: [Parish], + }) + @ApiResponse({ status: 404, description: 'Municipality not found' }) + findByMunicipality( + @Param('municipalityId', ParseIntPipe) municipalityId: number, + ) { + return this.parishesService.findByMunicipality(municipalityId); + } + + @Post() + @Roles('ADMIN') + @ApiOperation({ summary: 'Create a new parish' }) + @ApiResponse({ + status: 201, + description: 'Parish created', + type: Parish, + }) + create(@Body() createParishDto: CreateParishDto) { + return this.parishesService.create(createParishDto); + } + + @Patch(':id') + @Roles('ADMIN') + @ApiOperation({ summary: 'Update a parish' }) + @ApiResponse({ + status: 200, + description: 'Parish updated', + type: Parish, + }) + @ApiResponse({ status: 404, description: 'Parish not found' }) + update( + @Param('id', ParseIntPipe) id: number, + @Body() updateParishDto: UpdateParishDto, + ) { + return this.parishesService.update(id, updateParishDto); + } + + @Delete(':id') + @Roles('ADMIN') + @ApiOperation({ summary: 'Delete a parish' }) + @ApiResponse({ status: 200, description: 'Parish deleted' }) + @ApiResponse({ status: 404, description: 'Parish not found' }) + remove(@Param('id', ParseIntPipe) id: number) { + return this.parishesService.remove(id); + } +} diff --git a/apps/api/src/features/configurations/parishes/parishes.module.ts b/apps/api/src/features/configurations/parishes/parishes.module.ts new file mode 100644 index 0000000..d490266 --- /dev/null +++ b/apps/api/src/features/configurations/parishes/parishes.module.ts @@ -0,0 +1,13 @@ +import { DrizzleModule } from '@/database/drizzle.module'; +import { Module } from '@nestjs/common'; +import { MunicipalitiesModule } from '../municipalities/municipalities.module'; +import { ParishesController } from './parishes.controller'; +import { ParishesService } from './parishes.service'; + +@Module({ + imports: [DrizzleModule, MunicipalitiesModule], + controllers: [ParishesController], + providers: [ParishesService], + exports: [ParishesService], +}) +export class ParishesModule {} diff --git a/apps/api/src/features/configurations/parishes/parishes.service.ts b/apps/api/src/features/configurations/parishes/parishes.service.ts new file mode 100644 index 0000000..99b4171 --- /dev/null +++ b/apps/api/src/features/configurations/parishes/parishes.service.ts @@ -0,0 +1,115 @@ +import { DRIZZLE_PROVIDER } from '@/database/drizzle-provider'; +import * as schema from '@/database/index'; +import { municipalities, parishes } from '@/database/schema/general'; +import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { eq } from 'drizzle-orm'; +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { MunicipalitiesService } from '../municipalities/municipalities.service'; +import { CreateParishDto } from './dto/create-parish.dto'; +import { UpdateParishDto } from './dto/update-parish.dto'; +import { Parish } from './entities/parish.entity'; + +@Injectable() +export class ParishesService { + constructor( + @Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase, + private municipalitiesService: MunicipalitiesService, + ) {} + + async findAll(): Promise { + return await this.drizzle + .select({ + id: parishes.id, + name: parishes.name, + municipalityId: parishes.municipalityId, + municipalityName: municipalities.name, + created_at: parishes.created_at, + updated_at: parishes.updated_at, + }) + .from(parishes) + .leftJoin(municipalities, eq(parishes.municipalityId, municipalities.id)); + } + + async findOne(id: number): Promise { + const parish = await this.drizzle + .select({ + id: parishes.id, + name: parishes.name, + municipalityId: parishes.municipalityId, + municipalityName: municipalities.name, + created_at: parishes.created_at, + updated_at: parishes.updated_at, + }) + .from(parishes) + .leftJoin(municipalities, eq(parishes.municipalityId, municipalities.id)) + .where(eq(parishes.id, id)); + + if (parish.length === 0) { + throw new HttpException('Parish not found', HttpStatus.NOT_FOUND); + } + + return parish[0]; + } + + async findByMunicipality(municipalityId: number): Promise { + // Verify municipality exists + await this.municipalitiesService.findOne(municipalityId); + + return await this.drizzle + .select({ + id: parishes.id, + name: parishes.name, + municipalityId: parishes.municipalityId, + municipalityName: municipalities.name, + created_at: parishes.created_at, + updated_at: parishes.updated_at, + }) + .from(parishes) + .leftJoin(municipalities, eq(parishes.municipalityId, municipalities.id)) + .where(eq(parishes.municipalityId, municipalityId)); + } + + async create(createParishDto: CreateParishDto): Promise { + // Verify municipality exists + await this.municipalitiesService.findOne(createParishDto.municipalityId); + + const [parish] = await this.drizzle + .insert(parishes) + .values({ + name: createParishDto.name, + municipalityId: createParishDto.municipalityId, + }) + .returning(); + + return this.findOne(parish.id); + } + + async update(id: number, updateParishDto: UpdateParishDto): Promise { + // Check if parish exists + await this.findOne(id); + + // If municipalityId is provided, verify it exists + if (updateParishDto.municipalityId) { + await this.municipalitiesService.findOne(updateParishDto.municipalityId); + } + + await this.drizzle + .update(parishes) + .set({ + name: updateParishDto.name, + municipalityId: updateParishDto.municipalityId, + }) + .where(eq(parishes.id, id)); + + return this.findOne(id); + } + + async remove(id: number): Promise<{ message: string }> { + // Check if parish exists + await this.findOne(id); + + await this.drizzle.delete(parishes).where(eq(parishes.id, id)); + + return { message: 'Parish deleted successfully' }; + } +} diff --git a/apps/api/src/features/configurations/states/dto/create-state.dto.ts b/apps/api/src/features/configurations/states/dto/create-state.dto.ts new file mode 100644 index 0000000..70df1cd --- /dev/null +++ b/apps/api/src/features/configurations/states/dto/create-state.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class CreateStateDto { + @ApiProperty({ + description: 'The name of the state', + example: 'California', + }) + @IsNotEmpty() + @IsString() + name: string; +} \ No newline at end of file diff --git a/apps/api/src/features/configurations/states/dto/update-state.dto.ts b/apps/api/src/features/configurations/states/dto/update-state.dto.ts new file mode 100644 index 0000000..93aaaee --- /dev/null +++ b/apps/api/src/features/configurations/states/dto/update-state.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateStateDto } from './create-state.dto'; + +export class UpdateStateDto extends PartialType(CreateStateDto) {} \ No newline at end of file diff --git a/apps/api/src/features/configurations/states/entities/state.entity.ts b/apps/api/src/features/configurations/states/entities/state.entity.ts new file mode 100644 index 0000000..5edb454 --- /dev/null +++ b/apps/api/src/features/configurations/states/entities/state.entity.ts @@ -0,0 +1,27 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class State { + @ApiProperty({ + description: 'The unique identifier of the state', + example: 1, + }) + id: number; + + @ApiProperty({ + description: 'The name of the state', + example: 'California', + }) + name: string; + + @ApiProperty({ + description: 'The date when the state was created', + example: '2023-01-01T00:00:00.000Z', + }) + created_at?: Date | null; + + @ApiProperty({ + description: 'The date when the state was last updated', + example: '2023-01-01T00:00:00.000Z', + }) + updated_at?: Date | null; +} diff --git a/apps/api/src/features/configurations/states/states.controller.ts b/apps/api/src/features/configurations/states/states.controller.ts new file mode 100644 index 0000000..cda71c6 --- /dev/null +++ b/apps/api/src/features/configurations/states/states.controller.ts @@ -0,0 +1,68 @@ +import { Roles } from '@/common/decorators'; +import { + Body, + Controller, + Delete, + Get, + Param, + ParseIntPipe, + Patch, + Post, +} from '@nestjs/common'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { UpdateStateDto } from './dto//update-state.dto'; +import { CreateStateDto } from './dto/create-state.dto'; +import { State } from './entities/state.entity'; +import { StatesService } from './states.service'; + +@ApiTags('States') +@Controller('configurations/states') +export class StatesController { + constructor(private readonly statesService: StatesService) {} + + @Get() + @Roles('admin', 'user') + @ApiOperation({ summary: 'Get all states' }) + @ApiResponse({ status: 200, description: 'Return all states', type: [State] }) + findAll() { + return this.statesService.findAll(); + } + + @Get(':id') + @Roles('admin', 'user') + @ApiOperation({ summary: 'Get a state by id' }) + @ApiResponse({ status: 200, description: 'Return a state', type: State }) + @ApiResponse({ status: 404, description: 'State not found' }) + findOne(@Param('id', ParseIntPipe) id: number) { + return this.statesService.findOne(id); + } + + @Post() + @Roles('admin') + @ApiOperation({ summary: 'Create a new state' }) + @ApiResponse({ status: 201, description: 'State created', type: State }) + create(@Body() createStateDto: CreateStateDto) { + return this.statesService.create(createStateDto); + } + + @Patch(':id') + @Roles('admin') + @ApiOperation({ summary: 'Update a state' }) + @ApiResponse({ status: 200, description: 'State updated', type: State }) + @ApiResponse({ status: 404, description: 'State not found' }) + update( + @Param('id', ParseIntPipe) id: number, + @Body() updateStateDto: UpdateStateDto, + ) { + return this.statesService.update(id, updateStateDto); + } + + @Delete(':id') + @Roles('admin') + @ApiOperation({ summary: 'Delete a state' }) + @ApiResponse({ status: 200, description: 'State deleted' }) + @ApiResponse({ status: 404, description: 'State not found' }) + remove(@Param('id', ParseIntPipe) id: number) { + return this.statesService.remove(id); + } +} diff --git a/apps/api/src/features/configurations/states/states.module.ts b/apps/api/src/features/configurations/states/states.module.ts new file mode 100644 index 0000000..414bf64 --- /dev/null +++ b/apps/api/src/features/configurations/states/states.module.ts @@ -0,0 +1,12 @@ +import { DrizzleModule } from '@/database/drizzle.module'; +import { Module } from '@nestjs/common'; +import { StatesController } from './states.controller'; +import { StatesService } from './states.service'; + +@Module({ + imports: [DrizzleModule], + controllers: [StatesController], + providers: [StatesService], + exports: [StatesService], +}) +export class StatesModule {} diff --git a/apps/api/src/features/configurations/states/states.service.ts b/apps/api/src/features/configurations/states/states.service.ts new file mode 100644 index 0000000..ad8c883 --- /dev/null +++ b/apps/api/src/features/configurations/states/states.service.ts @@ -0,0 +1,67 @@ +import { DRIZZLE_PROVIDER } from '@/database/drizzle-provider'; +import * as schema from '@/database/index'; +import { states } from '@/database/schema/general'; +import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { eq } from 'drizzle-orm'; +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { CreateStateDto } from './dto/create-state.dto'; +import { UpdateStateDto } from './dto/update-state.dto'; +import { State } from './entities/state.entity'; + +@Injectable() +export class StatesService { + constructor( + @Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase, + ) {} + + async findAll(): Promise { + return await this.drizzle.select().from(states); + } + + async findOne(id: number): Promise { + const state = await this.drizzle + .select() + .from(states) + .where(eq(states.id, id)); + + if (state.length === 0) { + throw new HttpException('State not found', HttpStatus.NOT_FOUND); + } + + return state[0]; + } + + async create(createStateDto: CreateStateDto): Promise { + const [state] = await this.drizzle + .insert(states) + .values({ + name: createStateDto.name, + }) + .returning(); + + return state; + } + + async update(id: number, updateStateDto: UpdateStateDto): Promise { + // Check if state exists + await this.findOne(id); + + await this.drizzle + .update(states) + .set({ + name: updateStateDto.name, + }) + .where(eq(states.id, id)); + + return this.findOne(id); + } + + async remove(id: number): Promise<{ message: string }> { + // Check if state exists + await this.findOne(id); + + await this.drizzle.delete(states).where(eq(states.id, id)); + + return { message: 'State deleted successfully' }; + } +} diff --git a/apps/api/src/features/location/entities/user.entity.ts b/apps/api/src/features/location/entities/user.entity.ts new file mode 100644 index 0000000..049e84c --- /dev/null +++ b/apps/api/src/features/location/entities/user.entity.ts @@ -0,0 +1,16 @@ +export class State { + id: number; + name: string; +} + +export class Municipality { + id: number; + stateId: number; + name: string; +} + +export class Parish { + id: number; + municipalityId: number; + name: string; +} \ No newline at end of file diff --git a/apps/api/src/features/location/location.controller.ts b/apps/api/src/features/location/location.controller.ts new file mode 100644 index 0000000..cd56acf --- /dev/null +++ b/apps/api/src/features/location/location.controller.ts @@ -0,0 +1,41 @@ +import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common'; +import { UsersService } from './location.service'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { Public } from '@/common/decorators'; +// import { Roles } from '../../common/decorators/roles.decorator'; +// import { PaginationDto } from '../../common/dto/pagination.dto'; +@Public() +@ApiTags('location') +@Controller('location') +export class UsersController { + constructor(private readonly usersService: UsersService) {} + + @Get("state") + // @Roles('admin') + // @ApiOperation({ summary: 'Get all users with pagination and filters' }) + // @ApiResponse({ status: 200, description: 'Return paginated users.' }) + async findState() { + const data = await this.usersService.StateAll(); + return { message: 'Data fetched successfully', data}; + } + + @Get('municipality/:id') + // @Roles('admin') + // @ApiOperation({ summary: 'Get a user by ID' }) + // @ApiResponse({ status: 200, description: 'Return the user.' }) + // @ApiResponse({ status: 404, description: 'User not found.' }) + async findMunicipality(@Param('id') id: string) { + const data = await this.usersService.MunicioalityAll(id); + return { message: 'Data fetched successfully', data }; + } + + @Get('parish/:id') + // @Roles('admin') + // @ApiOperation({ summary: 'Get a user by ID' }) + // @ApiResponse({ status: 200, description: 'Return the user.' }) + // @ApiResponse({ status: 404, description: 'User not found.' }) + async findParish(@Param('id') id: string) { + const data = await this.usersService.ParishAll(id); + return { message: 'Data fetched successfully', data }; + } +} diff --git a/apps/api/src/features/location/location.module.ts b/apps/api/src/features/location/location.module.ts new file mode 100644 index 0000000..0c18d46 --- /dev/null +++ b/apps/api/src/features/location/location.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { UsersController } from './location.controller'; +import { UsersService } from './location.service'; +import { DrizzleModule } from '@/database/drizzle.module'; + +@Module({ + imports: [DrizzleModule], + controllers: [UsersController], + providers: [UsersService], +}) +export class LocationModule {} diff --git a/apps/api/src/features/location/location.service.ts b/apps/api/src/features/location/location.service.ts new file mode 100644 index 0000000..3d3f9ee --- /dev/null +++ b/apps/api/src/features/location/location.service.ts @@ -0,0 +1,44 @@ +import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider'; +// import { Env, validateString } from '@/common/utils'; +import { Inject, Injectable, HttpException, HttpStatus, NotFoundException, UnauthorizedException } from '@nestjs/common'; +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import * as schema from 'src/database/index'; +import { states, municipalities, parishes } from 'src/database/index'; +import { eq, like, or, SQL, sql, and, not } from 'drizzle-orm'; +import * as bcrypt from 'bcryptjs'; +import { State, Municipality, Parish } from './entities/user.entity'; +// import { PaginationDto } from '../../common/dto/pagination.dto'; + +@Injectable() +export class UsersService { + constructor( + @Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase, + ) { } + + async StateAll(): Promise< State[]> { + const find = await this.drizzle + .select() + .from(states) + + return find; + } + + async MunicioalityAll(id: string): Promise< Municipality[]> { + const find = await this.drizzle + .select() + .from(municipalities) + .where(eq(municipalities.stateId, parseInt(id))); + + return find; + } + + async ParishAll(id: string): Promise< Parish[]> { + const find = await this.drizzle + .select() + .from(parishes) + .where(eq(parishes.municipalityId, parseInt(id))); + + return find; + } +} + diff --git a/apps/api/src/features/mail/mail.module.ts b/apps/api/src/features/mail/mail.module.ts new file mode 100644 index 0000000..48c8edc --- /dev/null +++ b/apps/api/src/features/mail/mail.module.ts @@ -0,0 +1,11 @@ +import { MailerModule } from '@nestjs-modules/mailer'; +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { MailService } from './mail.service'; + +@Module({ + imports: [MailerModule, ConfigModule], + providers: [MailService], + exports: [MailService], +}) +export class MailModule {} diff --git a/apps/api/src/features/mail/mail.service.spec.ts b/apps/api/src/features/mail/mail.service.spec.ts new file mode 100644 index 0000000..4297913 --- /dev/null +++ b/apps/api/src/features/mail/mail.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { MailService } from './mail.service'; + +describe('MailService', () => { + let service: MailService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [MailService], + }).compile(); + + service = module.get(MailService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/api/src/features/mail/mail.service.ts b/apps/api/src/features/mail/mail.service.ts new file mode 100644 index 0000000..cad291d --- /dev/null +++ b/apps/api/src/features/mail/mail.service.ts @@ -0,0 +1,18 @@ +import { ISendMailOptions, MailerService } from '@nestjs-modules/mailer'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class MailService { + constructor( + private readonly mailerService: MailerService, + private readonly config: ConfigService, + ) {} + + async sendEmail(mailOptions: ISendMailOptions): Promise { + await this.mailerService.sendMail({ + from: `Turbo NPN <${this.config.get('MAIL_USERNAME')}>`, + ...mailOptions, + }); + } +} diff --git a/apps/api/src/features/mail/templates/change-password.mail.ts b/apps/api/src/features/mail/templates/change-password.mail.ts new file mode 100644 index 0000000..7f6c53a --- /dev/null +++ b/apps/api/src/features/mail/templates/change-password.mail.ts @@ -0,0 +1,177 @@ +const ChangePasswordMail = ({ name }: { name: string }) => { + return ` + + + + + + +
+ Join Turbo NPN +
+ + + + + + + +
+ + + + + + +
+ Vercel +
+

+ Turbo NPN +

+

+ Hello, ${name} +

+

+ Turbo NPN send your sign in notification email +

+
+

+ New Sign In Detected +

+
+
+

+ Turbo NPN is a free and open source prodcut +

+
+ + +`; +}; + +export default ChangePasswordMail; diff --git a/apps/api/src/features/mail/templates/confirm-email.mail.ts b/apps/api/src/features/mail/templates/confirm-email.mail.ts new file mode 100644 index 0000000..926e0ba --- /dev/null +++ b/apps/api/src/features/mail/templates/confirm-email.mail.ts @@ -0,0 +1,177 @@ +const SignInMail = ({ name }: { name: string }) => { + return ` + + + + + + +
+ Join Turbo NPN +
+ + + + + + + +
+ + + + + + +
+ Vercel +
+

+ Turbo NPN +

+

+ Hello, ${name} +

+

+ Turbo NPN send your email verification success message +

+
+

+ Success +

+
+
+

+ Turbo NPN is a free and open source prodcut +

+
+ + +`; +}; + +export default SignInMail; diff --git a/apps/api/src/features/mail/templates/forgot-password.mail.ts b/apps/api/src/features/mail/templates/forgot-password.mail.ts new file mode 100644 index 0000000..e3cfe37 --- /dev/null +++ b/apps/api/src/features/mail/templates/forgot-password.mail.ts @@ -0,0 +1,183 @@ +const ForgotPasswordMail = ({ + name, + code, +}: { + name: string; + code: string | number; +}) => { + return ` + + + + + + +
+ Join Turbo NPN +
+ + + + + + + +
+ + + + + + +
+ Vercel +
+

+ Turbo NPN +

+

+ Hello, ${name} +

+

+ Turbo NPN send your password reset code +

+
+

+ ${code} +

+
+
+

+ Turbo NPN is a free and open source prodcut +

+
+ + +`; +}; + +export default ForgotPasswordMail; diff --git a/apps/api/src/features/mail/templates/index.ts b/apps/api/src/features/mail/templates/index.ts new file mode 100644 index 0000000..dcc2384 --- /dev/null +++ b/apps/api/src/features/mail/templates/index.ts @@ -0,0 +1,195 @@ +const EmailTemplate = ({ + name = '', + action = 'sign in', + timestamp = new Date().toLocaleString(), + location = 'Unknown', + device = 'Unknown Device', + ipAddress = 'Unknown IP', +}) => { + return ` + + + + + + + +
+ Security Alert - New ${action} detected for your Turbo NPN account +
+ + + + + + + +
+ + + + + + +
+ Turbo NPN Logo +
+ +

+ Security Alert +

+ +

+ Hello ${name}, +

+ +

+ We detected a new ${action} to your Turbo NPN account. Here are the details: +

+ +
+ + + + + + + + + + + + + + + + + +
Time:${timestamp}
Location:${location}
Device:${device}
IP Address:${ipAddress}
+
+ + + +

+ If you don't recognize this activity, please contact our support team immediately and change your password. +

+ +
+ +
+

+ Turbo NPN - Secure, Fast, Reliable +

+ + + +

+ © 2025 Turbo NPN. All rights reserved. +

+ +

+ Privacy Policy • + Terms of Service • + Unsubscribe +

+
+
+ + +`; +}; + +export default EmailTemplate; diff --git a/apps/api/src/features/mail/templates/register.mail.ts b/apps/api/src/features/mail/templates/register.mail.ts new file mode 100644 index 0000000..71fa827 --- /dev/null +++ b/apps/api/src/features/mail/templates/register.mail.ts @@ -0,0 +1,183 @@ +const RegisterMail = ({ + name, + code, +}: { + name: string; + code: string | number; +}) => { + return ` + + + + + + +
+ Join Turbo NPN +
+ + + + + + + +
+ + + + + + +
+ Vercel +
+

+ Join Turbo NPN +

+

+ Hello, ${name} +

+

+ Turbo NPN send your email verification code +

+
+

+ ${code} +

+
+
+

+ Turbo NPN is a free and open source prodcut +

+
+ + +`; +}; + +export default RegisterMail; diff --git a/apps/api/src/features/mail/templates/sign-in.mail.ts b/apps/api/src/features/mail/templates/sign-in.mail.ts new file mode 100644 index 0000000..33df94a --- /dev/null +++ b/apps/api/src/features/mail/templates/sign-in.mail.ts @@ -0,0 +1,177 @@ +const SignInMail = ({ name }: { name: string }) => { + return ` + + + + + + +
+ Join Turbo NPN +
+ + + + + + + +
+ + + + + + +
+ Vercel +
+

+ Turbo NPN +

+

+ Hello, ${name} +

+

+ Turbo NPN send your sign in notification email +

+
+

+ New Sign In Detected +

+
+
+

+ Turbo NPN is a free and open source prodcut +

+
+ + +`; +}; + +export default SignInMail; diff --git a/apps/api/src/features/roles/dto/create-role.dto.ts b/apps/api/src/features/roles/dto/create-role.dto.ts new file mode 100644 index 0000000..937abf7 --- /dev/null +++ b/apps/api/src/features/roles/dto/create-role.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsNotEmpty } from 'class-validator'; + +export class CreateRoleDto { + @ApiProperty() + @IsString() + @IsNotEmpty() + name: string; +} \ No newline at end of file diff --git a/apps/api/src/features/roles/dto/update-role.dto.ts b/apps/api/src/features/roles/dto/update-role.dto.ts new file mode 100644 index 0000000..3f80ce5 --- /dev/null +++ b/apps/api/src/features/roles/dto/update-role.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateRoleDto } from './create-role.dto'; + +export class UpdateRoleDto extends PartialType(CreateRoleDto) {} \ No newline at end of file diff --git a/apps/api/src/features/roles/entities/role.entity.ts b/apps/api/src/features/roles/entities/role.entity.ts new file mode 100644 index 0000000..d39451f --- /dev/null +++ b/apps/api/src/features/roles/entities/role.entity.ts @@ -0,0 +1,6 @@ +export class Role { + id: number; + name: string; + createdAt?: Date; + updatedAt?: Date | null; +} diff --git a/apps/api/src/features/roles/roles.controller.ts b/apps/api/src/features/roles/roles.controller.ts new file mode 100644 index 0000000..4ac24bb --- /dev/null +++ b/apps/api/src/features/roles/roles.controller.ts @@ -0,0 +1,59 @@ +import { Roles } from '@/common/decorators/roles.decorator'; +import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { CreateRoleDto } from './dto/create-role.dto'; +import { UpdateRoleDto } from './dto/update-role.dto'; +import { RolesService } from './roles.service'; + +@ApiTags('roles') +@Controller('roles') +export class RolesController { + constructor(private readonly rolesService: RolesService) {} + + @Get() + @Roles('admin') + @ApiOperation({ summary: 'Get all roles' }) + @ApiResponse({ status: 200, description: 'Return all roles.' }) + async findAll() { + const data = await this.rolesService.findAll(); + return { message: 'Roles fetched successfully', data }; + } + + @Get(':id') + @Roles('admin') + @ApiOperation({ summary: 'Get a role by ID' }) + @ApiResponse({ status: 200, description: 'Return the role.' }) + @ApiResponse({ status: 404, description: 'Role not found.' }) + async findOne(@Param('id') id: string) { + const data = await this.rolesService.findOne(+id); + return { message: 'Role fetched successfully', data }; + } + + @Post() + @Roles('admin') + @ApiOperation({ summary: 'Create a new role' }) + @ApiResponse({ status: 201, description: 'Role created successfully.' }) + async create(@Body() createRoleDto: CreateRoleDto) { + const data = await this.rolesService.create(createRoleDto); + return { message: 'Role created successfully', data }; + } + + @Patch(':id') + @Roles('admin') + @ApiOperation({ summary: 'Update a role' }) + @ApiResponse({ status: 200, description: 'Role updated successfully.' }) + @ApiResponse({ status: 404, description: 'Role not found.' }) + async update(@Param('id') id: string, @Body() updateRoleDto: UpdateRoleDto) { + const data = await this.rolesService.update(+id, updateRoleDto); + return { message: 'Role updated successfully', data }; + } + + @Delete(':id') + @Roles('admin') + @ApiOperation({ summary: 'Delete a role' }) + @ApiResponse({ status: 200, description: 'Role deleted successfully.' }) + @ApiResponse({ status: 404, description: 'Role not found.' }) + async remove(@Param('id') id: string) { + return await this.rolesService.remove(+id); + } +} diff --git a/apps/api/src/features/roles/roles.module.ts b/apps/api/src/features/roles/roles.module.ts new file mode 100644 index 0000000..d726a48 --- /dev/null +++ b/apps/api/src/features/roles/roles.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { RolesController } from './roles.controller'; +import { RolesService } from './roles.service'; +import { DrizzleModule } from '@/database/drizzle.module'; + +@Module({ + imports: [DrizzleModule], + controllers: [RolesController], + providers: [RolesService], + exports: [RolesService], +}) +export class RolesModule {} \ No newline at end of file diff --git a/apps/api/src/features/roles/roles.service.ts b/apps/api/src/features/roles/roles.service.ts new file mode 100644 index 0000000..42735d7 --- /dev/null +++ b/apps/api/src/features/roles/roles.service.ts @@ -0,0 +1,67 @@ +import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { eq } from 'drizzle-orm'; +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider'; +import * as schema from 'src/database/index'; +import { roles } from 'src/database/index'; +import { CreateRoleDto } from './dto/create-role.dto'; +import { UpdateRoleDto } from './dto/update-role.dto'; +import { Role } from './entities/role.entity'; + +@Injectable() +export class RolesService { + constructor( + @Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase, + ) {} + + async findAll(): Promise { + return await this.drizzle.select().from(roles); + } + + async findOne(id: number): Promise { + const role = await this.drizzle + .select() + .from(roles) + .where(eq(roles.id, id)); + + if (role.length === 0) { + throw new HttpException('Role not found', HttpStatus.NOT_FOUND); + } + + return role[0]; + } + + async create(createRoleDto: CreateRoleDto): Promise { + const [role] = await this.drizzle + .insert(roles) + .values({ + name: createRoleDto.name, + }) + .returning(); + + return role; + } + + async update(id: number, updateRoleDto: UpdateRoleDto): Promise { + // Check if role exists + await this.findOne(id); + + await this.drizzle + .update(roles) + .set({ + name: updateRoleDto.name, + }) + .where(eq(roles.id, id)); + + return this.findOne(id); + } + + async remove(id: number): Promise<{ message: string }> { + // Check if role exists + await this.findOne(id); + + await this.drizzle.delete(roles).where(eq(roles.id, id)); + + return { message: 'Role deleted successfully' }; + } +} diff --git a/apps/api/src/features/surveys/Untitled-1.json b/apps/api/src/features/surveys/Untitled-1.json new file mode 100644 index 0000000..0e64019 --- /dev/null +++ b/apps/api/src/features/surveys/Untitled-1.json @@ -0,0 +1,76 @@ +[ + { + "id": "q-1", + "type": "simple", + "position": 0, + "question": "Pregunta N° 1 Simple", + "required": true + }, + { + "id": "q-2", + "type": "multiple_choice", + "options": [ + { + "id": "1", + "text": "Opcion Prueba 1" + }, + { + "id": "2", + "text": "Opcion Prueba 2 " + }, + { + "id": "3", + "text": "Opcion Prueba 3" + }, + { + "id": "4", + "text": "Opcion Prueba 4" + } + ], + "position": 1, + "question": "Pregunta de Multiples Opciones N°2 ", + "required": true + }, + { + "id": "q-3", + "type": "single_choice", + "options": [ + { + "id": "1", + "text": "Opcion Unica Prueba 1" + }, + { + "id": "2", + "text": "Opcion Unica Prueba 2" + } + ], + "position": 2, + "question": "Preguntas de una sola opcion N°3 ", + "required": true + }, + { + "id": "q-4", + "type": "select", + "options": [ + { + "id": "1", + "text": "Seleccion 1" + }, + { + "id": "2", + "text": "Seleccion 2 " + }, + { + "id": "3", + "text": "Seleccion 3" + }, + { + "id": "4", + "text": "Seleccion 4" + } + ], + "position": 3, + "question": "Pregunta seleccion N° 4", + "required": true + } +] \ No newline at end of file diff --git a/apps/api/src/features/surveys/dto/create-survey.dto.ts b/apps/api/src/features/surveys/dto/create-survey.dto.ts new file mode 100644 index 0000000..d9380f4 --- /dev/null +++ b/apps/api/src/features/surveys/dto/create-survey.dto.ts @@ -0,0 +1,73 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsArray, IsBoolean, IsDate, IsInt, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator'; + +export class CreateSurveyDto { + @ApiProperty({ description: 'Survey title' }) + @IsString() + @IsNotEmpty() + title: string; + + @ApiProperty({ description: 'Survey description' }) + @IsString() + @IsNotEmpty() + description: string; + + @ApiProperty({ description: 'Target audience' }) + @IsString() + @IsNotEmpty() + targetAudience: string; + + @ApiProperty({ description: 'Closing date' }) + @IsOptional() + @IsDate() + closingDate?: Date; + + @ApiProperty({ description: 'Publication status' }) + @IsBoolean() + published: boolean; + + @ApiProperty({ description: 'Survey questions' }) + @IsArray() + @ValidateNested({ each: true }) // Asegura que cada elemento sea validado individualmente + @Type(() => QuestionDto) // Evita que se envuelva en otro array + questions: any[]; +} + + + +class QuestionDto { + @IsString() + id: string; + + @IsInt() + position: number; + + @IsOptional() + @IsString() + question?: string; + + @IsOptional() + @IsString() + content?: string; + + @IsBoolean() + required: boolean; + + @IsString() + type: string; + + @IsArray() + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => OptionsDto) + options?: OptionsDto[]; +} + +class OptionsDto { + @IsString() + id: string; + + @IsString() + text: string; +} \ No newline at end of file diff --git a/apps/api/src/features/surveys/dto/find-for-user.dto.ts b/apps/api/src/features/surveys/dto/find-for-user.dto.ts new file mode 100644 index 0000000..11024d2 --- /dev/null +++ b/apps/api/src/features/surveys/dto/find-for-user.dto.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsArray, IsNotEmpty } from 'class-validator'; + +export class FindForUserDto { + @ApiProperty({ description: 'Survey rol' }) + @IsArray() + @IsNotEmpty() + rol: any; + +} diff --git a/apps/api/src/features/surveys/dto/response-survey.dto.ts b/apps/api/src/features/surveys/dto/response-survey.dto.ts new file mode 100644 index 0000000..f28a287 --- /dev/null +++ b/apps/api/src/features/surveys/dto/response-survey.dto.ts @@ -0,0 +1,15 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsArray, IsNotEmpty, IsString } from 'class-validator'; + +export class AnswersSurveyDto { + @ApiProperty({ description: 'Survey id' }) + @IsString() + @IsNotEmpty() + surveyId: string; + + @ApiProperty({ description: 'Survey answers' }) + @IsArray() + @IsNotEmpty() + answers: any; + +} diff --git a/apps/api/src/features/surveys/dto/statistics-response.dto.ts b/apps/api/src/features/surveys/dto/statistics-response.dto.ts new file mode 100644 index 0000000..03e4295 --- /dev/null +++ b/apps/api/src/features/surveys/dto/statistics-response.dto.ts @@ -0,0 +1,63 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class QuestionStatDto { + @ApiProperty({ description: 'Question identifier' }) + questionId: string; + + @ApiProperty({ description: 'Question label' }) + label: string; + + @ApiProperty({ description: 'Count of responses for this option' }) + count: number; +} + +export class SurveyDetailDto { + @ApiProperty({ description: 'Survey ID' }) + id: number; + + @ApiProperty({ description: 'Survey title' }) + title: string; + + @ApiProperty({ description: 'Survey description' }) + description: string; + + @ApiProperty({ description: 'Total responses received' }) + totalResponses: number; + + @ApiProperty({ description: 'Target audience' }) + targetAudience: any; + + @ApiProperty({ description: 'Creation date' }) + createdAt: string; + + @ApiProperty({ description: 'Closing date' }) + closingDate?: string | null; + + @ApiProperty({ description: 'Question statistics', type: [QuestionStatDto] }) + // @ApiProperty({ description: 'Question statistics' }) + questionStats: QuestionStatDto[]; + // questionStats: any; +} + +export class SurveyStatisticsResponseDto { + @ApiProperty({ description: 'Total number of surveys' }) + totalSurveys: number; + + @ApiProperty({ description: 'Total number of responses across all surveys' }) + totalResponses: number; + + @ApiProperty({ description: 'Completion rate percentage' }) + completionRate: number; + + @ApiProperty({ description: 'Surveys created by month' }) + surveysByMonth: { month: string; count: number }[]; + + @ApiProperty({ description: 'Responses by audience type' }) + responsesByAudience: { name: any; value: number }[]; + + @ApiProperty({ description: 'Response distribution by survey' }) + responseDistribution: { title: string; responses: number }[]; + + @ApiProperty({ description: 'Detailed statistics for each survey', type: [SurveyDetailDto] }) + surveyDetails: SurveyDetailDto[]; +} \ No newline at end of file diff --git a/apps/api/src/features/surveys/dto/update-survey.dto.ts b/apps/api/src/features/surveys/dto/update-survey.dto.ts new file mode 100644 index 0000000..50d3283 --- /dev/null +++ b/apps/api/src/features/surveys/dto/update-survey.dto.ts @@ -0,0 +1,5 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateSurveyDto } from './create-survey.dto'; + + +export class UpdateSurveyDto extends PartialType(CreateSurveyDto) {} \ No newline at end of file diff --git a/apps/api/src/features/surveys/entities/survey.entity.ts b/apps/api/src/features/surveys/entities/survey.entity.ts new file mode 100644 index 0000000..f329602 --- /dev/null +++ b/apps/api/src/features/surveys/entities/survey.entity.ts @@ -0,0 +1,30 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class Survey { + @ApiProperty({ description: 'Survey ID' }) + id: number; + + @ApiProperty({ description: 'Survey title' }) + title: string; + + @ApiProperty({ description: 'Survey description' }) + description: string; + + @ApiProperty({ description: 'Target audience for the survey' }) + targetAudience: string; + + @ApiProperty({ description: 'Survey closing date' }) + closingDate?: Date; + + @ApiProperty({ description: 'Survey publication status' }) + published: boolean; + + @ApiProperty({ description: 'Survey questions' }) + questions: any[]; + + @ApiProperty({ description: 'Creation date' }) + created_at?: Date; + + @ApiProperty({ description: 'Last update date' }) + updated_at?: Date; +} \ No newline at end of file diff --git a/apps/api/src/features/surveys/surveys.controller.ts b/apps/api/src/features/surveys/surveys.controller.ts new file mode 100644 index 0000000..797295d --- /dev/null +++ b/apps/api/src/features/surveys/surveys.controller.ts @@ -0,0 +1,125 @@ +import { Roles } from '@/common/decorators/roles.decorator'; +import { PaginationDto } from '@/common/dto/pagination.dto'; +import { + Body, + Controller, + Delete, + Get, + Param, + Patch, + Post, + Query, + Req, +} from '@nestjs/common'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { CreateSurveyDto } from './dto/create-survey.dto'; +import { UpdateSurveyDto } from './dto/update-survey.dto'; +import { SurveysService } from './surveys.service'; +import { AnswersSurveyDto } from './dto/response-survey.dto'; +import { Request } from 'express'; +import { FindForUserDto } from './dto/find-for-user.dto'; +import { SurveyStatisticsResponseDto } from './dto/statistics-response.dto'; + +@ApiTags('surveys') +@Controller('surveys') +export class SurveysController { + constructor(private readonly surveysService: SurveysService) {} + + @Get() + @Roles('admin') + @ApiOperation({ summary: 'Get all surveys with pagination and filters' }) + @ApiResponse({ status: 200, description: 'Return paginated surveys.' }) + async findAll(@Query() paginationDto: PaginationDto) { + const result = await this.surveysService.findAll(paginationDto); + return { + message: 'Surveys fetched successfully', + data: result.data, + meta: result.meta, + }; + } + + + + @Post('for-user') + @ApiOperation({ summary: 'Get all surveys with pagination and filters for user' }) + @ApiResponse({ status: 200, description: 'Return paginated surveys for user.' }) + async findAllForUser(@Req() req: Request, @Query() paginationDto: PaginationDto, @Body() findForUserDto: FindForUserDto) { + + const userId = req['user'].id; + + const result = await this.surveysService.findAllForUser(paginationDto, userId, findForUserDto); + + return { + message: 'Surveys fetched successfully', + data: result.data, + meta: result.meta, + }; + } + + @Get('statistics') + @Roles('admin', 'superadmin', 'autoridad') + @ApiOperation({ summary: 'Get survey statistics' }) + + @ApiResponse({ status: 200, description: 'Return survey statistics.'}) + + async getStatistics() { + const data = await this.surveysService.getStatistics(); + return { + message: 'Survey statistics fetched successfully', + data + }; + } + + @Get(':id') + @ApiOperation({ summary: 'Get a survey by ID' }) + @ApiResponse({ status: 200, description: 'Return the survey.' }) + @ApiResponse({ status: 404, description: 'Survey not found.' }) + async findOne(@Param('id') id: string) { + const data = await this.surveysService.findOne(id); + return { message: 'Survey fetched successfully', data }; + } + + @Post() + @Roles('admin') + @ApiOperation({ summary: 'Create a new survey' }) + @ApiResponse({ status: 201, description: 'Survey created successfully.' }) + async create(@Body() createSurveyDto: CreateSurveyDto) { + console.log(createSurveyDto); + + const data = await this.surveysService.create(createSurveyDto); + return { message: 'Survey created successfully', data }; + } + + @Patch(':id') + @Roles('admin') + @ApiOperation({ summary: 'Update a survey' }) + @ApiResponse({ status: 200, description: 'Survey updated successfully.' }) + @ApiResponse({ status: 404, description: 'Survey not found.' }) + async update( + @Param('id') id: string, + @Body() updateSurveyDto: UpdateSurveyDto, + ) { + const data = await this.surveysService.update(id, updateSurveyDto); + return { message: 'Survey updated successfully', data }; + } + + @Delete(':id') + @Roles('admin') + @ApiOperation({ summary: 'Delete a survey' }) + @ApiResponse({ status: 200, description: 'Survey deleted successfully.' }) + @ApiResponse({ status: 404, description: 'Survey not found.' }) + async remove(@Param('id') id: string) { + return await this.surveysService.remove(id); + } + + @Post('answers') + @ApiOperation({ summary: 'Create a new answers' }) + @ApiResponse({ status: 201, description: 'Survey answers successfully.' }) + async answers(@Req() req: Request, @Body() answersSurveyDto: AnswersSurveyDto) { + const userId = (req as any).user?.id; + const data = await this.surveysService.answers(Number(userId),answersSurveyDto); + return { message: 'Survey answers created successfully', data }; + } + + +} \ No newline at end of file diff --git a/apps/api/src/features/surveys/surveys.module.ts b/apps/api/src/features/surveys/surveys.module.ts new file mode 100644 index 0000000..eef0e23 --- /dev/null +++ b/apps/api/src/features/surveys/surveys.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { SurveysService } from './surveys.service'; +import { SurveysController } from './surveys.controller'; + +@Module({ + controllers: [SurveysController], + providers: [SurveysService], + exports: [SurveysService], +}) +export class SurveysModule {} \ No newline at end of file diff --git a/apps/api/src/features/surveys/surveys.service.ts b/apps/api/src/features/surveys/surveys.service.ts new file mode 100644 index 0000000..7a7e6a8 --- /dev/null +++ b/apps/api/src/features/surveys/surveys.service.ts @@ -0,0 +1,535 @@ +import { DRIZZLE_PROVIDER } from '@/database/drizzle-provider'; +import { surveys, answersSurveys, viewSurveys } from '@/database/index'; +import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import * as schema from '@/database/index'; +import { CreateSurveyDto } from './dto/create-survey.dto'; +import { UpdateSurveyDto } from './dto/update-survey.dto'; +import { and, count, eq, ilike, isNull, or, sql } from 'drizzle-orm'; +import { SurveyDetailDto, SurveyStatisticsResponseDto } from './dto/statistics-response.dto'; +import { PaginationDto } from '@/common/dto/pagination.dto'; +import { AnswersSurveyDto } from './dto/response-survey.dto'; +import { FindForUserDto } from './dto/find-for-user.dto'; + +@Injectable() +export class SurveysService { + constructor( + @Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase, + ) { } + + async findAll(paginationDto: PaginationDto) { + const { page = 1, limit = 10, search = '', sortBy = 'id', sortOrder = 'asc' } = paginationDto || {}; + const offset = (page - 1) * limit; + + // Build search condition + const searchCondition = ilike(surveys.title, `%${search}%`); + + + // Build sort condition + const orderBy = sortOrder === 'asc' + ? sql`${surveys[sortBy as keyof typeof surveys]} asc` + : sql`${surveys[sortBy as keyof typeof surveys]} desc`; + + // Get total count for pagination + const totalCountResult = await this.drizzle + .select({ count: sql`count(*)` }) + .from(surveys) + .where(searchCondition); + + const totalCount = Number(totalCountResult[0].count); + const totalPages = Math.ceil(totalCount / limit); + + + // Get paginated data + const data = await this.drizzle + .select() + .from(surveys) + .where(searchCondition) + .orderBy(orderBy) + .limit(limit) + .offset(offset); + + const dataSurvey = data.map((survey) => { + return { + ...survey, + closingDate: survey.closingDate ? new Date(survey.closingDate) : null, + }; + }); + + // Build pagination metadata + const meta = { + page, + limit, + totalCount, + totalPages, + hasNextPage: page < totalPages, + hasPreviousPage: page > 1, + nextPage: page < totalPages ? page + 1 : null, + previousPage: page > 1 ? page - 1 : null, + }; + + return { data: dataSurvey, meta }; + + } + + async findAllForUser(paginationDto: PaginationDto, userId: number, findForUserDto: FindForUserDto) { + const { page = 1, limit = 10, search = '', sortBy = 'created_at', sortOrder = 'asc' } = paginationDto || {}; + const offset = (page - 1) * limit; + + let searchCondition : any = false + + // Build search condition + // if (findForUserDto.rol[0].rol === 'superadmin' || findForUserDto.rol[0].rol == 'admin') { + // searchCondition = and( + // or(eq(viewSurveys.targetAudience, 'producers'), eq(viewSurveys.targetAudience, 'organization'), eq(viewSurveys.targetAudience, 'all')), + // or(eq(viewSurveys.user_id, userId), isNull(viewSurveys.user_id)) + // ); + // } else { + // searchCondition = and( + // or(eq(viewSurveys.targetAudience, findForUserDto.rol[0].rol), eq(viewSurveys.targetAudience, 'all')), + // or(eq(viewSurveys.user_id, userId), isNull(viewSurveys.user_id)) + // ); + // } + + if (findForUserDto.rol[0].rol !== 'superadmin' && findForUserDto.rol[0].rol !== 'admin') { + searchCondition = or(eq(surveys.targetAudience, findForUserDto.rol[0].rol), eq(surveys.targetAudience, 'all')) + } + + // console.log(searchCondition); + + // Build sort condition + const orderBy = sortOrder === 'asc' + ? sql`${surveys[sortBy as keyof typeof surveys]} asc` + : sql`${surveys[sortBy as keyof typeof surveys]} desc`; + + // Get total count for pagination + const totalCountResult = await this.drizzle + .select({ count: sql`count(*)` }) + .from(surveys) + .leftJoin(answersSurveys, and(eq(answersSurveys.surveyId,surveys.id),eq(answersSurveys.userId,userId))) + .where(searchCondition); + + const totalCount = Number(totalCountResult[0].count); + const totalPages = Math.ceil(totalCount / limit); + + // Get paginated data + const data = await this.drizzle + .select() + .from(surveys) + .leftJoin(answersSurveys, and(eq(answersSurveys.surveyId,surveys.id),eq(answersSurveys.userId,userId))) + .where(searchCondition) + .orderBy(orderBy) + .limit(limit) + .offset(offset); + + // Build pagination metadata + const meta = { + page, + limit, + totalCount, + totalPages, + hasNextPage: page < totalPages, + hasPreviousPage: page > 1, + nextPage: page < totalPages ? page + 1 : null, + previousPage: page > 1 ? page - 1 : null, + }; + + return { data, meta }; + } + + async findOne(id: string) { + const survey = await this.drizzle + .select() + .from(surveys) + .where(eq(surveys.id, parseInt(id))); + + if (survey.length === 0) { + throw new HttpException('Survey not found', HttpStatus.NOT_FOUND); + } + + const dataSurvey = survey.map((survey) => { + return { + ...survey, + closingDate: survey.closingDate ? new Date(survey.closingDate) : null, + }; + }); + + return dataSurvey[0]; + } + + async findByTitle(title: string) { + return await this.drizzle + .select() + .from(surveys) + .where(eq(surveys.title, title)); + } + + async create(createSurveyDto: CreateSurveyDto) { + + const find = await this.findByTitle(createSurveyDto.title); + + if (find.length !== 0) { + throw new HttpException('Survey already exists', HttpStatus.BAD_REQUEST); + } + + const survey = await this.drizzle + .insert(surveys) + .values({ + ...createSurveyDto, + closingDate: createSurveyDto.closingDate?.toISOString(), + }) + .returning(); + const dataSurvey = survey.map((survey) => { + return { + ...survey, + closingDate: survey.closingDate ? new Date(survey.closingDate) : null, + }; + }); + + return dataSurvey[0]; + } + + async update(id: string, updateSurveyDto: UpdateSurveyDto) { + + const find = await this.findOne(id) + if (!find) { + throw new HttpException('Survey not found', HttpStatus.BAD_REQUEST) + } + + const find2 = await this.findByTitle(updateSurveyDto.title ?? ''); + + if (find2.length !== 0 && find2[0].id !== parseInt(id)) { + throw new HttpException('Survey already exists', HttpStatus.BAD_REQUEST); + } + + const survey = await this.drizzle + .update(surveys) + .set({ + ...updateSurveyDto, + closingDate: updateSurveyDto.closingDate?.toISOString(), + updated_at: new Date(), + }) + .where(eq(surveys.id, parseInt(id))) + .returning(); + + const dataSurvey = survey.map((survey) => { + return { + ...survey, + closingDate: survey.closingDate ? new Date(survey.closingDate) : null, + }; + }); + + return dataSurvey[0]; + } + + async remove(id: string) { + const find = await this.findOne(id); + if (!find) { + throw new HttpException('Survey not found', HttpStatus.BAD_REQUEST); + } + + await this.drizzle + .delete(surveys) + .where(eq(surveys.id, parseInt(id))); + + return { message: 'Survey deleted successfully' }; + } + + async answers(userId: number, answersSurveyDto: AnswersSurveyDto) { + + const find = await this.drizzle.select() + .from(answersSurveys) + .where(and(eq(answersSurveys.surveyId, Number(answersSurveyDto.surveyId)), (eq(answersSurveys.userId, userId)))); + + + if (find.length !== 0) { + throw new HttpException('Survey answers already exists', HttpStatus.BAD_REQUEST); + } + + const survey = await this.drizzle + .insert(answersSurveys) + .values({ + ...answersSurveyDto, + surveyId: Number(answersSurveyDto.surveyId), + userId: userId, + }) + .returning(); + + return survey[0]; + } + + async getStatistics(): Promise { + // Obtener el número total de encuestas + const totalSurveys = await this.getTotalSurveysCount(); + // Obtener el número total de respuestas + const totalResponses = await this.getTotalResponsesCount(); + + // Calcular la tasa de finalización + const completionRate = totalSurveys > 0 ? Math.round((totalResponses / totalSurveys) * 100) : 0; + + // Obtener las encuestas por mes + const surveysByMonth = await this.getSurveysByMonth(); + + // Obtener las respuestas por audiencia + const responsesByAudience = await this.getResponsesByAudience(); + + // Obtener la distribución de respuestas por encuesta + const responseDistribution = await this.getResponseDistribution(); + + // Obtener las estadísticas detalladas de las encuestas + const surveyDetails = await this.getSurveyDetails(); + + return { + totalSurveys, + totalResponses, + completionRate, + surveysByMonth, + responsesByAudience, + responseDistribution, + surveyDetails, + } + } + + private async getTotalSurveysCount(): Promise { + const result = await this.drizzle + .select({ count: sql`count(*)` }) + .from(surveys); + return Number(result[0].count); + } + + private async getTotalResponsesCount(): Promise { + const result = await this.drizzle + .select({ count: sql`count(*)` }) + .from(answersSurveys); + return Number(result[0].count); + } + + private async getSurveysByMonth(): Promise<{ month: string; count: number }[]> { + const result = await this.drizzle + .select({ + month: sql`to_char(created_at, 'YYYY-MM')`, + count: sql`count(*)`, + }) + .from(surveys) + .groupBy(sql`to_char(created_at, 'YYYY-MM')`) + .orderBy(sql`to_char(created_at, 'YYYY-MM')`); + return result.map(item => ({ month: item.month, count: Number(item.count) })); + } + + private async getResponsesByAudience(): Promise<{ name: any; value: number }[]> { + const result = await this.drizzle + .select({ + audience: surveys.targetAudience, + count: sql`count(*)`, + }) + .from(answersSurveys) + .leftJoin(surveys, eq(answersSurveys.surveyId, surveys.id)) + .groupBy(surveys.targetAudience); + return result.map(item => + { + let audience = 'Sin definir' + if (item.audience == 'all') { + audience = 'General' + } else if (item.audience == 'organization') { + audience = 'Organización' + } else if (item.audience == 'producers') { + audience = 'Productores' + } + return ({ name: audience, value: Number(item.count) }) + } + ); + } + + private async getResponseDistribution(): Promise<{ title: string; responses: number }[]> { + const result = await this.drizzle + .select({ + id: surveys.id, + title: surveys.title, + responses: sql`count(${answersSurveys.id})`, + }) + .from(surveys) + .leftJoin(answersSurveys, eq(surveys.id, answersSurveys.surveyId)) + .groupBy(surveys.id, surveys.title) + .orderBy(sql`count(${answersSurveys.id}) desc`) + .limit(10); + return result.map(item => ({ title: item.title, responses: Number(item.responses) })); + } + + private async getSurveyDetails(): Promise { + const allSurveys = await this.drizzle + .select({ + id: surveys.id, + title: surveys.title, + description: surveys.description, + targetAudience: surveys.targetAudience, + createdAt: surveys.created_at, + closingDate: surveys.closingDate, + questions: surveys.questions, + }) + .from(surveys); + + + return await Promise.all( + allSurveys.map(async (survey) => { + // Obtener el número total de respuestas para esta encuesta + const totalSurveyResponses = await this.getTotalSurveyResponses(survey.id); + + // Obtener todas las respuestas para esta encuesta + const answersResult = await this.drizzle + .select({ answers: answersSurveys.answers }) + .from(answersSurveys) + .where(eq(answersSurveys.surveyId, survey.id)); + + let audience = 'Sin definir' + if (survey.targetAudience == 'all') { + audience = 'General' + } else if (survey.targetAudience == 'organization') { + audience = 'Organización' + } else if (survey.targetAudience == 'producers') { + audience = 'Productores' + } + + // Procesar las estadísticas de las preguntas + const questionStats = this.processQuestionStats(survey.questions as any[], answersResult); + + return { + id: survey.id, + title: survey.title, + description: survey.description, + totalResponses: totalSurveyResponses, + // targetAudience: survey.targetAudience, + targetAudience: audience, + createdAt: survey.createdAt.toISOString(), + closingDate: survey.closingDate ? new Date(survey.closingDate).toISOString() : undefined, + questionStats, + }; + }) + ); + } + + private async getTotalSurveyResponses(surveyId: number): Promise { + const result = await this.drizzle + .select({ count: sql`count(*)` }) + .from(answersSurveys) + .where(eq(answersSurveys.surveyId, surveyId)); + return Number(result[0].count); + } + + // ================================== + + private processQuestionStats(questions: any[], answersResult: { answers: any }[]) { + // Initialize counters for each question option + const questionStats: Array<{ questionId: string; label: string; count: number }> = []; + + // Skip title questions (type: 'title') + const surveyQuestions = questions.filter(q => q.type !== 'title'); + + // console.log(surveyQuestions); + // console.log('Se llamo a processQuestionStats()'); + + for (const question of surveyQuestions) { + // console.log('Bucle1 se ejecuto'); + + // For single choice, multiple choice, and select questions + // if (['single_choice', 'multiple_choice', 'select'].includes(question.type)) { + const optionCounts: Record = {}; + + // // Initialize counts for each option + // for (const option of question.options) { + // optionCounts[option.text] = 0; + // } + + // // Count responses for each option + // for (const answerObj of answersResult) { + // const answer = answerObj.answers.find(a => a.questionId === question.id); + + // if (answer) { + // if (Array.isArray(answer.value)) { + // // For multiple choice questions + // for (const value of answer.value) { + // if (optionCounts[value] !== undefined) { + // optionCounts[value]++; + // } + // } + // } else { + // // For single choice questions + // if (optionCounts[answer.value] !== undefined) { + // optionCounts[answer.value]++; + // } + // } + // } + // } + + // // Convert to the required format + // for (const option of question.options) { + // questionStats.push({ + // questionId: String(question.id), + // label: option.text, + // count: optionCounts[option.value] || 0, + // }); + // } + + if (question.type == 'multiple_choice') { + for (const option of question.options) { + console.log(option); + let count :number = 0 + // Count responses for each option + for (const obj of answersResult) { + console.log(obj.answers) + const resp = obj.answers.find(a => a.questionId == question.id) + const respArray = resp.value.split(",") + // console.log(); + + if (respArray[option.id] == 'true') { + count++ + } + } + optionCounts[option.text] = count + // Convert to the required format + questionStats.push({ + questionId: String(question.id), + label: `${question.question} ${option.text}`, + count: optionCounts[option.text] || 0, + }) + } + + } else if (question.type == 'single_choice' || question.type == 'select') { + for (const option of question.options) { + let count :number = 0 + // Count responses for each option + for (const obj of answersResult) { + const resp = obj.answers.find(a => a.questionId == question.id) + if (resp.value == option.id) { + count++ + } + } + optionCounts[option.text] = count + // Convert to the required format + questionStats.push({ + questionId: String(question.id), + label: `${question.question} ${option.text}`, + count: optionCounts[option.text] || 0, + }) + } + } else if (question.type === 'simple') { + // For simple text questions, just count how many responses + let responseCount = 0; + + for (const answerObj of answersResult) { + const answer = answerObj.answers.find(a => a.questionId === question.id); + if (answer && answer.value) { + responseCount++; + } + } + + questionStats.push({ + questionId: String(question.id), + label: question.question, + count: responseCount, + }); + } + } + + return questionStats; + } +} \ No newline at end of file diff --git a/apps/api/src/features/user-roles/dto/assign-role.dto.ts b/apps/api/src/features/user-roles/dto/assign-role.dto.ts new file mode 100644 index 0000000..d799e25 --- /dev/null +++ b/apps/api/src/features/user-roles/dto/assign-role.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNumber } from 'class-validator'; + +export class AssignRoleDto { + @ApiProperty() + @IsNumber() + userId: number; + + @ApiProperty() + @IsNumber() + roleId: number; +} \ No newline at end of file diff --git a/apps/api/src/features/user-roles/user-roles.controller.ts b/apps/api/src/features/user-roles/user-roles.controller.ts new file mode 100644 index 0000000..daa33bb --- /dev/null +++ b/apps/api/src/features/user-roles/user-roles.controller.ts @@ -0,0 +1,46 @@ +import { Roles } from '@/common/decorators/roles.decorator'; +import { Body, Controller, Delete, Get, Param, Post } from '@nestjs/common'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { AssignRoleDto } from './dto/assign-role.dto'; +import { UserRolesService } from './user-roles.service'; + +@ApiTags('user-roles') +@Controller('user-roles') +export class UserRolesController { + constructor(private readonly userRolesService: UserRolesService) {} + + @Get('user/:userId') + @Roles('admin') + @ApiOperation({ summary: 'Get roles by user ID' }) + @ApiResponse({ status: 200, description: 'Return roles for the user.' }) + @ApiResponse({ status: 404, description: 'User not found.' }) + async getRolesByUserId(@Param('userId') userId: string) { + const data = await this.userRolesService.getRolesByUserId(+userId); + return { message: 'Roles fetched successfully', data }; + } + + @Post('assign') + @Roles('admin') + @ApiOperation({ summary: 'Assign a role to a user' }) + @ApiResponse({ status: 200, description: 'Role assigned successfully.' }) + @ApiResponse({ status: 404, description: 'User or role not found.' }) + async assignRoleToUser(@Body() assignRoleDto: AssignRoleDto) { + const data = await this.userRolesService.assignRoleToUser(assignRoleDto); + return { message: 'Role assigned successfully', data }; + } + + @Delete('user/:userId/role/:roleId') + @Roles('admin') + @ApiOperation({ summary: 'Remove a role from a user' }) + @ApiResponse({ status: 200, description: 'Role removed successfully.' }) + @ApiResponse({ + status: 404, + description: 'User-role relationship not found.', + }) + async removeRoleFromUser( + @Param('userId') userId: string, + @Param('roleId') roleId: string, + ) { + return await this.userRolesService.removeRoleFromUser(+userId, +roleId); + } +} diff --git a/apps/api/src/features/user-roles/user-roles.module.ts b/apps/api/src/features/user-roles/user-roles.module.ts new file mode 100644 index 0000000..76dd9ae --- /dev/null +++ b/apps/api/src/features/user-roles/user-roles.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { UserRolesController } from './user-roles.controller'; +import { UserRolesService } from './user-roles.service'; +import { DrizzleModule } from '@/database/drizzle.module'; + +@Module({ + imports: [DrizzleModule], + controllers: [UserRolesController], + providers: [UserRolesService], + exports: [UserRolesService], +}) +export class UserRolesModule {} \ No newline at end of file diff --git a/apps/api/src/features/user-roles/user-roles.service.ts b/apps/api/src/features/user-roles/user-roles.service.ts new file mode 100644 index 0000000..d636d37 --- /dev/null +++ b/apps/api/src/features/user-roles/user-roles.service.ts @@ -0,0 +1,118 @@ +import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { eq, and } from 'drizzle-orm'; +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider'; +import * as schema from 'src/database/index'; +import { users, roles, usersRole } from 'src/database/index'; +import { AssignRoleDto } from './dto/assign-role.dto'; + +@Injectable() +export class UserRolesService { + constructor( + @Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase, + ) {} + + async getRolesByUserId(userId: number) { + // Check if user exists + const user = await this.drizzle + .select() + .from(users) + .where(eq(users.id, userId)); + + if (user.length === 0) { + throw new HttpException('User not found', HttpStatus.NOT_FOUND); + } + + // Get roles for the user + const userRoles = await this.drizzle + .select({ + id: roles.id, + name: roles.name, + }) + .from(usersRole) + .leftJoin(roles, eq(roles.id, usersRole.roleId)) + .where(eq(usersRole.userId, userId)); + + return userRoles; + } + + async assignRoleToUser(assignRoleDto: AssignRoleDto) { + const { userId, roleId } = assignRoleDto; + + // Check if user exists + const user = await this.drizzle + .select() + .from(users) + .where(eq(users.id, userId)); + + if (user.length === 0) { + throw new HttpException('User not found', HttpStatus.NOT_FOUND); + } + + // Check if role exists + const role = await this.drizzle + .select() + .from(roles) + .where(eq(roles.id, roleId)); + + if (role.length === 0) { + throw new HttpException('Role not found', HttpStatus.NOT_FOUND); + } + + // Check if the user already has this role + const existingUserRole = await this.drizzle + .select() + .from(usersRole) + .where( + and( + eq(usersRole.userId, userId), + eq(usersRole.roleId, roleId), + ), + ); + + if (existingUserRole.length > 0) { + throw new HttpException('User already has this role', HttpStatus.BAD_REQUEST); + } + + // Assign role to user + await this.drizzle.insert(usersRole).values({ + userId, + roleId, + }); + + // Return the updated roles + return this.getRolesByUserId(userId); + } + + async removeRoleFromUser(userId: number, roleId: number) { + // Check if the user-role relationship exists + const userRole = await this.drizzle + .select() + .from(usersRole) + .where( + and( + eq(usersRole.userId, userId), + eq(usersRole.roleId, roleId), + ), + ); + + if (userRole.length === 0) { + throw new HttpException( + 'User-role relationship not found', + HttpStatus.NOT_FOUND, + ); + } + + // Remove the role from the user + await this.drizzle + .delete(usersRole) + .where( + and( + eq(usersRole.userId, userId), + eq(usersRole.roleId, roleId), + ), + ); + + return { message: 'Role removed from user successfully' }; + } +} \ No newline at end of file diff --git a/apps/api/src/features/users/dto/create-user.dto.ts b/apps/api/src/features/users/dto/create-user.dto.ts new file mode 100644 index 0000000..882bf1d --- /dev/null +++ b/apps/api/src/features/users/dto/create-user.dto.ts @@ -0,0 +1,33 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsInt, IsOptional, IsString } from 'class-validator'; + +export class CreateUserDto { + @ApiProperty() + @IsString() + username: string; + + @ApiProperty() + @IsEmail() + email: string; + + @ApiProperty() + @IsString() + fullname: string; + + @ApiProperty() + @IsString({ + message: 'Phone must be a string', + }) + @IsOptional() + phone: string; + + @ApiProperty() + @IsString({ + message: 'Password must be a string', + }) + password: string; + + @ApiProperty() + @IsInt() + role: number; +} diff --git a/apps/api/src/features/users/dto/update-user.dto.ts b/apps/api/src/features/users/dto/update-user.dto.ts new file mode 100644 index 0000000..933a7d9 --- /dev/null +++ b/apps/api/src/features/users/dto/update-user.dto.ts @@ -0,0 +1,40 @@ +import { ApiProperty, PartialType } from '@nestjs/swagger'; +import { CreateUserDto } from './create-user.dto'; + +// import { ApiProperty } from '@nestjs/swagger'; +import { IsOptional, IsString } from 'class-validator'; + +export class UpdateUserDto extends PartialType(CreateUserDto) { + // export class UpdateUserDto { + @IsOptional() + username: string; + + @IsOptional() + email: string; + + @IsOptional() + fullname: string; + + @IsOptional() + phone: string; + + @IsOptional() + password: string; + + @ApiProperty() + @IsString() + @IsOptional() + isActive: string; + + @IsOptional() + state: string | number | null; + + @IsOptional() + municipality: string | number | null; + + @IsOptional() + parish: string | number | null; + + @IsOptional() + role: number; +} diff --git a/apps/api/src/features/users/entities/user.entity.ts b/apps/api/src/features/users/entities/user.entity.ts new file mode 100644 index 0000000..2270748 --- /dev/null +++ b/apps/api/src/features/users/entities/user.entity.ts @@ -0,0 +1,25 @@ +export class User { + id?: number; + username!: string; + email!: string; + fullname!: string; + phone?: string | null; + password?: string; + isTwoFactorEnabled?: boolean; + twoFactorSecret?: string | null; + isEmailVerified?: boolean; + isActive!: boolean; + created_at?: Date | null; + updated_at?: Date | null; + state?: string | number | null; + municipality?: string | number | null; + parish?: string | number | null; +} + +export class UserRole { + id!: number; + userId!: number; + roleId!: number; + created_at?: Date | null; + updated_at?: Date | null; +} \ No newline at end of file diff --git a/apps/api/src/features/users/users.controller.ts b/apps/api/src/features/users/users.controller.ts new file mode 100644 index 0000000..80d26fc --- /dev/null +++ b/apps/api/src/features/users/users.controller.ts @@ -0,0 +1,81 @@ +import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common'; +import { UsersService } from './users.service'; +import { CreateUserDto } from './dto/create-user.dto'; +import { UpdateUserDto } from './dto/update-user.dto'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { Roles } from '../../common/decorators/roles.decorator'; +import { PaginationDto } from '../../common/dto/pagination.dto'; + +@ApiTags('users') +@Controller('users') +export class UsersController { + constructor(private readonly usersService: UsersService) {} + + @Get() + @Roles('admin') + @ApiOperation({ summary: 'Get all users with pagination and filters' }) + @ApiResponse({ status: 200, description: 'Return paginated users.' }) + async findAll(@Query() paginationDto: PaginationDto) { + const result = await this.usersService.findAll(paginationDto); + return { + message: 'Users fetched successfully', + data: result.data, + meta: result.meta + }; + } + + @Get(':id') + // @Roles('admin') + @ApiOperation({ summary: 'Get a user by ID' }) + @ApiResponse({ status: 200, description: 'Return the user.' }) + @ApiResponse({ status: 404, description: 'User not found.' }) + async findOne(@Param('id') id: string) { + const data = await this.usersService.findOne(id); + return { message: 'User fetched successfully', data }; + } + + @Post() + @Roles('admin') + @ApiOperation({ summary: 'Create a new user' }) + @ApiResponse({ status: 201, description: 'User created successfully.' }) + async create( + @Body() createUserDto: CreateUserDto, + @Query('roleId') roleId?: string, + ) { + const data = await this.usersService.create( + createUserDto, + roleId ? parseInt(roleId) : undefined, + ); + return { message: 'User created successfully', data }; + } + + @Patch(':id') + @Roles('admin') + @ApiOperation({ summary: 'Update a user' }) + @ApiResponse({ status: 200, description: 'User updated successfully.' }) + @ApiResponse({ status: 404, description: 'User not found.' }) + async update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) { + const data = await this.usersService.update(id, updateUserDto); + return { message: 'User updated successfully', data }; + } + + @Patch('profile/:id') + // @Roles('admin') + @ApiOperation({ summary: 'Update a user' }) + @ApiResponse({ status: 200, description: 'User updated successfully.' }) + @ApiResponse({ status: 400, description: 'email already exists.' }) + @ApiResponse({ status: 404, description: 'User not found.' }) + async updateProfile(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) { + const data = await this.usersService.updateProfile(id, updateUserDto); + return { message: 'User updated successfully', data }; + } + + @Delete(':id') + @Roles('admin') + @ApiOperation({ summary: 'Delete a user' }) + @ApiResponse({ status: 200, description: 'User deleted successfully.' }) + @ApiResponse({ status: 404, description: 'User not found.' }) + async remove(@Param('id') id: string) { + return await this.usersService.remove(id); + } +} diff --git a/apps/api/src/features/users/users.module.ts b/apps/api/src/features/users/users.module.ts new file mode 100644 index 0000000..ee7a016 --- /dev/null +++ b/apps/api/src/features/users/users.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { UsersController } from './users.controller'; +import { UsersService } from './users.service'; +import { DrizzleModule } from '@/database/drizzle.module'; + +@Module({ + imports: [DrizzleModule], + controllers: [UsersController], + providers: [UsersService], +}) +export class UsersModule {} diff --git a/apps/api/src/features/users/users.service.ts b/apps/api/src/features/users/users.service.ts new file mode 100644 index 0000000..ec6a114 --- /dev/null +++ b/apps/api/src/features/users/users.service.ts @@ -0,0 +1,288 @@ +import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider'; +import { Env, validateString } from '@/common/utils'; +import { Inject, Injectable, HttpException, HttpStatus, NotFoundException, UnauthorizedException } from '@nestjs/common'; +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import * as schema from 'src/database/index'; +import { users, roles, usersRole } from 'src/database/index'; +import { eq, like, or, SQL, sql, and, not } from 'drizzle-orm'; +import * as bcrypt from 'bcryptjs'; +import { CreateUserDto } from './dto/create-user.dto'; +import { UpdateUserDto } from './dto/update-user.dto'; +import { User } from './entities/user.entity'; +import { PaginationDto } from '../../common/dto/pagination.dto'; + +@Injectable() +export class UsersService { + constructor( + @Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase, + ) { } + + async findAll(paginationDto?: PaginationDto): Promise<{ data: User[], meta: any }> { + const { page = 1, limit = 10, search = '', sortBy = 'id', sortOrder = 'asc' } = paginationDto || {}; + + // Calculate offset + const offset = (page - 1) * limit; + + // Build search condition + let searchCondition: SQL | undefined; + if (search) { + searchCondition = or( + like(users.username, `%${search}%`), + like(users.email, `%${search}%`), + like(users.fullname, `%${search}%`) + ); + } + + // Build sort condition + const orderBy = sortOrder === 'asc' + ? sql`${users[sortBy as keyof typeof users]} asc` + : sql`${users[sortBy as keyof typeof users]} desc`; + + // Get total count for pagination + const totalCountResult = await this.drizzle + .select({ count: sql`count(*)` }) + .from(users) + .where(searchCondition); + + const totalCount = Number(totalCountResult[0].count); + const totalPages = Math.ceil(totalCount / limit); + + // Get paginated data + const data = await this.drizzle + .select({ + id: users.id, + username: users.username, + email: users.email, + fullname: users.fullname, + phone: users.phone, + isActive: users.isActive, + role: roles.name, + }) + .from(users) + .leftJoin(usersRole, eq(usersRole.userId, users.id)) + .leftJoin(roles, eq(roles.id, usersRole.roleId)) + .where(searchCondition) + .orderBy(orderBy) + .limit(limit) + .offset(offset); + + // Build pagination metadata + const meta = { + page, + limit, + totalCount, + totalPages, + hasNextPage: page < totalPages, + hasPreviousPage: page > 1, + nextPage: page < totalPages ? page + 1 : null, + previousPage: page > 1 ? page - 1 : null, + }; + + // console.log(data); + + return { data, meta }; + } + + async findOne(id: string): Promise { + const find = await this.drizzle + .select({ + id: users.id, + username: users.username, + email: users.email, + fullname: users.fullname, + phone: users.phone, + isActive: users.isActive, + role: roles.name, + state: schema.states.name, + municipality: schema.municipalities.name, + parish: schema.parishes.name + }) + .from(users) + .leftJoin(usersRole, eq(usersRole.userId, users.id)) + .leftJoin(roles, eq(roles.id, usersRole.roleId)) + .leftJoin(schema.states, eq(schema.states.id, users.state)) + .leftJoin(schema.municipalities, eq(schema.municipalities.id, users.municipality)) + .leftJoin(schema.parishes, eq(schema.parishes.id, users.parish)) + + .where(eq(users.id, parseInt(id))); + + if (find.length === 0) { + throw new HttpException('User does not exist', HttpStatus.BAD_REQUEST); + } + + return find[0]; + } + + // Rest of the service remains the same + async create( + createUserDto: CreateUserDto, + roleId: number = 2, + ): Promise { + // Hash the password + const hashedPassword = await bcrypt.hash(createUserDto.password, 10); + + const data = await this.drizzle + .select({ + id: users.id, + username: users.username, + email: users.email + }) + .from(users) + .where(or(eq(users.username, createUserDto.username), eq(users.email, createUserDto.email))); + + if (data.length > 0) { + if (data[0].username === createUserDto.username) { + throw new HttpException('Username already exists', HttpStatus.BAD_REQUEST); + } + if (data[0].email === createUserDto.email) { + throw new HttpException('Email already exists', HttpStatus.BAD_REQUEST); + } + } + + // Start a transaction + return await this.drizzle.transaction(async (tx) => { + // Create the user + const [newUser] = await tx + .insert(users) + .values({ + username: createUserDto.username, + email: createUserDto.email, + password: hashedPassword, + fullname: createUserDto.fullname, + isActive: true, + phone: createUserDto.phone, + isEmailVerified: false, + isTwoFactorEnabled: false, + }) + .returning(); + + // Assign role to user + await tx.insert(usersRole).values({ + userId: newUser.id, + roleId: roleId, + }); + + // Return the created user with role + const [userWithRole] = await tx + .select({ + id: users.id, + username: users.username, + email: users.email, + fullname: users.fullname, + phone: users.phone, + isActive: users.isActive, + role: roles.name, + }) + .from(users) + .leftJoin(usersRole, eq(usersRole.userId, users.id)) + .leftJoin(roles, eq(roles.id, usersRole.roleId)) + .where(eq(users.id, newUser.id)); + + return userWithRole; + }); + } + + async update(id: string, updateUserDto: UpdateUserDto): Promise { + const userId = parseInt(id); + + // Check if user exists + await this.findOne(id); + + // Prepare update data + const updateData: any = {}; + if (updateUserDto.username) updateData.username = updateUserDto.username; + if (updateUserDto.email) updateData.email = updateUserDto.email; + if (updateUserDto.fullname) updateData.fullname = updateUserDto.fullname; + if (updateUserDto.password) { + updateData.password = await bcrypt.hash(updateUserDto.password, 10); + } + if (updateUserDto.phone) updateData.phone = updateUserDto.phone; + if (updateUserDto.isActive) updateData.isActive = updateUserDto.isActive; + + const updateDataRole: any = {}; + if (updateUserDto.role) updateDataRole.roleId = updateUserDto.role; + // Update user + await this.drizzle + .update(users) + .set(updateData) + .where(eq(users.id, userId)); + + await this.drizzle + .update(usersRole) + .set(updateDataRole) + .where(eq(usersRole.userId, userId)); + + + // Return updated user + return this.findOne(id); + } + + async updateProfile(id: string, updateUserDto: UpdateUserDto): Promise { + // throw new HttpException('Email already exists', HttpStatus.BAD_REQUEST); + + const userId = parseInt(id); + + // Check if user exists + await this.findOne(id); + + const data = await this.drizzle + .select({ + id: users.id, + email: users.email + }) + .from(users) + .where(and( + not(eq(users.id, userId)), + eq(users.email, updateUserDto.email) + ) + ) + + if (data.length > 0) { + if (data[0].email === updateUserDto.email) { + throw new HttpException('Email already exists', HttpStatus.BAD_REQUEST); + } + } + + // Prepare update data + const updateData: any = {}; + // if (updateUserDto.username) updateData.username = updateUserDto.username; + if (updateUserDto.email) updateData.email = updateUserDto.email; + if (updateUserDto.fullname) updateData.fullname = updateUserDto.fullname; + if (updateUserDto.password) { + updateData.password = await bcrypt.hash(updateUserDto.password, 10); + } + if (updateUserDto.phone) updateData.phone = updateUserDto.phone; + if (updateUserDto.isActive) updateData.isActive = updateUserDto.isActive; + + if (updateUserDto.state) { + updateData.state = updateUserDto.state; + updateData.municipality = updateUserDto.municipality + updateData.parish = updateUserDto.parish + } + + + // Update user + await this.drizzle + .update(users) + .set(updateData) + .where(eq(users.id, userId)); + + // Return updated user + return this.findOne(id); + + } + + async remove(id: string): Promise<{ message: string, data: User }> { + const userId = parseInt(id); + + // Check if user exists + const user = await this.findOne(id); + + // Delete user (this will cascade delete related records due to foreign key constraints) + // await this.drizzle.delete(users).where(eq(users.id, userId)); + await this.drizzle.update(users).set({ isActive: false }).where(eq(users.id, userId)); + + return { message: 'User deleted successfully', data: user }; + } +} + diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts new file mode 100644 index 0000000..9c03b7b --- /dev/null +++ b/apps/api/src/main.ts @@ -0,0 +1,16 @@ +import { AppModule } from '@/app.module'; +import { bootstrap } from '@/bootstrap'; +import { NestFactory } from '@nestjs/core'; +import { NestExpressApplication } from '@nestjs/platform-express'; + +const main = async () => { + const app = await NestFactory.create(AppModule, { + bufferLogs: true, + }); + await bootstrap(app); +}; + +main().catch((error) => { + console.log(error); + process.exit(1); +}); diff --git a/apps/api/src/swagger.ts b/apps/api/src/swagger.ts new file mode 100644 index 0000000..2978220 --- /dev/null +++ b/apps/api/src/swagger.ts @@ -0,0 +1,11 @@ +import { NestExpressApplication } from '@nestjs/platform-express'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; + +export const swagger = async (app: NestExpressApplication) => { + const swaggerConfig = new DocumentBuilder() + .setTitle('Turbo repo') + .addBearerAuth() + .build(); + const document = SwaggerModule.createDocument(app, swaggerConfig); + SwaggerModule.setup('api-docs', app, document); +}; diff --git a/apps/api/test/app.e2e-spec.ts b/apps/api/test/app.e2e-spec.ts new file mode 100644 index 0000000..bdb152f --- /dev/null +++ b/apps/api/test/app.e2e-spec.ts @@ -0,0 +1,24 @@ +import { AppModule } from '@/app.module'; +import { INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import * as request from 'supertest'; + +describe('AppController (e2e)', () => { + let app: INestApplication; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + it('/ (GET)', () => { + return request(app.getHttpServer()) + .get('/') + .expect(200) + .expect('Hello World!'); + }); +}); diff --git a/apps/api/test/jest-e2e.json b/apps/api/test/jest-e2e.json new file mode 100644 index 0000000..e9d912f --- /dev/null +++ b/apps/api/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/apps/api/tsconfig.build.json b/apps/api/tsconfig.build.json new file mode 100644 index 0000000..64f86c6 --- /dev/null +++ b/apps/api/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json new file mode 100644 index 0000000..3602d5e --- /dev/null +++ b/apps/api/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "@repo/ts-config/nestjs.json", + "compilerOptions": { + "strict": true, + "strictNullChecks": true, + "strictPropertyInitialization": false, + "outDir": "./dist", + "baseUrl": "./", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/apps/api/uploads/.gitkeep b/apps/api/uploads/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/web/.env_template b/apps/web/.env_template new file mode 100644 index 0000000..9450d76 --- /dev/null +++ b/apps/web/.env_template @@ -0,0 +1,4 @@ +AUTH_URL = http://localhost:3000 +AUTH_SECRET=wWgIwkHIGr28ydIUPsgVGNUNxXQ+brg1XXtA8PfjJjAEPJLDP2UTghWL8aE= +API_URL=http://localhost:8000 + diff --git a/apps/web/.gitignore b/apps/web/.gitignore new file mode 100644 index 0000000..8db0e06 --- /dev/null +++ b/apps/web/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# env files (can opt-in for commiting if needed) +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/apps/web/README.md b/apps/web/README.md new file mode 100644 index 0000000..5bcd4e1 --- /dev/null +++ b/apps/web/README.md @@ -0,0 +1,49 @@ +### Frontend + +`SafeFetch` + +```ts +import z, { ZodSchema } from 'zod'; +import { env } from './env'; + +export const safeFetch = async >( + schema: T, + url: URL | RequestInfo, + init?: RequestInit, +): Promise<[string | null, z.TypeOf]> => { + const response: Response = await fetch(`${env.API_URL}${url}`, init); + const res = await response.json(); + + if (!response.ok) { + return [ + `HTTP error! Status: ${response.status} - ${response.statusText}`, + null, + ]; + } + + const validateFields = schema.safeParse(res); + + if (!validateFields.success) { + console.log(res); + console.log('Validation errors:', validateFields.error); + return [`Validation error: ${validateFields.error.message}`, null]; + } + + return [null, validateFields.data]; +}; +``` + +`How to use SafeFetch?` + +```ts +export const getAllUsers = async (): Promise => { + const [isError, data] = await safeFetch(GetAllUsersSchema, '/users', { + cache: 'no-store', + }); + if (isError) + return { + data: [], + }; + return data; +}; +``` diff --git a/apps/web/app/(auth)/page.tsx b/apps/web/app/(auth)/page.tsx new file mode 100644 index 0000000..c6c678e --- /dev/null +++ b/apps/web/app/(auth)/page.tsx @@ -0,0 +1,14 @@ +import { LoginForm } from '@/feactures/auth/components/sigin-view'; + + +const Page = () => { + return ( +
+
+ +
+
+ ) +}; + +export default Page; diff --git a/apps/web/app/api/auth/[...nextauth]/route.ts b/apps/web/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..62b0319 --- /dev/null +++ b/apps/web/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,2 @@ +import { handlers } from '@/lib/auth'; // Referring to the auth.ts we just created +export const { GET, POST } = handlers; diff --git a/apps/web/app/dashboard/administracion/encuestas/crear/page.tsx b/apps/web/app/dashboard/administracion/encuestas/crear/page.tsx new file mode 100644 index 0000000..2a1c9af --- /dev/null +++ b/apps/web/app/dashboard/administracion/encuestas/crear/page.tsx @@ -0,0 +1,13 @@ +import PageContainer from '@/components/layout/page-container'; +import { SurveyBuilder } from '@/feactures/surveys/components/admin/survey-builder'; + +export default function CreateSurveyPage() { + return ( + +
+

Crear Nueva Encuesta

+ +
+
+ ); +} \ No newline at end of file diff --git a/apps/web/app/dashboard/administracion/encuestas/editar/[id]/page.tsx b/apps/web/app/dashboard/administracion/encuestas/editar/[id]/page.tsx new file mode 100644 index 0000000..2733a99 --- /dev/null +++ b/apps/web/app/dashboard/administracion/encuestas/editar/[id]/page.tsx @@ -0,0 +1,13 @@ +import PageContainer from "@/components/layout/page-container"; +import { SurveyBuilder } from "@/feactures/surveys/components/admin/survey-builder"; + +export default function EditSurveyPage() { + return ( + +
+

Editar Encuesta

+ +
+
+ ) +} \ No newline at end of file diff --git a/apps/web/app/dashboard/administracion/encuestas/page.tsx b/apps/web/app/dashboard/administracion/encuestas/page.tsx new file mode 100644 index 0000000..1d137ec --- /dev/null +++ b/apps/web/app/dashboard/administracion/encuestas/page.tsx @@ -0,0 +1,37 @@ +import PageContainer from '@/components/layout/page-container'; +import SurveysAdminList from '@/feactures/surveys/components/admin/surveys-admin-list'; +import { SurveysHeader } from '@/feactures/surveys/components/admin/surveys-header'; +import SurveysTableAction from '@/feactures/surveys/components/admin/surveys-tables/survey-table-action'; +import { searchParamsCache, serialize } from '@/feactures/surveys/utils/searchparams'; +import { SearchParams } from 'nuqs'; + +type pageProps = { + searchParams: Promise; +}; + + +export default async function SurveyAdminPage(props: pageProps) { + const searchParams = await props.searchParams; + searchParamsCache.parse(searchParams); + const key = serialize({ ...searchParams }); + + const page = Number(searchParamsCache.get('page')) || 1; + const search = searchParamsCache.get('q'); + const pageLimit = Number(searchParamsCache.get('limit')) || 10; + const type = searchParamsCache.get('type'); + + return ( + +
+ + + +
+
+ ); +} \ No newline at end of file diff --git a/apps/web/app/dashboard/administracion/usuario/page.tsx b/apps/web/app/dashboard/administracion/usuario/page.tsx new file mode 100644 index 0000000..116c593 --- /dev/null +++ b/apps/web/app/dashboard/administracion/usuario/page.tsx @@ -0,0 +1,37 @@ +import PageContainer from '@/components/layout/page-container'; +import UsersAdminList from '@/feactures/users/components/admin/users-admin-list'; +import { UsersHeader } from '@/feactures/users/components/admin/users-header'; +import UsersTableAction from '@/feactures/users/components/admin/surveys-tables/users-table-action'; +import { searchParamsCache, serialize } from '@/feactures/users/utils/searchparams'; +import { SearchParams } from 'nuqs'; + +type pageProps = { + searchParams: Promise; +}; + + +export default async function SurveyAdminPage(props: pageProps) { + const searchParams = await props.searchParams; + searchParamsCache.parse(searchParams); + const key = serialize({ ...searchParams }); + + const page = Number(searchParamsCache.get('page')) || 1; + const search = searchParamsCache.get('q'); + const pageLimit = Number(searchParamsCache.get('limit')) || 10; + const type = searchParamsCache.get('type'); + + return ( + +
+ + + +
+
+ ); +} \ No newline at end of file diff --git a/apps/web/app/dashboard/configuraciones/caja-ahorro/page.tsx b/apps/web/app/dashboard/configuraciones/caja-ahorro/page.tsx new file mode 100644 index 0000000..4344857 --- /dev/null +++ b/apps/web/app/dashboard/configuraciones/caja-ahorro/page.tsx @@ -0,0 +1,19 @@ +import PageContainer from '@/components/layout/page-container'; + +const Page = () => { + return ( + +
+ En mantenimiento +
+ +
+ //
+ //
+ + //
+ //
+ ); +}; + +export default Page; diff --git a/apps/web/app/dashboard/encuestas/[id]/responder/page.tsx b/apps/web/app/dashboard/encuestas/[id]/responder/page.tsx new file mode 100644 index 0000000..f5e6302 --- /dev/null +++ b/apps/web/app/dashboard/encuestas/[id]/responder/page.tsx @@ -0,0 +1,24 @@ +import PageContainer from '@/components/layout/page-container'; +import { getSurveyByIdAction } from '@/feactures/surveys/actions/surveys-actions'; +import { SurveyResponse } from '@/feactures/surveys/components/survey-response'; + +// La función ahora recibe 'params' con el parámetro dinámico 'id' +export default async function SurveyResponsePage({ params }: { params: { id: string } }) { + const { id } = await params; // Obtienes el id desde los params de la URL + + if (!id || id === '') { + // Maneja el caso en el que no se proporciona un id + return null; + } + + // Llamas a la función pasando el id dinámico + const data = await getSurveyByIdAction(Number(id)); + + return ( + +
+ +
+
+ ); +} diff --git a/apps/web/app/dashboard/encuestas/page.tsx b/apps/web/app/dashboard/encuestas/page.tsx new file mode 100644 index 0000000..0fec60b --- /dev/null +++ b/apps/web/app/dashboard/encuestas/page.tsx @@ -0,0 +1,21 @@ +import PageContainer from '@/components/layout/page-container'; +import { SurveyList } from '@/feactures/surveys/components/survey-list'; +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Encuestas - Fondemi', + description: 'Listado de encuestas disponibles', +}; + +export default function SurveysPage() { + + return ( + +
+

Encuestas Disponibles

+ +
+
+ + ); +} \ No newline at end of file diff --git a/apps/web/app/dashboard/estadisticas/encuestas/page.tsx b/apps/web/app/dashboard/estadisticas/encuestas/page.tsx new file mode 100644 index 0000000..2ee30c9 --- /dev/null +++ b/apps/web/app/dashboard/estadisticas/encuestas/page.tsx @@ -0,0 +1,19 @@ +import PageContainer from '@/components/layout/page-container'; +import { SurveyStatistics } from '@/feactures/statistics/components/survey-statistics'; +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Estadísticas de Encuestas - Fondemi', + description: 'Análisis y estadísticas de las encuestas realizadas', +}; + +export default function SurveyStatisticsPage() { + return ( + +
+

Estadísticas de Encuestas

+ +
+
+ ); +} \ No newline at end of file diff --git a/apps/web/app/dashboard/inicio/page.tsx b/apps/web/app/dashboard/inicio/page.tsx new file mode 100644 index 0000000..d42420e --- /dev/null +++ b/apps/web/app/dashboard/inicio/page.tsx @@ -0,0 +1,11 @@ + +export default async function Page() { + + return ( + // +
+ Image +
+ //
+ ); +} diff --git a/apps/web/app/dashboard/layout.tsx b/apps/web/app/dashboard/layout.tsx new file mode 100644 index 0000000..db51a54 --- /dev/null +++ b/apps/web/app/dashboard/layout.tsx @@ -0,0 +1,31 @@ +import { AppSidebar } from '@/components/layout/app-sidebar'; +import Header from '@/components/layout/header'; +import { SidebarInset, SidebarProvider } from '@repo/shadcn/sidebar'; +import type { Metadata } from 'next'; +import { cookies } from 'next/headers'; + +export const metadata: Metadata = { + title: 'Dashboard', + description: 'Sistema integral para Cajas de Ahorro', +}; + +export default async function DashboardLayout({ + children, +}: { + children: React.ReactNode; +}) { + // Persisting the sidebar state in the cookie. + const cookieStore = await cookies(); + //const defaultOpen = cookieStore.get('sidebar:state')?.value === 'false'; + return ( + + + +
+ {/* page main content */} + {children} + {/* page main content ends */} + + + ); +} diff --git a/apps/web/app/dashboard/page.tsx b/apps/web/app/dashboard/page.tsx new file mode 100644 index 0000000..8fbbe21 --- /dev/null +++ b/apps/web/app/dashboard/page.tsx @@ -0,0 +1,12 @@ +import { auth } from '@/lib/auth'; +import { redirect } from 'next/navigation'; + +export default async function Dashboard() { + const session = await auth(); + + if (!session?.user) { + return redirect('/'); + } else { + redirect('/dashboard/inicio'); + } +} diff --git a/apps/web/app/dashboard/profile/page.tsx b/apps/web/app/dashboard/profile/page.tsx new file mode 100644 index 0000000..afb494d --- /dev/null +++ b/apps/web/app/dashboard/profile/page.tsx @@ -0,0 +1,17 @@ +import PageContainer from "@/components/layout/page-container"; +import {Profile} from '@/feactures/users/components/user-profile' + + +export default function ProfilePage() { + + return ( + +
+

Perfil

+

Aquí puede ver y editar sus datos de perfil

+ +
+
+ + ); +} \ No newline at end of file diff --git a/apps/web/app/error.tsx b/apps/web/app/error.tsx new file mode 100644 index 0000000..929d238 --- /dev/null +++ b/apps/web/app/error.tsx @@ -0,0 +1,37 @@ +'use client'; + +import { Button } from '@repo/shadcn/button'; +import { RotateCw } from '@repo/shadcn/icon'; +import { cn } from '@repo/shadcn/lib/utils'; +import { useRouter } from 'next/navigation'; +import { useEffect, useTransition } from 'react'; + +const Error = ({ error, reset }: { error: Error; reset: () => void }) => { + const router = useRouter(); + const [isPending, startTransition] = useTransition(); + useEffect(() => { + // Log the error to an error reporting service + console.error(error); + }, [error]); + + return ( +
+

+ Oh no, something went wrong... maybe refresh? +

+ +
+ ); +}; + +export default Error; diff --git a/apps/web/app/fonts/GeistMonoVF.woff b/apps/web/app/fonts/GeistMonoVF.woff new file mode 100644 index 0000000000000000000000000000000000000000..f2ae185cbfd16946a534d819e9eb03924abbcc49 GIT binary patch literal 67864 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ϣ%UY8`RWp~V-U_7 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 CLkb=M literal 0 HcmV?d00001 diff --git a/apps/web/app/fonts/GeistVF.woff b/apps/web/app/fonts/GeistVF.woff new file mode 100644 index 0000000000000000000000000000000000000000..1b62daacff96dad6584e71cd962051b82957c313 GIT binary patch literal 66268 zcmZsCWl$YW*X1l87)X>$?@vE);t4{YH1mFe0jBE_;zih3)d=3HtKOj};a$8LQ z;{mKizBoEx@QFoo%Q3U|F#Q_99{@n6699-amrKppH2XhZHUQxC)koh9Z`96Da}z^j z06>M|%Z~L6Y&1qSu;yQl0D#8RSN+!)NZ{U~8_aE--M@I|0KoT10055byf;V0+Ro^U zCui_=E#qI~`=w~)LS|#={?)gfz?a>x{{Y1Z*tIpZF#!PdSpa}6(AxtIw;VAx60fHIlil?>9x#H)4lkwAf#?OoR zq}|UH1-_GP?ro-XFe6E6ogAsB_lMb{eMTseU$Q#8C1b*`2YJE2UbHtB7q=F#8c?(} z7MH~UQP;KATrXR0jxH^-9xhh?btgLZV8`yP{4?~5t>#`dU`oKckttiKqS}=0h)-TL zm0*m)Fqi`0;=bZIlJL!*^OrHroA}Fuoxd5CU8V%At$}@aT%_Z<7=JytQ)D?oC4fu; zC9haKy!Hbi0eF1ipxzXiPt=aQ5wop-RG^?s>L>gO@@+lUXG(XGZgCD!0D&Zs4~^e% z(4?{(WBL;9gTH%!vIjaaOL4-?5F%AuAhqP$}Z5*a}4%FHO z__`OOSOe6f$5}vgbHKxcU-p9ue+OOu{ZSHabi?^-WyLLrt+h>i_s0J8MO%1(?6KJ{ z63srC7MKwg5YmV8R^udkjP>c;o0jS%3s1#VZSd_ZMMe}<_%<&|(8tdaVsob9SlD{! zxA!4>pO-DKVwcU1_Qs8{!D!x(rP>~w#&w_8M_z*m4KGu9`d7DfIq*xDA@Pot6Re`h`d%{lBo3am-vR=-J-SO9A>&egV84q&m&9c$A=5 z%sfs3V4GByk@8gn49E{h<(XwIcWcps58AEdX7(zpG>h`7(%)_eh+vz{k!pm%BiGC` z_=5Uzd3aO%4=d~2*uWjw8`-E&TB2z!BU(IgE;XDXw1NdI?B6(MBrV0BsbKgOQ)gVq zTiiW$Yclle$O3+`9mkU9lI}kdXSxZCVc3#pUpLeJh8n71U(M+H_oIWzXjf>?Ub;nl zgr}Vj|2|%YuvXf+F+N$AD`H8>BgpF)5=3ZV&6AF!QO#3~-9`j5fsyJ#B#%vv4OtoE zoN*Lf4;gCHrm9!=;fkWSwnDPm>OzFyN{<}u3vWw{2o9!32OW3*>roJVbmjZQzlG(e zE4}U2iH!Q@$Q{J!?*)q_&o{ma{Zw*#>>xizG(K?ovKtF`xdX~MyHu+y&V2B#8?UA} z3)GS+=ALKVHi<)w-QE08#-CNleh`G&y`sLDidTfmrv{gWy`!r=i}Q2v#-<1h==FuW zo4*3ygV;zyKBgxN{?HQ@hj_U+#I$gm{DHH5VFhB{&2 z43OeSH?8bW8=avoZjrZrTVFiF@fH_w@Xx3vrm3WK)B*ir9HxIFotJ&j?Ql0|_MlDW zFAFtz22CtP@SyIE`u?GZ)=dVaum({0Bk5$QOjPFeR;d)dg^tAMWb#XR zx1N+SC{!SJ|LgCF#-Y>9V0n)&ec+ON<`=rB^tflD@PO&5dd1P!f>fx9N5?Gz0tYaF*sLZO0G1fGI zJBmO(<#@h+D1mjw+HK82Tc@$VtNxi% zE|8*n7FS*<*b%&+mElheV^vn-j|^j#B3O7EpDyIt*oZgUdgrVD+nieQ%oCn z=tvim?Kk=%r6-5a5KYn{cSN(c#);ls)$rs z$>2WG89OeQn+$u%7X^jeuG!?UPZfU>)k2TT`WR;^in+~$27hvw5jonPA>KXZH+n=U z-HdTmV=8Uz@-l4RwROKIHX;)pYhnQ{-gA8{I9_E$1U2#W?a|Z=G1jId8eMbFB2X74 z`tO++;x+F#xG;{RF=LA2>8C&>LFr85=i$Wb6{aFrO{Wxnxot^AOP6_d{#zLQ$rDOh zmx8VSzye=SUQ$IMq75xI4HXEA59Fnh)i7cO!uVPQIAC%WY#)85)HZ%qC7?%_55Ys0-MmZ(mFLWpk4!|Q@tKYGc|M5aQKvdmMnP?P5ZYRPA@UcNk!m! zYM=N4>}|X9#ViD-@-{OA)mQFn9XsaS7Y9(?%-TyN$#35%!F`M`?q#}XOl%HVhbwjt zCD9hq%W@?Vb7iv9#SQ!^zs1Ahj*)z0u^gwJ$gQZK>LPl(dju$D&tWsLLmc6KaS3pr1Z2W;DVO|v_@95?1- zMM>VRwrEw^(?(cgn2z03cSM3w9re}A9@&J-iar~ThaWK;6qbgl9R+_nN+$C===>ifAHw@+mVJro54y_ie`FBKhGpGJfp{7P=$nYHDU85j@aE6xcjU`6`n+UdYu z;k~!=E%i><*SAqRV{@mB5+D#ad!{z`YfsejCwwfQ^S{HX?u$eA4ev+DnZ3iM@r`m+ zLRU?0^iI5+CYyk-JQeAW21GoJm#CuR4}=^0OawIPmLf^Bj+NP;px>mQ@ju91?hU?A z@^6NFDk5sm}DxK#dVoV-L%Npvrr+ooO@;l>4Y7QQ- zdW3cE{K)ywgL|nTIL7??f&XRGbC`}V$#eCsHr>w^yd7NU`;^EDQzm7ei3K5D%lm`+ z_NbNiy=Tm2b-)>1W5&6%wKhpFs?&aw_c-nSe6$OHn}oFM`AT6SSBsV1dD$@{#%ECO zaiNNq2pee!IeZP@I^E+v@_!MPqwA4mCt$2(@-z0LcW4k^>Eo>KuM~B@sNL97E6TFl z1)4A2mU)d_2f0GJOww_Oc7q4(mz@Oz)qi8`E+3Ka*{~&X^P|?>khUM&hA! za-0+zz-fA;NCpK8V8&lEAj~kov2%5g?yoc=(AvRjAGX}w(W#TavcyO)!zy( zBwy-z_~z`5c)^_D?7n6Bk6s#PY%1IH^>8*9DYTP!!0{`s;pmNC!t)DD8_4WWoHDid z?f}^jLEV%i`>#l)r6O{$EICF?lGtwyEIZdkw3-n3GcpRG_G3g24WI%{ z$9%gN{?t7?aUhEagsS=Crvcft)p%O>j4XBnA15^iRW@>yZTAu@VcFtzH z7Pjzcy@{m*?pI;}+Li)cVqSjK+o9$8<#htd>v|Z!spzHUXXhL2&VAWwmO>TOz#2F* zLKBCt%h1UO`bcZm61+W2uiv-$*AWdy4%*JD#Q%mVN~LX?P?L)W5)_vf~Eysd%ifN06o<4DrIb zo`rgBZ)aY-Er1H(R(loTgeRKc`aiNY*ov~%7tdG23sIk0S|&| zI`ym(F~+g~Z@5Ak*#hsXsk%wMma1o}98R11$`-WqDhE~YQA+mXDy(Q>%<^37G)?hj z+kV3owb?Lm^=xvbUF5qgnn3}%i9dP8l?^m`M069e_$gUu1G~Si$r#Db>RW?Xxr1i3 zU}3e66CnC_N(ryScVhF%p7!Zs;o9%K&6EYZ3oRWH+nY=r>ML5RV}UVM5LU3?&R^3c z*yGY}>NGt9GBX1LpI6=voIS=^Xvm|6n<>r?b&=nFv_-Z%Mm7gp! zSI@=w{S$c{z45YBG@x~lPoG6l=DOXaZPZVlw2+33otl)CnYysT!Y~2K-zCtw?30-Z z+j4f4G}f{>C*}kX%RUJeNc7CBpe@lm@?8X1D0HyuJA7fg9{pXg(i_i5pHz&enAz99 zWY3;MKvcgk8C$XtDv6Yv9nuV?irv9MVk&VuUm#O*IQgealiPX?FMl0-hGD?jlbT|; zME&f##=f<={Z30HDUKa?&A?`}^JL%n$By&#!^_LLX#Hw!dL^x^o6ADIYq{oZ_wI$f zBPDV!nu9vX(9U=M4q63-<+v6a=_auzKjbnp>~RgNBkd^lU158+SLy@%Fg|_0De54h z^rK{5>e-9~goCutBe7pS^s-`ZU@;qFoc`@|Uwyz__~mA3V5aaYCZ<4e6g-K3SmT;h z@it4I5vQD*>)Q*Fk+6`Eb4vzkclOo0&Bf~(wh1Wr-GBRg!}h;jXKPr10(}{2!1D1% zZnFF}mr~=Vjw0b47Mu_oQ`l$EqB>V3NVJyRF^Qh4r|cIXJIkCIu|e32zE3D{>g4&%2EEepV0ihrnN0lI*h$OJUUNEJ+f5_s5*kt zmQfjSrXy0*UszZofNBGqi063mn#*;wW}5WUXL;JVcPLTyPpbj}@IfE`+)C3>1iy6( zj@xZ`!%VYN^QX6s+4^nia$?ubBc1sgz=wkk0rC;u!2s(j`^WgqwSUq;DL&UAG&u(% ztx2nnfUn_>ZkfgUW8E9g}L@NcOjYNW~s;MKbcH~h0cpk{_HWNdfijblYz+h2z03P3!{w_^F+Z{6(m;mYyc?e=$R~S7W6r)rmnhc^ zWDY8UgC=qhHXPr6E&p}OFapx)Yqfq0c|%ScJfo!5%;`l<0^eYMGZSctYCudt4D;QS zllZXAwPzujN)eGld?PN9>@xFHYu!q3RYPgwD4^+{ZX+R4pqMO?|LJJ$&|pqT%}z(2 zws%$GBS~6_4OO$4U!NF5sidchXC;p!pWSoPq9I=D?mxL{Zt)>jI<~1LE1+Oz;S?N` zsjnlQu+gxjSKXW_*MzO^o#-wU70)7mu(uLfuB-0YqK5E?-e-<1nICGBYERzbSu?t- z1J9I?E{8Qu_&Px*?|>1;GK>itJ}M{~z2zc|c`DfS=_rwR>wbvoH*rc9Ca=CCq-4Jh z+IxAat$A_beud7*u*t20_~6e9o9BJn_Ho1ME|LyR2HWhz8j>^3+Tpo;1 z#OP$C#H+-wZB1(eXsCdjH8Y>Be8*l^l2z0+y_nU@-|33tBxzRwJX*%MM2dIi{#=IoY<7?7I@41JDTMl z|9r8UIP#bjPm~nR+<#Sib?~q)WS#taf5E>&WYVfkl0n+1X*26v+XO>&f<8pb)x%vS;$rMu{Rcy+BTIL?an0i7iczQl+`d} zYwfz$K@_rR)TcHqJ%uE`{3$4djVoPQ;Hn?ilq^IOYxj-eWN$8weIZ>f`k+fXTv4XV zxXVid5tejj=$k{SJ|9C8d_7#uwA^RYU!2J#ik0bpw9U$J7X!0I3Cu;srmBFnZmXU! zu!~xOmIrL+e;d4Fy_Yn8BTM_b>7-kEqBb{bS3=bJ-^ zArybG{xTk8B}Ff%l0yRj=@m6PP)-nCvyy%R%;|U!{>YrP!}BK`AZ-hu>ElmSHK=&> zEupkk&(|o!b>Z|PcSs`6=3@`isI1|I>wG~8HCk8BNXvslF zb2qb{NmN5#uR-97^5i7Y3#R5QJ74sp0$r%yKu?ed&+ivClsUAJZB~9o<~Q6;L}dp| zgxwnq#X_ME*@s7~+yMyT#C>E|gD=JjzeA}2|Gfez+Cs^Y@3HvO`zi4Y z2oH@RhUH`=t1aWXIifih7aEhgjrV*`ZHH6adZ_+ar&ZyfD2E$B z6i?p|;Ppl5a{2F&Nn$CdcSjfBzTQctXYmW#oGbBx!zpUKne^JrV-1O*A zte39UNS;l(F=?FNaY}cPnV{;IWxW<}kbX@ieFQx@krv%HfvG%4XlKg9O7V3+8>hFt zsZ_-g>;fy72bHS{qLMf>2diP8r87W*IH+%^i_F?^Vcf&!KcIFoE=h>1+K_QCN5_s_ z4q#&aN9h^Ld$%bf!>GnfOUhgzxE|*hE-EA?ojuK5A@-75Y%0`lR@w?JsH>*y%6tpk?I`Tui&N%cfoY1R<> ziTCSG=en`fKl@2rmFUkA)=$oTW&^T_;Wp@KWjYX;@4#NB@x@!36O)_Th#4Bu=8*MK zKC=NwyP~_@yce6Gz$)Y@)bwMU2i2q)9rf>$?y76AlgTZUdG4W6;#_}FOmo!8WcV9? z=tw8waqML#6=2IOVbtwANc83v@=3>m-{G0{Ny)8;7W=g^yEtkE^>yoYbICa)d+sE5R5 ziLK%3zGNws91-!M=Gf<__>gK>e=N=WaVosXzjacH1QSgiHH~f)O#=+XaX|Rsy<^PZ z+N0swA*aXW@XXfN_}RltlFet{@n-5?bzS1KAire&KbctG3g4A!B3yFxfvaUB0=oHU>7e+qgGXcrRVL zaJBKZ_7?3UZ~OFGJ@XP}4U>$LdyBF54(1j_{1m|hWwpUDgwKj})AR%%l7uYevu|w~ zkBOe1zQNCkzkSc_-nZ%ZL1wYmEb(6jIMU>7Yg+K%!3ogU`%s>|sEID}D>#`ArT1Xg zY3DbPR2EFVq|exiDiMyL{;h7zv1OiG^7pKqV>Nm=z2UX6`q@g1l92J6cc+a@kZm*I z1)8d3#;T!<7VjIabqo@eyQoJ)37|fr}Z$3c;pZLeiyn9}` zOV#On7kX{lo-U2XtHNsMgs1tS-$8(nM4yol$L~+TU_|hSo}B(aT+{L@Qqtw>&LoFVZ&5)JcX<|jF-?{%dp72IDUzD0V*CKhi2*j^8=68STUt&br&iVp zT&BuNStFLR+Z&i$V42R4;X^c+lSmq13oJAc!GbaOKI=Lp0;>JnzgjCjp67xP4qg9a zdR?9CTpwbT3D8_T3Xu@c7&a8<3RUEg#=nkbg0w+8cqc?u^a08zbMm@Aj|2z%eC+0^ zql|__mJH(p_&ZY9I9)`pcdL0P#sxFdeI2ZfGdQl2{heylGP}w_1jKaz3a+xS@%id) zUXNpAXIJ~d{kp)a&3uJ>KeBkF0>+^h%Q=^5J_{f0O-z>PK22*&cP1cXs-$D9ble+= z=~ByXN64k!9VyHHrr*1R(d9x1ns%vcOG)`V zQ)GPJ#*rwA?dc^MkkKtXkNRsa6q5~dJ6-YNo3j!4o!ms;ejpQ=^?m|rTJiRsg{K^5 zM7|8=3C>L;f(3o71q@ZNtzz4^=Fuj+G^&VWgU!g5T&)PxJb%5;=Q=oV5ZTVL+>-dx zhhj@57~9XMJMd%ThH!JwXU+%2)FLU@1Uk_VOT~m8v)Dkv{-tP3(1{W3lsxylL+)Ams{`mFkBBHjmQA(dV4hlVkETa_SZqb@%q znl$-FD&x1SE-}P^LFZj6804F6E=n>Fjh=Og^ix@pmsBrc;SD;KvAb}^#tTq|XnPVJ zpT2sEeG7j1wQD4@_IZCbtQ+%9$cJfH+nzm7ZuJ_=8dWlMMAS=kbX_atKBec%d{?j6 zMT6`Wiljm1dZ+vZ>{ozBVSFPAiexw&_`jBDO04g7sG4t^{7&T_s(;7^OJkPNAk7EeNPJB+3 zvnI>9baeSf@IPpZWe^9Ev^W9*!{4{x=I31$Z|j8kg4qYeZnj)K>zaEC-uPo>RSdLE zc5^nm$Is!d8}Ln;f6P3~vKgXj)_-B2uSEdl}Se4P3<09 z^@w?vWg%xH_Jh8+7{G4dT9PLFNw#Cn%B3(2XpP%XOtP_Pkbs9kV z$Q-3kxGQq+N6qKq^axgH)t_hF!-n7lva+Iw5CB1Z-2D814juglNK5g0+ch`iw<~fn zBWiwk;dB}#ap%1RpZax*IFkCNe69y@xvGr^2Afgy<;hRjPZ&4)J9UVSLbPd*Li8;& zj#t5gx0#(>uO7y{KHFrUSnY5iQ0@N6dsnw_XV|c+=cU4sBcs8D_UkF3q_a)o2PEyF zbx!;+GWe_i*JgQHGt(zo)>&;KdH-r4|K=fgzy_@zMbL|azNlnsLrvmF=z&Dr_F>=o zOyF^3ZU?9&s$M>Umkl(GgqVraCNJfNUCn%G@b_nHt!Eto8>uzL_&DQ#UKq=` zEOCp8rf~adZdQ?Loa}6dzb~63LkY2ne7g0#S%1Qt>FW9*{J};0(eM>Uzxxx+Jc=Sw zNbr5M_&QPzoZD-!SVIZ2uWzT1bQFtWLBLeutjw; z$)QUUFgL}$slTMW_j9~~-^lx*3A=|OsaHGxyolndAN+|6ft0Ht44TqVo7R95)TnNp zQPr`<3|W_hYJ{+oFnY|oclbRNqpM?1ZI3)7DWPW?MC-KgzoKB4o$cuW)CsOirDD1w zYu)U^(;c3@$p6$5*I$McZuo=gLiFH--|M}MGVvfh^UWW1Xk z488s>afB{8n19#I#%Qg?lGX-cA!ZQ4>3`_FPJvUKpF0!VF%u(QnO~)ezL2D@n4T!J z^TLk=W9ioU>M>iMaW}C(=-VESzwQY4UB6i(J)vX3hlOv*D;9`p!YA;Jo09ZALCS0x z``9xT+*}tmjgwkb^Ht;=)Ha!3m$Ej3da-!tbc8;59KaUhVqo*5YWio)fbPmVPBcs1 z+E63@FJJHMU>@vmiQydDtYDEDw-;?c`FlUhl)EW~JP2Mw#)x;w4hND9y52uN1_s_U zbd_D{vg>WVjMxf{SyxjYYv!SG;qijw`Avz%TbMSMhM?mvIZsNd^g$c$N zjY3h7e`WP_q^S_Dy4f4fx-AJ5imltL_1J#=C9HNs((E^m&@8SiY?#ONNoMOI@>V{| zzt8Ato5|}rgG6+Vlv&z@Jl89_!mE$lDYbygNM$O9HcfPZ8)J&)hQ5)GD`$Pp07xQF zz?AEtd23`xy<1Ka)JF^Wrs@gF){X)*UPwPU%$$DHY3tQ6>{Qy( zI+f9}N*VO;dNX^!aO=whm+vK|KxofHRE+nIq|`WcH)SPb3^IW+jjZ=GtMEFhD9ZBe*g4qo_y3(B`47t?#J9n|fsREt^6+oZnYE|O>VMg+UqNs?XySy+NRDe)ZhJ21Dg9^xuAx;~ADlE4?&9K+FY zLY4OquJPQc%9&G=agFz$sVapHEv;W~Z~-$7(71afdx?2z$CZQEcPm+W`E#ptJe_EF zNs=>4HZsJh-4Qn(h6^Ly;cS>|l~Oy?Vb**xPSqlKMvd+md;Jbp5$L(AjPu#&qk;SC zAt$%M%wCWtQ^L+WOVlob&+GL-GaUCk#gJ^FLpSQBfr6E<#a#buo+bMG8I6`=zw;r!Zr#``Y6%cj7(T>{_-N(%43famwv!j2H*;aMnE} z3GVb9&|gq~f{@+%UQ0=%)KWoB_Ja5(-oZW5k!XrVeL$#1)yf?DPP>*7gtBIkO=2|+ zk~!gxywqm20328+c`k!6&&}#+`iC12b(fR~H@v`kgQjgjkhYliLxiiTJFyoT;X5wY zcxSuxt=;A-b_ohLABKbb?a(Jhv(SoLXjJ*6#VgC^Io-IMR~6zl(u$kjz>u4tzd>T> z`OWiT@O8#+O-b3Dj>Cs(NV8K4hT@nw0v)>J!1}~dmAfC&V&Zcm*7+tb&a0Z2n8`=t z%UU0!STkH%} z$Gl|&T*vRGX=^F|=5m3yDO-g-DW8gQsZGYyk=GWZYos0>I=7MG=mlij%mv9*cE`-i zOfyQu?`5;Xqoa6A?@IAVZTZ+GKMps-AN9#tA#vufqKlEtZ$svUYH7;UrL&7ymjs2h z|KJgsm=GK=mx9x=_IzQv$QXlsJgVYsJOU@iW2Aue47K{Mnr(% zls~)ux`ll{bGrQkeB|0MiR_WX)dU3Fd+OF-Ge_2T_8?>Be~_-;ZvT)7Zx!wtQpoYp#(5_i;Y-fOez&Vj(Be{*bW0QNL}yF}Evr-^v_z zz`DK8xp-uCA?9=`PCl{K9OF*$Cm#5y5;OM?SL#}a#eLWpBhNG~@!M4?Z$4jfC!=gm zwl??6gY&C;;dY!;dQ0gQq^Oe0;%f}`irfoFJIxYe)A6OkkC#f3**Mwr55;81L&Q#h z4uWd~D;nFML_bM6Oc{`GjE-N8*A4VR6tbVinQavNGX(AZ9ne1yAqUQbT+waTR?Mf- z(1^OPqjl>UaH%1+UOZPb@dmn)9aTIjh$&r~avj7?&MSZ7ScL*zE({Z&cFZKv6Rs=B*a|GANc994A_xCl+Q`(OY-EcW-Fv$LZe zgIZN8U4pg4tAIGcvk0PLjwhoB7aq8huIOyN z`E5b`yf>PB|DN`}Lu}QTO#It#`Hguqc>QFXWJDlzEvMW0boIu_)MOBy(+b7MyFJ?xJ&+m}|daP2c&rshQpR z)GHe(QM5MdovXb$_%7Y(vrNMUtr4Yjn!qiQA=ixG3GH;1o_+P|hR5akMmE-M*Ms|i z1zcxF_VRVeWruX?W?FoDYr)}h6sI*;r_srH#qEkqTOKig7dN0^n|V^>(b-Xe>rT4A zPq`G!qtB#EBi#=wtL+upix1#Ta)5CyiF1vB6@sz*`dEY%4RsHD^&B9-h4mg`dY8x7 z_qZ?9dG$;j%KN(2{QcDTEikCJ_Yp)=duVdShqLMXqUZcR+3_cbp=_-2mp(`Io)J~S zFAl*AZH*t-rHT3z-tb6K2+XM0&3jcV?|oi06Z^?-6K&(f?2Z{PdVr08yrcFtJ=|C( z=PdRx-g375e6xI@43*Vhqn4SE;3Yl~Psq70Wa5WZ^LtC`1H@ip$VdGCBQf)3_^>k4 zr8Me`cr1T*IO|7V`=tNF%G35Z>{6%pImj2~0Q;yab~CH1QLk2})BHu3Nua~R0DD-H z>A@MT%`-#?+5~~3RlX7mc6-3{YnmIpgXfG=rKza{J>QoaRBXcUsfJY*4uWc4>uX>f z;YN5AT$9%>?^qn-sI$j#<{O|-pa1DOuQJgXN#A`IctZ)`h%a1qXvX{lQzj*xYo&<$ zIb$i9ixGfSF3|K1a&;?++Es`CP>1Sx_`Wq^a^Se*?(=izf-dxS^D=3}sYHF&%Wb0k za~X?P_o-`s4p?eSoIb(zv`qwQMo`-^0!B>BB+T+wm3*IbheA#Hfnr))SZBHSAZ z4eS_C>y$B@v{{G>!U8*7kWc{peLy0kp=;NT3SR=uIp1x3KEH90sVP5~g!6&rn@eo8 z)nZ&OldlPLX+U5!^1U@L)6d%grvfNvT7d~YvxXx0yJV+JW z>V$;VyO-ZZvijEI@THu7SJuJ(+inZ3f0%=5tYhab7?M?1VO-R7eYBwUm2FEiVl{W` zZsI228CZIWoMRr6?Gcg7e9e7Bm3{3${S-VrdSRM!kyYZW<<7V>3@JJj6#^W}Q#Oyi zN%4)!(CAN#GA-bbNg-<&troPLENSK6__zm49n`e(>h+4tVQV~{ntLxMDPP2`Nz9UJ zH_j{E7~py=u6`1GlT;;)+-1FmlHe*=2^YZYYFIU}s3x(QEt;e_dp5GsE}GS;Yjfwh z7WJAw0GcYg)F&#+_2+-yZTA@Mp9OM>drJzdj~zNDCUWcYDbb~6$2~;H&5@&3F5uyu zlpzWm>RN&8xG0O4^Ei0%)0XknL?Gpx5$Fvbj zrjP@9?#yj#Xi7eUK;y80gEP;1%|p0ir#CX9vKy}2+TlYwuq!QV4cjgh&3SdJ;^KdA zrd5@meTVihq&d?MrBRe1Lvi)Yf8#DlpkWs*b>Dg(qi}a)aFM=VoUPy8)Vd+T${eM{ zn89PbY{>3iDWyJGZ~XnG9eM0MKSccm4XG;XWQ%qRs+l(S3R&(59I)|IoeUosjNqhM zul>F@wJs_|#T-%vEua08J4^~3u%sFcdd&PM?upyceQ%p7e}XY*D5+1vJLo>+gy`M# zOXV{DQ0gX?5jtyb$ECyt!sTCR6s&`L{8?GvqU`*yxEA@yX5<-_Th;O~_UK4KL-(=U zgY*m8?FK(arYzh(_X*T2IqCB>qWd2pI>l;Cdf9nyNZ6I0^fkMVV=UN4-YDjfAN*9y zuGA&CPxFNRUGl;+pIsOao{pxAW5)x0aySe1>=7zh9G#0S{5Z@B+>?cFp0qknz^GCS z6Bl=f@_agDx+q83L8Vgy6^e|c04=289z#@%)S~3u$sGQ@#O=fR_;%re z{piCv?e+oLQf;nbp!Ya-t1~tpDHqL@F!dX6y%tVVF(E6JmelcdSdJpCHb}2;}aa zkk@zgTc?BFnc!0xqF%uxtrDf|_@ll}db$DzXKtS0nY$x)?oyw_<^k($+OZp!^JV3t zqH5tCLsBDTLEhi8`b=bhnJ60o|M94@fr80rc=m=vRMl{963-HZnm{mC(<||dNX8Lw^k|t^_-o{YXWA-TsoICH6tPD%?-ZfK2mpkDK zHKi;bEQ?_1qCcToxpUrTS(0QyRXrj`DSAkSu&^t51+cny?fdvNZgWPtp5Y=K{br>y z$ueJ`_-D~ANmmIx-c6(N{tjp;N!Vgxu`cM@hv^ve=8GF?zR zK=wg!M(GxY7zq#JgTlCd*rj^aIc%A`z4T~MeoS~-L$7tAqO@8?D`jRg6LZnH{+iH5 zsqdFfY~M#4AN`&5w;;*w=>1y3etqDPDNNQQ&;*UP9xbpL-8+bRstIN`Gjz0UZ(J#` zb5V!yFAQ$C^iF*Ib-~qE{BI>0DIP2a8KgkXn8~2JW=rs(roFg(d+xQ5{G~gRYcLP2 zvpxnoOKx#=3VU~tZyiKjK8;euXsnS*G_BjL2ozE;;ozoD*-Id}SCnyDq>g6J?ac@q zYtQz3*CPn8_C^exl^@oW>{DwX=u~i8@NFfLedDg<$f-MYd#yOQ$?3lZ7x=P}MZ_iG zlJ7>8Xab@bK@qRtYOg5(K;I+!z-N9NsOl+j{(mxiPTW1=EDeEB&S*32c{p8cAq2 zL-QEor6gyn{fpi$?UZdOh8;}^EcDPo46s&;TWsLb**!d-^UK>_-1y-}Jcu(7B{I8x za%>O##Iwe=R|0O=hR*i_5)Ix4L6vT%0M7~P=zec>+bfO`jH5M3@8f!a{m`j4dquPR zH_iLI2iDDHSElfWyDqG48tP>a=%I z?|0#@f`xRF@)L76(_pQ%Z>Qxv6_p$PDKAYWr_i7m@tEFPv_LU_!9@=I=3%z%KRi(a zvdOJ~bDuJ>*^y(lGt6XAHu=?Xk)O;_{6Y>hK9su*UW{^45yDx#At2tg!huQ5gq!;z z=bqLpDqHH1c5Z~|skW)Z2r0{M99}}a3r3G4=*rc`o1JiVEy*8&!Ih^?7cr;?Jipx4 z{0FUX?VG?B)}wPC&QD1c#++01q;9HUv?#Tm-7)jMX=Wt!dmbh zpWusIE@O`jmu8<(HkOy4|CEQLZIkXWYm;jei4t+)W!kBf@ML|H#M>~a`_~=ee(Nt7 z5Lhu5(x`IZgL}P!kOziuX$zKO#1s-a1Cbh;&9=*)O|~Ff4w8+~ZmwOZ^Dz1y@ATWP zV$dx^85>bx^Tde_2v(gX@_Mn3cl{)0J=G5XYOBxqw>_xj1%gLdZBTu_JvfW+f%)lQ zT6o_EhwP?1r+_(RoXlrqNHAfIAkVipcMEJPD13cfBt*f=UozVzQ9$;r(#tyc5g&fB zR6ilW?pNAe=MIEn_5bBVvx}U`Bzego8U0XWPM`I+oCWeI9UB}|Nrep<_p#0X>{z5% zD8~JGTyqiSu5rgWKXX!=-}6uS-5Z-b|AZK}v-F%&S(6 zEPe;|5fF5G|7eKpC2P5Hu@ zxXbm|NgqQx`l7Vy%KtK|P9APXPkOJ%QcpOaCG4i4Xeuyhb$w?AR-fN-UTc)L+T(FQ9VOHyPqPrC? z)grB4n=O;n**2AA=1=Yq=_l0n9+A}L**0X4Vs)YqRQZM)FQPynYW>(j->PDH{cQA7 z;z+-c0;7&W{q09lboEzA?YUd#mE41DMVt~D8t3GsmyBw{%2Er%A${%Hx`|B`HB}X_ zb4WWqF+IsX-IZd>y^L-)bxC!Neb{|%Sk{5uGyj{FKk1Y63yBbEX9|}MiAnBb500$5 zx7VE7F)#S1oo?g71etXDHPL#-%0NfmLs!}NCqH}lU+8C*GAJsH^lDL>Wtj!_RD`?< zaHfiI*blCmi>&wQD4JTq$*Z2GuQTg{;sK5M-B^^eh|UR8=khTgXo>kx50V8|r;inV z!)B0AhurOYjrd+-SGDpEThfjoK7#SYCsMWY= z>P7YkL5+9PBB1LBe=C7)A={TPH?y=;=u%4D>q4$|kgI_0(cn)AM?EKQC1+_ zKtX`)Z&cci!uc8Au;pf$*HS*@=7AL4=I*WYUQyXMoirTQcf1}d?K&q&=6^RNvgi~4 z9t^(us$1rfxe|!T=JH|w3pv*Jp|}^Re$@y;eC*>{b4_#10U`K_`~zK|CXzznaLMSQ zM88*atx|VQ(@>+G8n~djt&3|BZ!4f%4m(OHQjz<96m0ixKXfpY-=2VC!R5^CnxF*( zwKtBn{gb*N-NpN|qeQR=g8@KpQXDmac0nBla4)}2?r)G1c2LXIoX%&_!h&k6Zlxe7%cZ#Cp>b_Z#CMUt7GEg2T2-l1VO(=3oEh!?bzm z&>D)f3*B74eq%kzJ2tBGupu3k;ayq}f_rR?wA!Uivbkqe^h;{{pyZTmMSYNUz2Mam zlPq15NX;Kirpnns63I#}cUF-qq?ssZ6s^~quu%x3Ygls-sb{0Yz-X6y!kiPgQxj;a?=n<*Vp3XayHTD@# z4+Kx|fC>H$%O_?rHA%z&Yz09}1$an>(m!E8bJm-s_=QF?#~{aET=lUZEd(p8bHhpj zbu({YXPZHzKrr?rBoC4T4@#lLdWUL;K;Ark!9`|;78CR+3c{Aad~tXIOpgeA&ZUi+ zmR2VTFF0z@#$LX1+tqA2=K&wrCwY7rOs`~@J&hC>7;KjywBz(^PV7X=KY0fLj!^;d zNU((50g-@?a%j-(qJH@$o6S?V#vV$Rt~eGx3rs4iQ#%^CdhWq<*{n)R76NFhMkzy2 zgK@sU(m#7#K)|0Wm<;q)zB8p{0s5w&D_Wo)z@`@%cpZh~--IGAE`9K=mSUS+>^$Xu zeqW8$3>z9&6tWFNnqJ{Fn?-b}uvg_^%?#7R$a4K>2Gf1aBgbo%X^QLwIP$>pKBkCB zLO%UxlLbl3sjL+HZNntR;+Q;`GOG0Z>jg zmlY&Wc7YiVVHw`nZ>%*#%7Fo)p?~SI=nfO28*T;G_pQZ!sD4_62;v~;%j#8D z*q=JSpA|d$&6QQqBQe9VjC3 zh9o2m;i>M00DtxAVHEMw4=N1Ew(RWiY8FZsEiB`*$`=+<)dQB(=hiOOK44XwAuHy6 zamDmm^V<^NVe~SilUnwr*1p}T=C(|B@1tT~SQ3}{otzI=k~-!pS9H;5pCu~&`THa+ zXa0_`E<-ZbP}YXe~ecQe!#dJ*3NoDRAb<jpsxKx1@jJVeo=*MjpnVj( zEE$NdEEJSe@?tM9E^x};X)+Cdi)Cl_Gr!OJ`%D@q_N}2!8|BRZV}VzIPC8Y)kO!em z{P`^`La-O-bi^C`km6*B?ZZ!WFi%7gX|RYiV}ZrEO-+!B^(3vWxzlZorFZ+20AI16 zsk3?L%H~0FvcJGb8APAmE^m4~a-zvw>U_+;8Ur`Vij3nQ8f~P81WH49EkQaLNWm1t zM7o0H)%p{oIs0dG`uoluD3^0?Iwf0T$HO77n?1>O`-8||n5atn!MnX@D_5(>O2uAz%5r!#A7&QQqQWT37#AdY44R=aACIL%i*Vn zD1kB+ac@8e(U6LP3w*FU27y+5TGSbT6Xg9MdctdOHFnfeh0^6c%2ARj7G}QA9~p!D zIC~01GSW-?fL3JqX^ZaW0#x-9tbHN>hA|#DYRNY)Wv`;MB7<9ZtgUO&xL38?#n?eZ zq9(T;=Yh;D+iyktMfRK~xWASX%nuWkI)~qU38o5S$uN14?kQm(Dnq;Q^F8fg*cg>TA4oJQ%ZRlia zmQib%rxv0jS0I2m9;|A*qlIusT~9EdAgoJq@~=lMuzq?k24_6H&Z7^>VHNKb(zxxh0=$Op<-76-3k7Eq5H35 zhiuHU{rGE*qK5bYJtPvH6!(UZpeL90y+hvpwUK~&!I+-uL&=tfRXk!4fy7<>mg0tM z5gF2*zxlCKh1W~S3>`rYk&WRC+a;pEAN9SXOy{ff`2gWH#@>(9XYxcmc_BIEiJg!E zP6c}dE~s#gXT3(@VPW28<@VkUawKroZ!OpS$FM`CI1r;~oRo$Ph;w5?P;}beNgZMjCx#g4!?? z!&LY_^-$vBc0N2cSQCj6NAI6f>7F|H2m*!)h5|37#U=ZoIu=U-3d-WF%34!MX#A=^ z%z5PI$)x4R;g^Y+YDSs6oPji3g+>0T4J#P_qWe_nY`>vwl9pHQlJRVc zPR1Iy(h^veY%P|fu4G=7Z5WjeSRsYh=RsxWXQwHi@)BLmi+_`^mUI( zU$+l*K4j(~_z?KfLxfLCT@_ytJ?ZMMYwP*yK_XV#d1PFJtFw6I1t>;5UZK!F%l^{B zoxcsbS~yjiQVGh|!N?pHqirr2u0JA1#vzF>YU>%X3OYaK9$z?qB)*g}h(%|(fe9YD z^$pD7c%k>HaPB?O#14wkq{Zp9zD+XCE6<@^w`@k1H=u5Dtc00Q~_-C_jie3UGaF zF7FBlP>@V|{o%B^XZAV+>uOr0)LlGr`=^`Ix6(8T`ycn%zK@%6cAl<1P3K*ujBRi8 z!N)~r8u-{Ah=u5rVTP>-G0~EN*`uRe8YKQ5eSA+7LpC-NM zR!QT<-p-KjZ(F@#BAk=EU80_U`f)b$R91 zh&lcuyf`*4ETc&Jpjx7JH<2{6}dyAD#bMhmt zPI(>Lz@=zngFxv1B>?~l6D4YRAPv{OE>!)`J2ZV~?_1<}%&vLDdbr%N0S-39S+h`~ zf(cRcP^+)rJ!-yW2ejKSi^F63JjdeYhH`?Z+b?c=;Xd+)FWpscIf$x9#ZzwLPxnvy z_CkH|4d36FMx5ObxicOgwbyScPr0L*n;yk+upRv37iF~9@2s15ywam9M@lgmuIfe! zs3Pk`TjHIXez0JR4AVjXc@(8l4M`^$FojP1_1G2fs5i0YmUVaf$sgd8zbAXYaBIJ4 zaPR>700;nj0HD7!AOJi7@L$BVUm!F9U;t2eK$t$@-h6HVfLYCogCVy$$YXoA5Y3@xh)+T_)!ZjoX`QTufJRt&hP{XVFZGdlq$*Rk~GED^ZXW-&Wi7HPzgu`!Dy4PQ3K<( zywFs-+cCOHb!UPhD7lO9((Y{*j!=gcgpO^J>OS7vRtGo$`9d2+9Y7 zHHKGd*OE#6pc}7nLfksM}n%-ekpXs9W2`}q5{ zEbEwW#6gl%E-O^p!L*8bGwJHe8J9zh-kzGZL391=oYs!L)pafLQvMO*Fcl5~V z8P%27S-LGoH!k&H^)dA|?d#{)$hY+~F5J~{>%X@JKrQY*M_fE_)pG$f?6K5069Y9Na~@+#nS z0P-$QE0Apf_%5b9FmC|9JasY(ps+%?<6pynNabOge{IbXu)<9LaVpT3DPEL9U^*=3?(8-QjidsBtc1Z6$#8Uo~1tuf;mQO z%is~(#lMW=AL2{?V^&xv=Sc<}$2v;M)TJqLRb(@dV3DdQd73}Am}nGQN9HMxb=G-# zr1r$_3ghMHEB;|n#2O4|ki^)E_8lfS%5?A_E;uWb<)9I%n4@(D(h+KzHG0J964jf9 ze~iP-T$|K1rE`k)822_FY67YVR2jiCk*SB%(5vKgHRNiFxrA~>_sa2^lDJ@Y0At6_ zrkZABE1uY5v}J3_tQ z3k2`W+69lAQDn;SpoXUE9k0czguLi|uSK+m(&}BVHRGn08((njr+{}S&5c6eFLo!{ z_IKL_eg*0Fx7!7O1^xE-L#Pu`Owj$;kDMWlry#A2&?Jn^AXJIyCWvGTnH3_{ucL5D zzVl-xtWy9vmu)W7NW_Vx6Y-4-0#ENeBoDx!wAO5+I`eAtbCnZg&l>bQ+t6kI<$TtO zH?c-Iag&77e3CQ?)tG~03O7lQ1!rbdYJrP|UV9o|QR$h?d$z9$g*qx)L#Q=3*C=g6 z=_S`pFZ3C3NmUi0<4JEoR%~S^pFEpipu1D z)$y|YMV-#VwdIa8CC9F{^FrIy*3q@dOHJDF#2)HHIJmBqU9sD`*M-@AG2c=TE(*jt zm{QO{-$;CL%s{NcjlFRz4>uMsOphpLfuaHiOWd+3dSTeyiTX&+!QS1byO%d>0?{8N zB@oaCH}>eW!#ZxUy0e%`^UCxa&#X-|k4!r_%w;oQ z(xIgY1P0$%akLD@E+c##$YY1f*wNGWH8&%@9QbmFDqb5!Be5>|&Z2kgepR|Vppm|@ zzP>&)Yp$Y&HsXxkLrOr#8z?XWw_+Mn;B2Je&&{XWp0c4X@L@d@eSk0^w-NMzrobJr zDh0UGS^^=oLT;wP#%fzf`go1iEbo780mSluHlfSw#md;xacA>VDUr_4jYU??O$GNU z^)Z1@Bv454(0gvCz|5HcHhoaZkCGFY1 zBL15WE8sgG9YuNgTVz&AlXQ&$II(fOm!2Y@tRSy=SLju8KjS`UK^)l`*NLo`tT8U% zU|D=1d9z;~n!*8&P5k8HnBb=2O*>FS5o#7C*@QZHb1Xy4BTr5M!liKVCvG=)arM=M z8U?^LX6X+BpA@<{yENYyo1IdlpJ-HpU4>n7RAkW)D(PuIug-iAL%F0`e)}P@ zF0wZj%WDcn6LE{eS8WHGoHR{ha49V_Bot#VlvD1LA{&u_l0-J!Q1QQN4_X1QXS#rr zg2+X9qy3Z)`|n|rtIoca2a%&xz(1V-JiIFc;tJdGwsYL94|b4K3eI^fjJ9XD*}nI+ z=EDv#tBFKY`)FH(xHhSlmhj3iZcjN~xq`?5`GE5<0N!e8{_K7V#(e z=I56iKKyZna&ofkn~JG-0Jc)UrJq*`6mV;IXx#^DHUv7@-V++5sMAstmb*iJda>x6 z(C@R>%bg@3ZO#uREUef2(gtUO6vur(Ou8S4uezfBpby(j=$gTa$6MA$e!!#QE9*|I z#&MsDa|pJ1U$n^}uj>$5h_I%mcmQaId6-j$6N69KAM!-Bh#v?OD&g*FT}Iqg+Az;r;Y+l zV48VoQ)MbOdayno99glE@g2}(W^E2NfqvknaGOAIXTFKq+NH z!Z7V_J?breAgSDl(|F|iVp$zj9@(5~C0b3rYN#PUsy33YgKLS5K^8B{MhH=`Wb%j> z7Gf|--&xy(c;HwXfr)Y*l00V|0KTIcl9chy_il%DC0WlCzm@n9 zcWe)LLL!maQh};T2yI3B@`dG&c&yxQ@vS)l?o5i}2ZF_lLpR1bFVTWou5F(4Z!AW= z?2>bnsezZ4QD~%dW%9E0E-T9CaW=Wkn7b^i-m%Kfx5(*3pV-DtBSS7X%wX)-0X!LF zw9O}}cZ$ASB&ZjmTIIH|&{h|oQs>9D^FE6k*loa-@^tWo3F5ewm&uGbg3nK%GaKn0 zbZ`bd-}1{t;fm8#QUPZRhIZQ@OaD82^48c*!Qi(G@x!&GkiMG?E~rHx7LXbRC(8K1 z;GS^%5w>%3AgucVn9PN)`Tu$>_f9Y5PYBcAPmbSswj@6yO7A2%KtcxS@PB&F0Lmb{ zw|Bg^Z*d5vueWy>_AllEMl=QoW_+(8Sji7uw4C3-tAW5YFAO*aiZ2tx%xg`5e7|=< zf=obw0jGGZMEDs-yrRB7AVA3){4dh5JD~9la4kLq0@&@;QH9Np_5F3+`v3KYHq5qYD-Y#wFh@AZ(B%ghdn7P!NxVO&ElwQJDr& z@A@T;j+)N3KB|P4IWA&@qbUx?2j{827+bW-S0;k)G4=^rfZ|a(60qMC07&LgXyy>R z7?7Rn5UA>qy&Mom>`~cnA?R*teHFCU3a?0>4L*{-f|499n>8BJeiK-})+cRM*Fe!o-Dq1WG4@-tk0yb(LOUO^sTAb~&`N$WG>&uuf99z;YaIO1;F6$h0 zxGN0{4J%HoPMc0+PD@(7Y{XfUspMLb))p(W@7Le;+G*kG^$LKRqFTa^2_lE+Ln5FG zH1d8L+|7!i=QHXnBx9$HuKC;OvU1^Z%=YoHZSfn;YE<0kIoKI9_DzW63 z!1EoK;v6^Q9Pi^CDSsq~s>e%yQB2MKZ)pI+rQesDqqFffFfoyRk-OgyI=HA|oCX^0 z-7rAT5NyMCaUnWFZTgQ58VHbzK;=N;LEQxGjqFA2Wos$Yfy!LbazE|MRbofLih7k4`WE3lp!O7+LU5KeMq#~fmqCeo6J6Q*)nzcOo2v?1pc0S z<_^m4mLcyJcBdiBxqj3PpM*53-aM+MeR*_Ulk37-r!r0TLa}OY0INEpUA5($bE{;+ zxq93s*JggsQ~1QIk#;`lyaup*zJXIriCgr`x*=8pyGdC~h7^u0l-N+B2<^#2$VqcP zvhUFh0N7&O`Is?kjoLW&+87YLAqSWv99hHA#XURBJ-O5)y3{=s-6M|8Bg+j!oHRsP zw=^6|l7fkRMMqi7$;w)$D#L}P<$CY|M1flxNKP^B#G+S<`OxJ24k*SWg|t&tYrB-? zW{Dow^nqAF**n4k1;tS*d6fK>X7(6h7jq&s3}leG+9{0 zAw$TQbYXlM3Vo2_vCnB0o|rl| zTvIBJz6|@Orc-#+F1^(d!*W1UB{rE;`_r-X#RTSZm^t2GGQEY684MY)iz-&Fs=o)v z60|CzXI++58biO5u04{$j=XV% z`L28Dc9<8(TXrv+AV?yaGNzWl2~SbqbvsX0)AiD4rsw@MEc}9Tyxf2FuB~x0$A6|Ji!A(QdhsqoN$Q!l7WfjMHoz>v1~X^8`!V z+_`Kl#dJk;)7+(EDhCdp^K0=a&9+B~c~GdpY_DVFPv62V`=DT=x%l&^pMbrz{(mm# ztR5UeAlffVJU>VhBtq}7HBde%fahmUb8LG_YG}aU;Dp@x+Vr55n4F}B!ltUO;*5~C zvbv6zu(;Biw7jgSilXGsz{>3U$j0b`#B$C25A+{!Y)2^cUp+28O`?PRbgXUxwH+Rp=!&`}1O+oK2-)1yFUimoxl z)uYrVxKWyG)ROLsu%Mwath0K)DXvj4On#XXH?;J_83dE3v=HKq1XoD4=9Hb$Q;KZ1 zdd3+E(Wg`i0y9pQ$VAb(B=x2wC{ygrdMe4e`q+e1?}1c@f7p6X#CVETr`!X4CnO#? z5mx{pw5L#-p_whDsms9uAr5hiy=4^Lg{KGWab_9L?oC{5rtOpmn1g}Ft#wSt_JjK< zWE(83ApUq*_&cPsc%h0sV)&iQv|H&xfNvj&deJjt*`~N@#N4^ZJ+*7%#rCUV+`?0oFxes z#VA7IOHey}rEGLe)G29uQu_9Dq{ti3MQpM5XKgIwJ6DqWgPhAPM^M#~I&xNFMufp? z6<5fE{{-*~w2^7v+~*f&WDg1^+1Q=SGourJOtFSw&g#q;kPED@!yV8%m_?BIx3xf` z&L*0h*_KXs5FfZ_uKyR1TkH4cg;Qg91~G{H+5no!cZ2>ZM=%GYempSRTHTmw>Z(Z) zgu?e-Z#_*jQp1!hFS6MX92`e;5^~37^9TZD;%DOu?+32^>>ouqF2QvLS&oD39c}jG zR%GLB=g7*1>3FAQjuQ`|+(78im|DwZ!Zhu=;TVPk>-rI1l5V9E!~PcZo4YZHuXJmXS&w)mN?gKZXn$81IO$5?I zL0YHu3f15lgTDAqh3)|+QEt*MwuGYYODLO!S5(XAbF-T|$$`#|#}2qL=0`jQ6X_3R zAowK&5IKN8Ukh~{tJ43(AXSHykRy~sBvlk}NXnP~sh}4tpw*lksRs>{ub{wZHkmJ# z=!D7Yv_G9LmG1Zp2!+OAu$XQJODL60rL&lA2Z~6gR;f3cZiUKdHD9eZne7A!iN)p& z8cTD;5G$HZ>$Ex_t;cA&UGum<9bu{@j~C5UplVwGqW=MxsQ<$R?`1?v^3^Z9(0SPkzN7z`Gp_255- z15)WsMw{VEjt4Yq&3fyha+Zt#zNO7bHO~he4yWVgU>Va1t#-TP)o>Np3m&)U{pC;v z+YPVx`~B5OP58g`*5IP##^}myzrfu;I==_?{L?Sn<||FHO|fPhzK!Oo9e2@ZN~|L+ zw`mDEg$s-2+EkZHGhpnsLDS~iC8pe`?31ot5ju}GD&42dm99M*JC6;n?Wf!qpIssR zw^cIUr;HgHh9%|&%)K~F)B7|((+r!~w&M)DfDkkd>xkl14cm|uRSlb%rezJgpcvLQ z>!_;cx=2)OBd)H=;*_mMdKuCQYct+o-4K@Jx@HsC^}KciKn00#7#~D!Kq1CH%nQeU zSPK{w3WLpHIoS%C6w5vi(+~`S{6~_FCz@fJ8*O1P{XmxeEO}v?eF6_HK?JPr@HLQI z(dUdR_C5ur#QO?+=RKBLRAbkR?{!Yjmox_|^&tm;a8=?@$EpB_N%H)d!#cY-q>Jz0 zP|NkQcR2)Y1Yr~aeiZHP{p;B<@7XXQ^xemf?2f%@7?!JY!5lCdO^{&WLE<9gLzLvk zv)N*?JU}7Q=nQ(3;cQST)k=^340N9RaqJuK+cET=&)bQ-BUmG^1+DGpShubdANl7;aGW9Y+k#XhM{sM}`67t6(K$ARdRLi;RJ zl{V~Rips5R)N==_zUo2WyL;BE61q4i-#Txz#z9FbT?y)}PW3ViwxL>~ z0mjKQuF?u(-UY`YFNuwkz8l)vIRl4b#UzbhNyC zuX12_u~fVy7mo``N5y9k(}9OWW*@i_Ghhqa5$W>YvVIv4Gfk*`Bd&ZWSKsFklsi>J zCyf?&By_Jw4t;lN71}E0(^hv!?UFZ3j~9hX-ZG@Lrh8F#=I@8tSMUg)zRnR&ZM5T+ z?tI>3>#m+OylvH11G)DM`qEhicQD|Bg4A5>3rByJ+cfd42nUAhYcday?&T4W6}Omk z_io_(N(0F`QLv)2;I1D-W0Qx~*xn1SVbJ3TkM7X=$J7!AMcAoldZL@ue+cKcBCbWx zjb0Vu^>SPJ7B|uJF7Bmte5+30MQ5J0zO=`lxqNsqG~lDGdqUgtEvrTmP>U829?}&t=p^X zFgqi%udmGVI=RN{^ka_`7E<0sz9Z8bxvz<6UlP>po)Y{mJPLN<tNU_Zh? zq?&Gsil57+9up#eYjyDNgr{cOeJkQX=rXJQmQ83Xgtm z7Bmmc^!eT_A6}~;H|+b!LaiUje#XbhgT+ty9N&J@_ujK+(H1CEDFsRI>#gz><~4dm zg|c7EvB-K_c!Z8ZdN?#>pB5>DM2C-2|6jRu?Qk3vLhz7LgFp9;2xaL1OFF8DbEEx| z;tI~SCEiu^yw1v2p}--9wDX=qMqOY(j9eC^l5Q1A%ZesX{xFQ| zA%Y$hESfd9d(R#v>25wqJk0-0{|u0}$!vYOyXhQWJXXHd{RQlT*kI;IPR<`Vf49XX@pRgZ9ja2h$IK#oz?;;sHmt?@I~6p^`Yov zcwPtma5^yBKVf#i<57d^}DW{}Sy?13A znS6<4f|>W@1v$}!5Dl*71A76{>bnW}rbINgQYz~l?4H_xv(v*|{mfpKUh~0j zm4?yiP+_cWbjrI~lyFY;k07(k$XP$=ymaYQSo^8h?i*k-%ta!fo{G$?l0XvG_i&%W?PSYWux(ykS_}%|KMp@W z<)&~0#-;knw0<3r3(?4 z*Yk~A<-_*ij5(y=8~wFrlVDn7#5uEM7rMVtLaA5r15}AHk^OrfBAKiM6fgh)-lOCD z&H7^W@_XikL;v2u=;OD87$vSjj6^0~oNGP?#zHsCwg`}XbtGWr6y<`bC6wNJSQZHB z=4Hd`3AY}};pb=k*8^dg-aDA80aWB68r=a=f`9=k_yPFoE)Z%ot#3cMHK z)(#DTfk>>EZ?JNg4@n$~F(@#f`yaGsP_90EIuu$^%q~e%(%D3`sVU<`M%ARjG3-N> z$|{aEN%NnLfUB8Uqmz28)vZg3XRx$Hs)4D4W&4g+a^CV(@-rTY5i^t2oI4>gJ_0q4&m$)+_V~s+!Qg% zQj~vGk}}1yi+vn{+S<7_eanl~?kS5?GRF;$0v+W%3O^NDnqt=#u4-ac%qpmsw9cWQ zvPdmrQ~9MzkLHdoE1GiFJ+7Eg@?nvCA8Vnk!9RKx?7_6bT6!ODX}w|n2*FAC&*ZHZ zkzvJ@<~$qGb41zZoE}l5R)_B#yf)F}hMDdhJ5lk6(eHpi@qYeGyYBvp6q^qL9MHL{CrS=~6qy`BE()|<22ZF%{4Gy3BA zw)~0t;Q}IRBBCPf2_zOc&X?u_L`?9Xeh`D$TESJKY=mkE z_`yj+1g%J&A(ef|yM$y_q@vJyn6u1BVbw!^JZinfn=!lJ+;V=js_ehDCChWin1ykx zuEw@?imS|LA@rwXPp+;sUg^97zBxW@iD=hh*@J?+-d6)tHmgjTDY#>Pr>vAM$0|Zq zl8UOO5lzdS#$2tuD;QV2td;{;ijL5(SzRkWheWRWh2FDEYA3w5-leT(Te+9~wCRbX zyWA@VyVjPKnZ2}oGte_&I&=I|1U2$p1pPi6yp&OK}iH$00JPf z0%G+6FyM~^n)Kn>VXK2ic2Qp;z8T9hq@`s`0F<&VMxu>n>qRs&a7TDg5}j;XgEk?r zA@jm#M$!&Y@gAn$Y(E9RE91q;DU{J`=>^k?ve9gzYla#PdF!%A!@Guf6m`oQm6f0* zg)K>*QeCCci_z-|X5v@I!H*{HmEN$WAs>1b^ZoB@cZ4!0mq}E3MIpZ z6c!<4grR2zoR!8(8Wlq+p_6&W7yR+r(b>^2@jfxfu{6=AQLk~kvA(g(@DPbKiv)_K zjD?LAm?ato8+{w~9)&BFtu-%GBA3q27u>(ydtS$1zh6UMeP~)#6_^^I*D-9mTs6E3 zTNYPNKOU_@t({p)FtB5&hSijqz_lnUk(ZS&qH-3e4b|#dI=XoJc=hw#?m4m-dNYo+ z9eDR9TLDaK{5S_O4#G-;X{yyU$wQ{L1_${LX&zIm{6?1D5|nv6%C$XS$XKow;*n z(UxYN`Fdu4A8hjMW{$3h-dJfep2Y;uf&{9YQ&LusL$z1aHV?J8+dAdZ$lY`?M!2W7 zyu5dHz1-M%tz1nU6ci8wK`A0BN)SNC>uy`Ii*Fhq(iQ^0-Q_J*J54W58$VagZftIZ zw#c~+l+KC)!s7ru_7&}(77DUu$asfDA{CU^=`OHiD*b_>=9SCdK z3Hl*~xQ~U4E3J35m(RDf1R3t|YFYWa1kmNFfD*z6TVHs~w#S#Cwe4}tW}L(0_ipA> zABRQexw{|-`rF|QA3FZo)4v~EpXtJl*W=#U`>=16{rmY{W7wLt^ixRa8^?Dv3SVEj zmdZ()7ju9rMREf+D2d8hLt|}sS2?)i?DRA})6v>hlkH}wr>EoOuq^4-t6}-9+v}w| z?EI=2?N&&BXQLvF#!%!py=HAnA$4>WN;Gw3O@P4eIGFep=lyv%f)*9@Sc6P{3go|T z4+WkU31XHjohehcJK0s!^ZmZQ{D)${JDYjx4~+hivK%w=~%&b8TAF;M2z=)q(3=yLeG2(*J0eI_(4NfT{dzIl1YLgNjOL3s2|i+==U-#6lmGNjjorL zk%2|V#fl6Rdu8Qghd0fR?h^u2%rgZ7 zj5=DoP8Oq}1`RdqnH#5VzFm~rnAiqk3BkvTTEgXGMeG9wAzqmBw zJgy81tn5Pn;jsF^a4>-`igxs&hWZ76i5Ckw2-f`D6TV!zkPlL|T6=ly!bu>&a^Wl) zXt`n`8ECp}0cLTxULhRmS17E^t!dk3?Avt+Swxm#D@$GMZ@IagKST3*q{b}C)KX8+ z$A>R_xCmRN1;*QfJuV^s0JmaAvFLMXJa9$RAc0;k|K~vT7(1dw9(oA!4}Rl{F7I z6YVv3c{PWtPBnXf2~V{~1BvG1B?{X8i41yLMZ_#n{$KZZ=-t8jF6i{hNAbkurZ_coZ z3ELc%166D@o*>ab8c`!uRNA!OOOE=9#U2uTv8IINGi)wSyR9fJ_`l2S9RrEDU-u=l zD{E!RXELNL&^ChjDN~PGjJhvAI91rv9STm&BxYu?U;&WBNEzQqReUtl@bEUp9b1y> zl94HhXsL#h{mP2bWYpwC`@s~@m)!Laqs>G2B4#N!|1yDE}j~>b77}PNzdYxbT zL$j``C>9lenC{YmIdL_kG;>5+yjtLz^;6bxb7J2ZPCYF>_Swnm{W@h zffoE%GIRfdL)ifUb1|dbSuqiK(a&lnmBn1GHcRGj{=$M#yzH0ha`PBuQcz|D2JE{Tx99@?!K>3C( z?COjCP(C3hzhfd77@G-vDAz+7LmA^xJzJ~4qMe|4&C+^Tv|iGC6Q|mQy%c$e8YIvN zcu_1^_f`hSNH9d!icp9mmn0e*^fN0`%c)nPNFkNb)zXYM|6v+Z9b!T+o|u?0Gc!98 zRIrEk@g@~I;%+TE#!=?nuq*haJ;`9|sOUWt#(c)xRt-^kqDWp26?I6lR)ucV>`QH| z0B%{eRW6rnBB_MZKxKq={pa90*hUib5Gn_Gy8|)`t*lg{7gPma{k=yb*TJ5YhS){O zubtoR)>HJ2rN|c}mqL$ez+G=w&A+>*QrudOcs9GM&lg8iZp}(|dJC^C7dQBBpU9F= zWn&gvYm`r8;@OWB;+Qf@nNYU&^A;yWmFKr%1)^u*60yke3C`xdruu=S0Dn zHEWizn&MMs0c;=xKDU6<%uH?D_=wSmDOQa06=>#dHK zruB3@d<+Z>Iqa4^?}sTiIa{{hLgaTjG6CDF71wz)nZGk?3ECp_iTSsI#_6`np zeSFbI79N&)XY%x`TRu;eZ9#nq<8DwD-ax6TOs(Y8%v$+2TcS!T9U^hkk0YL*AkJuG zr$7~j(A-?@IsAJx*DH3NG!8 z(4AC&8}}|-wPQU`nwQbxa5@Gyl-T;Z zdfEPoLM&GiX{bEiGG#nV@o%WF)=c$-^G&B8(xKjl6=cX4UwX?X{ z9onZt#eH+P-izWybK*&Yp>YVSM8l(C8`@f%QO)>_vS)U z>NaUdNR}?W;t`Z&)m&W&&n`T>^*KV4C7KSm8{3__!m6sK?*4y@Wyz8>SS2>|{b)H`!gYk1?#iFvvqUh;x8F-j8o6*bcc4`PaZ(5y~Y+R^4 z4;wh238#OaeJ(6I1v_m_2?{)0KsdFl2-!u$H9H#1NJwTrxq@_k8{5dvA?;it0ys1K|vv>J($ zgxstXc?4laMUTr^nEnEytd24@ntmm{JHa20d+HAy1SIsM?)w+}8_ea1a^nrrdyOdh z@-bfhK(&?9fbTy)AJsrR08>JaUsmDeCN9c>YZOG&l#%0bj@;A2Fdb3~s4G}tOfHt3 zEwYR=-i4sTxDe18Rty{;>#Xw>Z+wm?xu!i#==6YIGDMP&K4lO*;vp*>Uh$0CMg;tB zFvSR-k%Rw(K5W>;c1dD0rZ_PwqBy=cdOyS#92bMsR;(-(2g!?t&g6>{QY*pGvfsU* zm}y1!yyh#dNA%0Z6=4d_w3=rwH;QL2$QnK~Hy3Gx3D7S`{6ybE>jAqK!vI;)Ir4M0Chl$znD&n4H0ILVjmM`m11Lrm5HqAtm$cHac=sF#grkL#qq#5GK(--$SUSm z;ufi_V*lo6^NGWSd}8e0XY2VyXfEUu<6?@okV|aIx?HQdM2Q^Aw z8NwLCBx83sG(Xo*cnsF(+6iO9PDp4~8PS}QIhR!XA7nUsT?d=szp0Vp>kaS{H1r%PO)+z+m z$YdZ|Yb|3Fo{}x;!nht;+5IozH{eJ$fZ&#&_YU3?W|!_p70WAYj*A|#BoX@ zucy%j)&)wSfj;$E1|VWpNYnlg=nloy4F0Q zWzW*TgY+LD?TV&x0kBl0%q)vMxpkX?Xk=k>GLcP1BUufeuSY`uQJi>JM5)I`pi?L` zd_JF_nusZ?+V^I%GKJ#BM#a*jsRKX@f+ihX2rdSrMqC-yOy0pV(1H1I)0ig-brn`K zpN_dk$3P~BRLZVSqN1f|p2cuvG0B-4>Vf7s8IP1s#zG+@COqm4T3V1TqTOCl zsn+cEVW8j`0N9@33k4i^_wKz(pGS-WTpk~VegVvT#*vJBLokOifUUzp-E=u1e_b== z2Q!YaUJ1*SLqiVRg)3LC__z|Kjn$qGW{#dOU=5L$<{ zq+aue^(qKWK1*L-o3lQaM)}Y}rKZAco}R`qOb!Vp{!+vjr%+T=i{hM-B&nU6zUiP2 z)CroQ$z|Z{R%I0s=PeY8;9u<89iBN+fA1G9O`+eXk)J`Xa8FLU;V1TeR#1p1ov?BL zxA?DK_5b8Cyd-ETDiVR8W*p~$g4Y3{nawQ3%w_UeaM3$6V~*#s$N6|w;1c@O`G(DDMO_<2mKjKVn^Ef_Z&wWk!TfY#I+_D@Tf$kTQMT)5!c1W zTC1*Xb^BO0?>%|p!i9I=?%u3hUc7i=f8CO9bLZ7}7vPwf)7x0Z5I?D~gT!Wm#y@AV zw74vw=!uH;C*;q0!u%8Ks9S$x_Bl@|)}Kf|=LzNd6XxeUkywAC{2NdF20rnd0MPLh zW?)NeYwNCd>jE!F>m%3e^g50V>CKCe!^^3 z@;onN3>QxJo;!E0_jJ!IM^7Bv+p@tNR~jzf~L);W8$JD78omzy2uvf zh;LsF-I5lFP^~mI6Us_cp3sJ3%9H&fQoD4?1Sz@cS^7&ze_5pME*Jcav)~h~t4jZ8 znu*;f&!0c}GtS0ApaA=#Tlg*jIsRo4NCE+mKiTMR8`YcBZ?fl?@0 z$0MX}Qoe|4H>4GWK9Qo*Ju6U#P=hp$5Ndjs@<>%81zJFSqmNl>B>Z|&=@cn#DXv?w zN=M-TBBc&NH~gPsd6L{7c~iPjwg#z9q{=X@$5c2TuDTWke2^O+9v=6l1S*xgA!9e$ zY;|>YN8oRW|JYwY%3>XguCA^_T}PD4BlS0mT2hmi+SghtqSd9e@ZJv2>(=S70xbb? zeuIJlcLc}^)MjJ91{e482OnNbZWh<{+k(LSfl_G@D5pgt;~OMdjkhIosf1Yxd-i=s zO`PMzgNjG)v9U!M!zdyi6j=8JN}^xG`g~sWp5FZ6;>89yfvon3z@B{>Wgw9o9wRI3 zL}}|T!uCmJI9S5Wg>svbZANC`R$NieWHREW_Aa^IS#Sxm=)9>43OzLVdXBo5#>PgE z9zA;M;?bi<*e}R*s$>p|dwLdYy#xSF+{nnp$e1fIGch_b<`20h@iH2XOm=1V0p{No zigYr(8n3}DO4}2OB<+lEVk%&#(|B4Uk1J6TR6^X&8Sz6kf1}CQa|)F~&#}XuFYfPr zv15;T!Ym#r)5bRZgbI_Y*nVtPC2bLmN~O_KrbG20$A5UKP)*3E@1vUd`mtM(yT`;& z6Yl=?cg@;Xb>YZ^@%v9a?loN)E$G6P;L^8PJ@!O*!{X~X(|z#3(IZ3;CUs3~dJtW5 z_f#4i)1gY5xQ8v=ohaESa;%QLRVKB1s|d{$Q!(^5yli*=yW zQVhj1_=8^k$7pj*4r61CM5tLbpRRs>C}6>0V}1xsMoN5!JV-uKj4_W+VgrUAuQbRp z)WC?i>$njeKwb>TX*gJou{egnP#XKXNQ`=1(zn=<))6`@O_hY2rD-{#ercK@w7fux z-8>@Fx_kFvC5t8~yAlr0O;1nH1;c>noDiPD(~Oxg+!OweYA67f_28_Y*>uSEG-=TO z%0-k?JBkVAw3a$R@AbNx=1^Sg`3u!r{$e$8P~1O?^sjQQekJ z$lbq>3o7KA!aU6M+@kN%@CeR}9Mdt}N@xO`n+(Tc4!719pHJCYIS&a`0Os9?4q|jX zzZ!0C;vntBF8<#TYbE^v3b?I7vnv8VYWv^xvZUvI0enAdd~a9AO3K7i8FVcI^`&mp4qH7sxm9Up{FUM z;*1{c=k)Y4Pm&AM=x07zO=d9%5A8PNaaIC&xt*T+{0qBg$e9Li)B1`a(qo7K$t{Ww z7gf0*&()S!qS5805FUH`UMuq_%C248(p8@0Sqd^awH9*>C`mYInY zx%X(=J32ZwGq$Qk9^q`xxR>l4CWJRBd9)g@zj5j6)weERzIy56s;W34Xp~BiJAOKE)|Wwd9|xS83+U-w1rFH*3-1V`r$96sp?%Pam&4SwEe(oOe?-@gOftvR&nK) zi55*kC8G=Bg=mUHVKC9?JSIgJGxD;U`i9yvE!SUivJoJ;xswuJ2Vn*&W*}^v6f57L z&N9Mm1@;cI_mJ)4^07$Bi&@@>ckhl)qaE?i2k}a3(Vpni;>Va$G%XSTqx<*oa~!w@ zDwDCR^EpVz@mh(e8P0A&=}s;zC&hdj?mu4)thj9I6yMtAi`N{!@SA_}7k}|9mo9zq zhxq%KUps?WcLTohy7l)ZoV*hmZG)i^>PTB~YVLyE+{W_@j%9k>zB1amikO z>eQ*O27P84`%qqPm4~M8{_p?&zyHq=zu8ID3C6&Sx{?lDRe!)>vTM);%J;aBq9!JnBWCZ&Q`2%D_QLxGszN(P0SX9kkZ0 z?zec+|H8>QSjS>OeCABpA5Eo#&>sHT2|xh` z*W}i)_6-taWO6=?5wU9#c~}Nah38$$;uojZ^xXMv{f5Y8=-z_swT8Xnlgmi3RL0^A-b84 z+>9)-gKf|;EHL>WGrisLUFy}->lE}76os1g|dZn!BMBH6^A`UV;Q(0+{6&-|c&q^JHLn5D% zsijy#?Zyc$ zU!%pI1)+^dOLQDXSnV?<3+Lj5RX)p(BRhetK_(X+UKypfh$m_WQ&|}W3$(>tMlCLi z+0{969GFUiTyCdk1|4+A!3K;N9t6-liU-^vMhp$%C7jdcXebz1Jxg=rOP%xTB|J=9 zQr905Cv){cP?gPbD(z|xQ8Z0VHj8IzTQpqOg(fe|RhC9W9L$mUyh}=6IYP^%X$7G& zX=>iE<~l-Wq^WYlb`ykJ)@ZR`KDpojvPlvXH{K9|Une5_)_Oz;BIjmt`8g0pLxU`0tLSg|$(UtwwL zCFq79NO&+L$9e?*V1sN(6pnA;bD?jzfj8iX-5XfN)bniS5|QQU4K!U84sEc5BG4t3 z`JNPoK;GoKRr*HS6#P$-UO@V{OQ{b&5$RQ=|F)FghJPv2-$gq3l)i=ZZKQ3S0x#NZ zmMskrDfrBi=Mi2{FjL`+rv6`N{{h%mk?oJ;bGy1^NtR_x?k#TV)r61)0tqY-Ah48O z>Qc7w-tu~XzETXk|JQqO-}cHbKiI+smR^>GkhsN8;@)l9mMrVaRxkh0NOCuMW$Y_m z&D^PX%9(RM=Zsn{aY;fgad?LTfdtZEMwYdyNN6!^uC1+=1lDC>nYl5r>8Q#wVI@)4 z3o`tltEv+vovpkUZd+YVO{KliXfzp&S|g_7(rwtQRyfFB zSynMD$5Ux=NH$A|ETk=Ya3qyV5rL#+O`e#JB$A8>&BSaA?xXzwGC~UDs0b8TP<&5- z>hS_`fI^Q3=qk;o(u|8`(f|YW_|j%bu`FqCPmf!prsxVmU{HLuMN`xuR_)wbw7*5g zimXOSsI42VQG5zY13mKWM)WX%!W2L3@hPi{WtvckDtO8wcAj&gc-p19I35zfo1&_4 z`}ezxFl|{XvI=HnQ$V9mQRJ|6=#WIJ5DNmV{5-wjg7Jbp1=}F1<#z6zdt-^N(h}96 zL~G|po})G5!fkx41%rTVK0S7G3)D?Et*)`G#?#Hq{lY*PTtq~RP$vww@q?BTng-KM zgcnbby_o(s5<*F`&+7?;YxVglK5!wm$W1yBLns-e`Eu0*%QyZ}9v@cMIcJTzOxH^LT##=ZVMj>`O0w`z7*a znFpNqUbG4{f5lTU;BoTgsg0E37;T+Ww9bFc9>xtUZImLk7NM$Jf^Tubci#=Z3v4C# zS~&a~zQuRBw}Q7|jQ$nhcJjB_%46hD$)7TnFCHV)KusEy9|Up3@u)6uXWgvIsi*Lp|sJrCZJ zBDa)))3G>)PJZ2=Wb#VO%4TQh!VJj=Y`IjY)(EXCE|TO#E=|%e?=dma==0AVDUqfi z8SzNA!a|#B7Dj%e1v~D2U}knv>ufj-!OQUzx1G2R?r?*X97Yx@M}0jtN^_*%sab^a z4uioUE(~6xs(rl!Gf|fg<6cmyBhdu4Wz$O5>rEFFys1`Sxzac~N=G5N%}p-6to`uA zrfEo`#&_%h&E5i?X*YDIUnVPD>3xV%>9Gh zhFSBE2(~l-pY+fYB{0Gd;hsHB9)b6UaTLI_bj_fe^c!tMOa~c`9~`t;Ixl_R(a)37 zOdlVLxVioNN#fOn^&Yf#0e0k$|pQJtdhVmBgV^jWbyd%<413SdM^2SnQ`b}-mt>4NGyk<`|k1^I98U${pVW=!>}v=EX&h> z&N?4qn8>^j<^{%mQL`C}n5ypn7A~3KIa$N;i6pt`&)c8pcU7w*8C}?d>V1Gb?yD{! zLv%5O%4|kceS5*w$&*uPi55PUBpmBP;v|`ZHu6DeBVWKkxd7S8!BeMRS#2pX(^5-l zsiWkt<+Ceu;|}=SV++0+&n$(jV$vU(oeu%@{K+RVazSRD>9m`HN{Qs_$2R4vFZPPP z6Ply5b4yVS?&qIB*<_ssC-RnCI!U?AX&px1#f0W$Y1?j$=tGUQudJnI)mUqDPSsX0 z%D=a`Kt3WDUF=1W398fQ_m4fLP<7o?F7^~TC9hi_sEv{=Zh?cXh(TW0V;LNkNybpb zFN_7B;(r0Cqh)&x1&C9K!KK3sSdPWAy7xlMG2hGNOD>*8#?T4VHY_L7)bLx#o}4;M z^CvVd8{TSu*%}R(YkFGtN!Cv;x+Rg8iu!gRr{za~-lPNG*0!Pq&hz+@U9GW-wn$iw zru?B;+O5J0on5Nk1z4h&mB6X49-mbMCslYJntF{D&U}?yHH!he*U7GEBke_Q)XJ%2 z{CnRU|AHJ}lh1CMBdI$EJ+r^G*L^|GzlL~Uobv&~;6l#)M<0Rx6jFScvwccPrNR$2 zRL<2QDi70O?%67H$5=EvcE=qWYc+(e)mBY!?;Ur<`yfT>ixUT;ojXUi&U>T96MvS% z)-R97n+b!9kWxCkwoOg7jgAUT0zEsyK&KKv?ATY^1yI*+9VH63EL|y`hKpW(wP^qT zC}#zIWaXk%Z*umt*Is)Kn&uir-n(~p_6B9#Fn{e?o~KR{1{WcfIja`_si9$eLE1l& zF=jF0PuuK6gOmP`J{lS#BanzuvkGoA01YM7Dnrif+sNEpROTF$lMZ*KHXaNHY;8uR&~%jcU9*5vcl5>(?#Isg}=`TJ4e8jVJjxk;yU(!HT{agM!k zaWs(7gTB=#0;8W@VAxn-7UcTyI3z%;B zE-KGHvA=-H0En4_{ZBlr1jT~#j46)tf?eCT?II0G2ONtUlxKf_)@a1_rKQ+%Iw%}U zw-q05_hvqvF1w$8m+q&xT(?%@?8{NqPOiV7d-wdsw)V^Kz542_=ndB{fA-0=6lBF815^G@t2V9{?dl6O-E*mZ_f%d&9p z+|pzq;bJuTvUI)eop;_j-`)EP$>@}0UU{&L6xuWMT1Ilo<=_DH13q@X?O)qI`Mmv; zbKigc+-H5TUGUzI{^hU!>R*2Js!YjU#%*8->~zouuc1adNKqluT80(iq7L_P9GgFO z8meVAHQVnz^X!W+K6~cQJ*HG@&r`?9Uy#3G?tDTPs{0uxod!oWjmB1=IzZ;motv|r zA{+J{3^Uk%`Q4Zh1p{$%@bk~{`@-w5zkXqmw4-xjt5GELCaqe-xmDv(Su9b7sn+87 z_?~?Sp7iz2BoYZ-8CVzNJMR7Z*S~)64!R@Gsw?uoV8kDFtBUd3yJp!Ht;ORx+;m0o zUA&#k7eD^sCm4Hg{_OJQUQBUUKK}Rv`i|(!!vrU@ct>ZsR5Xr_8wPQdQl@nl(M@+h z6;o&Mst)hpw{I8TRb5qC+0sWJeKZgkW#9cfui99RA3PuGP#%ufJ za=UwVFLZEa&ZBe7*0b%1tQ#7#TEAe@GZ@Bp>`)SVuy*wc<--qm>=^&(-~R32J{l*S z%&66_EhpSe-uL9Ja8&Em`YTtjbPW_5q{XS|TyNK>oI%^&t>r%akSiG&DB%VMsD7Im z^1+4DvLxkK!sSacn;svhMpBxZ=#|+Sa@UsZPaP+2@-O6nmHbM~HR`i%qgk4{xf#S78yOz*gz7E% zwnB%qw5+1C%Ij|a&#e7ycNRG+7)Hy6d{gt$g5p@Ay?W=N=9~9#HUqS6qY)du-Qg_S z)`S&n_pVvb-1OA7tDv0P+8w$6QI^wCH$j_yN1dJv27Qa6G_=}7=%F9&FL&`68pj`P zHHkleI3+Ya@Wd0(eC5kuLEAoy@Zah4yLjaF&iOSGpWR4J*Y?+c-FAb$;NQuAN4|E9 zbdfIMYyX8kA@I7}w*5_R_msmvT=>&Jy|8Xa@)z=-k!>0BfZ4WjXTqE&l$b;+f3kua zr;@3BTE0yd>OPcP*IKB{4?OWiV3U=)V>C7QT0?ak=I(wvcYkYn?kcJcAXU^DHb>Uw`^S=4!vO4_gzNwMcU5%*gH1e;??zJlU zKcHnlyGA>IPi~fQcKq$%c6hGog2RE;$nk=7DPx7#yl8kJlEQ9GOurXV&UN*lUV?H#4!A{4z4kMio z^x>_SF2H%dVBso&d0q@;jN_GIoNjvRDO-b3HE^R9Yjv*{%kI^h>Anu7--=&za=FIO zS;Kg}HhE5-+Qb_WXkB&#(0iDXnNB+1S>P*{d34XEkQ8eh75-XndY|OjAosiqGR| zYN{z~s6TYLx}>nEr12I^`^R>a>3zs;PF+N|eovp?T}o~Oi$quGFp2`u`PMvxA*J{i zXO~1tQmNroJj=+&n;I>AXaMCJ4D*&o2z;`&yCt_nwORVhg;&~@aY%MFX_rn5rkO9HDQs-?`ADV5wD-h`6AwTA^rQINljl(eFjSdG9$~_` z32PsDM2p=i)g&}YT7!yBFkHfwcd({V1Ct>K51P{pV~|su&1-le<}yN50&>qGXW7Qa zl2(Dw^a8%Z@{q?0e28kJbXO#!S^1H5mA}1_pXg~9JY};jSlXGLL^uM}d*@*RSQFjA z78VR}i2-3e)UBD~7t2Uvi7amSlo;=yF!ADfT7YbvLx^)YYr$YDC98USjmD18FMZxm zxrnj~EoAEJHIhD=!&q0&su~+f5#!QnIYf963U-jWeR3_TM`;a9i+0yCS8rWkeRtCOM9E<%#p_ zo+!=joK$tAKV`?h|NXI7kEWmJ{;<3I5AiL&%Kmh;j{GtBj-z+|YWlzl@_+Gn02uce z8DyS$<~SL|-5>GkU%hJ-0}fRd1d7DSd;_yA2=sEVS`>Sjzy;)O7cTY;dBJp_>xG-c zjc>H){Lct8KY9g5<}Q5t>1X)r8UjDOrI2Td2RN(ggub+-*yo)KaRnGv1tf)eluKhe z=3Z%lCGVS>?Ws}F*qHtxHb0p8VYJnJvQ4Dt@ zg>0khSR`o!98G__b%R~2@vQv2W(!*Z*)VZ6EHAf4>pTD8Q@wEcvY3^Z~6UKuJjCg z1@c~&e>m;t8XM#M%XuDj_0P{&RQ%{i^}BY}R(Oa;7NMJV;2_QJ^Upc{WwPE*kMNT~ zBWZ|wL)P|j8FR$4 z>8vx84|xu=8VJTVrZYj)xn=XpIY<5PhyRwAxCXkl!)zlm;FX*18EIla*KAJtI!)os z=Czm2$_Gmkw#;eF*&{1g5>%5>S;*)ijQbW?I#nzTQk!`Tnw}m_#sqXSNzLW)97liz z&|aJ-g`hqQ$@ImGuc#^+EI&-;@uzMhXUU&s{?3}8I(`$z$4$513FWLiZ?%8(n|6%k zR@o7YCIx+-$z+0%C>f2#b{7f(n1Blig}ZmlOftD?civ8G^x|@jw&&4kziFbTor3#D4^Up`fy|UF*W>IC- z&^4Ov`@pchX?K%GvqpYyS;upv-A4F0Dw7MO+r@T+02UsaJmdKlNhXhr`$&i!Ngk02 z;-a@$~)u@+;T4qvU_Hd)Fq<+MAk=lHb!DNoF&_r@SH) zGm>>YN?O-(HblDJ7#Osghj}K6O6JPdn3Id;qfA3tCxj@@Xb8XQ0!(qC(L~av>X}RE zD=I1=y3EH5sMw2jX>Wzc4{Wht_s~P&bJAHIvJEYla;bLOxp{2n0Tf!{f!;)AE8}3O zY?%{e%vs=MS0Z^JfH?iqorurt#VyAV#%zW z5vX61Nn&}#9xBVOspdSwavRE&C$x7PtV2FHp}Jb|4fz&iW2j<%v5L_Y9traC4$uY8 znwlD?rsLY1Z@zhL@yL-yVwV}MR@QDa1x8^`4=9hY}4kITblS-k;^ndestc>0OS z*38Wg+w%idg(Z--+J|SogJZHu(iKxx7K$WaiV;l1<;%($2k$#GF{8_AWoTz6&YV5~ zrbA&NMT*#$6*S1=;>3zchia=;C3A}1uH?#j^GbQhN=Y*15(She!d+||4=@DD1_c;=aBPHe-rRZJ&i zyoS<(^YgMgRt8zHC#EkebCVU$)_usU7F*Wx=6w$iWx%=qO8Uqxo4V~Ok~NGHO5~{)oo8fWhJX_D-`ad>b4;;j_?b9`?Mjd zl#Ak-_4;Ic5akoZ6DNkjS^W6Qu&h3M^ytk8_s-4jwYWIFK9O)|Y2@4tL*X2fkj1vE zAzjKJY#VGBMqGS;V^7aTxv>4n5w#7Y)uwL02A z`q^lVIyj`Z5MOm{kKE_Ngh4*XLJ)q43Fr7*jd?V(`ebSXUNCfO6`p`$L@OQ@#nsLL+!9TQ**YuHac`y4>*kI`N53)dB-j;gkIt>NfVT&V7oKm5Z_Zn(?( zyIYBiEa1=eU)pZX%K`&JY|Aaz%Fcz-V0n>`K8mc{NqhoMU(qr09r7KfXycB8d4PcY zSV?6{gNpD(l3cw-GHyq8Xi2@y6z3B{r&y^^(kbgf#qaO5)SNI zpOmV!baZqzxmB)UJ#DACH{O_Ahu1$RyVnBtiS-z95trV&4!BQA6b)@HvI^f{;R!ZV zp5W;BzBl?sbnxr4dkaF?srj{E(|i#z{G`k<%oh>FTgf4J-qF) zbwq!-wT$GMn2jr0i*am&R_yv^40!0R7BOp8)fURJ)~#2qjk^CUdna1H^|of|scz$+ za`Z$u($K0BpMIL`eL*BI$ZjyzTi4q>XLi?{(Zq@1{LC;=@}K?S-~0OJ=OfgHKCI$T zbyF$E`20MBDM7k;@%?s%8b*>BhA8dtqaT_scTY!&AtSmlkmz*x<<`1@h91~Og+Qe{ zsEnef;-;Has^}mH&Vi(D=jkV&c;enY)ztwAB&1U(ns+qqEaY91P`I;cNArnOvgy>_ z%{DUiDLuz)irAX(UPeFMl(RosvXImpVXRjbTj03R{74@-iGu_E0|N_O|L0sru9AkN zD^ZBK%Y|l^`S>hWS{Hh?c28q$iV< zU*%EqH|#Hq=;&@)ljhXggyDzpK$_;#LBsIw+mC`~C+P{cb%W;EQr4_-H}u2$rOr-C z=;#p06=4;wB}tNr#tuz=-ro|pg8(YZqyzVJ#Yu}A0 zzMDC@L0^r2R;|ySd!dd}Ntnh~z7t%UUFBe*BMOy-We@^Qu&KXniL90K(~YP0T8Q^^ zbgR$3#Ikq!1S>mXa1o-zCMZSH>2yzz7MY4QH6ggzD>^ZeNJ&K)=-NW zw3Q~EW;w#C*eRei%advUKwl4DhLV5a$>$=AoTZ%Z5pO>6rLX?RZyY(2B!^^UK~t^M zVP+IcbhSYX)1^s+wa%-N(rQy_KnrFdlVcFKEJPLt4 zUZ=v)^XbYgmNEvw38tj^!7uyf)g{fa#rLKA?>_^>11ApDk>f}@ufF~!D)6S z_l8I4Nqy)0hx{&0d@&k|gp?G9MXnB3!r;oRy-ZdHqjG4#iCz(?r4=7+b*GI&*_Jh(Eaz{dFK9y z?mP44haPy~fjjqCk-LzNlwYtNwXQSJ!xDQZCuQBab7qr71xFeKpWb*Dh?d&A;KP2; zY-O1kp6%?o-s@Rf3I+m!P+G{x(SLdIz#!Fq3vwg|L_s)}NW09Opr(hO@mH_T#^4eu zhLQD`rc!2bw<_|)&;UIPM1>Kobvl~vxNTuUEW){?XU^Pm_~>mAY#iB9!QySD3hGWi z_Sj=z+F49)M$)=`v({w}j19Fx&3(>l<)9e65KhDrvi^u8HU#9-Wo&91j~sDtI9;fy z5}KmZ)6t2EA`*}}!-4(#Wp?**38xEP{z)|IaNI;CpjMfSUp{wEX5SuPo&z95$AuTR zUqmz5%gU_y;?t=lMG1Na2Pg3rN~EmlzWS6Ot>8%+aG#f&!~J}U_E;^5Zz3>~1SK!t zrRCLt$xDntK$Xh{mpm~wkiY7f2VFX?D@KzQ>(YL|`#>>|#*r)*6Iyzs*5eNIg5#ry7l?z!jg*+;&C3{#0DsO(gPAw28S zvOHm8sWitVVV=I=&I1k(ATiEy;LbY>l9L@^V{}X=3kq^A_Eo~*!nia$9HUcl(cail zS(%r$4Jf8!0l28BDa9O8BECcYZIZA zwkmsI=F<4JYwjkSlz#N#V~rN?oM$=`3rA4Xl(uje)T?(kT7r1*3&x6l)b{872WrV} zNL*c0w;#Pi+uP-VmOY<{#F2Pxd`dR%sxhP%y0Q9QnNMh|cI|Snw~9+7YD}CkXUPQE z$D4WmyAcX%BeYc*n+@}96~<@7rnd^yWy9vT3e#u9rnU;>ZjhfU8>ZYK-o$@5O(`3e zB>9`eoY}C*`Y>TNP1lV>Hp#HF>G25rqBcq2IK?k$5$#rC+=iOnD8<`y`@w2mU!U&3 zu+rlk)ba5zSnjJsjsuqe!jiA1Vsmn%Wk1WAD$DZ1HR_Cfl%b#Mx4F=)cW&;(@O$D# zLf8M8i-t4Va1MJ#i5D}}z%KzGEgm2lTELa5E1yFrkUaNUHg8q(zT#gD|La@$Yv6C% z!e0x2?H2y|@Q-fcPxBSG@YloNu!X<*3(Bd3e|YP3Xn8hr3AwVskly_YH^P*r+&QX9 zmD^+S|G@xvCBMw46gw%EU)~TJV#dh?Lh}?0DcTs?!p$?pk5Ii)A+}9%eT5yftxMUtWj@Dq)H{<*yPWA{A|AzdJsM9)V9=??<`TL@0A_?1Y$QU(?=nfBC21Kq z#<4}>Xi&z+V4XrsCa>t-j81SB3Oa+S00&kTm<-f3Detr!I72>|qIMJ@2kkwZMavq& z)%ALeHXCTSC1SA$+-vB?GD2L!QY0Mi@24#wlvhZS#J(a5Bx8U`5J?(`QLxhZz5cQ`?)CW=W5fvjqu~`vFz1vU=o3!b{Bqc4ktk8 zsr=#5ATfeW)e}J=2HfaqVcaC`Vk6<0i(y#23fK>}D70-898_;G8KyL5luOqtqzNde zq>ODvE2HM*Z4QT7%TfA9ElFw)xRch6QgF zR6r`Wh(a#_rR-8M1SBxeLG$U0D06mpab$Lc{kUIc36ez%IkiYsgR_0nKy)xYrV8g1 zeVB~s$;yr?Yt1RikddL8C<8qxF1j!>oJ@v7BiFCY!1gvs&-p+Ios}9v)C5uAC1OB- z(6~7;wdPzr!xHR5h)OPX*o|rq=vz*0$SX*Z(o%b|-EK8o(G&C3YEl52oR=gcDrXSW z)S68^E^B9J%{qxXQOF@5?$2?h89{KFRT{#QbV;Fx#C&5D6CvztU3!M-=sV#%yHmw-E9OEo4l^K)ut6lz-l5WN7!Qh|>7B_f$nbCX1t zmfS>gv4T$Jsud0S7~NKr4WG2q45KnwQRjSv3ipyBANN)R9qKA-N1voQj&-S6jt+UA zQt~#7LBxO*4H!A;h~h(2_>@RGy=vq8bOw*Xuw&CH!CdMn(g+~W5kC=kVQdRp`Z`jJ zsK+7%9crGW7SXBrQmYH|0!g_r{LgAf7YTh%lX-0hKFO6jEP8fPSxk!@<0_C0dJ`Qp zTD3q&z1B)gof$uB6*O`&9GRt9E1Hx?k}QjthLl!b+R7~20zBO+=fP42AJw*PC&&(7QkPM{3E$~@Jy@Fo1kwAn6QS9iLkiqzp`HqfQX{lS#D9VWw z`($zeUbo)LClVXbT6Avj!Z5eGxrGHfTEWj=e>MjvG2nF)>)GrB`{ni4GGi2S3h%?vuAJ zqPPl5%avC<9J1sntSGOpzV+7D4fdmZI@^&ZMSjOZ_@=40a0#{uyIgA_n*bzl=h?hl zPu`70k@T#85vkH-`TpUdX=>1NvVXXry!&phE_dYS#7Z`aeZMG*ixbz*f5tK4*@@As z*!XpHTx`2^iDhwtyg)w-vD!RaC8*;9E{(CGWC%x1w}Unj*uRqC}!dGaNBNaFiG9y=KV^tE<%EJj=D-;OO~L_d1Ph zqE5Wq&0YJO*M`X7%fF{y$TKR=BR7?Re*C@cb0s<1lEDHq6$!!OdS4)nO@00(-+LR|?h={R6_VlmhpE4)lyd}F~(dNPhH@AED$cTI6 z88jX3v@Kr|7N7eXHBs@(`f$Nw9vdTL2%npI?5pJDa(F)4x&+}^$`}qUDsbFT`(PJ0 zHE=l~>m`r~Qb7%D9o7_p*3~9VWji20*U0pg75Gb7P}k$83ENMxg=O(q76 zL=Q0nK%VOfs%5DJCGxuH0Nni?!Ejura1Z2ULk>`gxxv`c)e~CeIBs!fh@QkTgJ}HB zymu06>%NJ}$q|<-Fhya${ZoNfM>M2>s{)&R_uYNhsh9;blLgYylaPf1XTWQ&j!woz7w_V|C_R>GGWLg zw0-LNlqB#x7nr_s;d6{`uXn5)qx(Wv_m#FbqM#Vcbf(tRbd;;pF;38FoK)?MO$)rs z3M=7SV{xI?Xt9vh_GuUypPL@MdbKC+IQaOJN-(Z3*>(V<{lwk(!3^Js7NmjJQ4f!L zddRwQ-_H69D;FL@At%xdCJ$RG8VDE|ySJVLAU3qSW%Mx8yC$A$ zdDR%<#@RswVI?KX!id2aJTZhP@)VA(?*AV@(ZcM^Jki3uNmhH`;f%IIM_VW45?#Zy z+zi?~>n^o*{P<^W5PrHqgS$+|(#3&`EAF#TeXUNc9|DmyMw>%fVm0QXa-9YoxNx|_ zt|3;rXsGXc@8A&JSW#(JRaIGGStY(oOQwg0+-q^z1f-7VC!;^{U>0Chk?*J!#e4UY zcY6W%W5n2ZvSl@`oECYV>wNRgPC8>S5!G20>t~<&>Q|q^!)_)f=34*09L-uAV^we> zMldJRJ2n=%etq;h+|b0t5WeV-2zEp!mZVv=$yVf;_IQ;j)v;!GHtA$tGR`m*?y=O} z#j@^Nm3I(sdJ&R^X?o{X6*(LSZim}dQL&4DA8b)5A)ziE{%>kovHv>GZLuz zx88jFLO2{_W2`9czvajga9r1y7lK?4E*Yi=R%CvRkM>@H>$%?7cfE(+^^T6Cyjr%a zdx>QQkc{!9%<7tUy7E|#M5*mhN0H5>X48b0mu07}!Fl6xFa4eZ*_6NQDBS+KhK9QR z^ln!^mnrX&Be(3AL>8qBhcCSS=36MQ1ZibJ<#djXE}<@b80Fmx>&m~{{p#y2%yvvw zV|Rb)?t5F9*H6pqsF~#_2e|KZuQOfSflXy!Wbb88zwRPyQzQ~c5%e7NH@+(=gZF&x zoJzlg zEA~z1uW*4Dc4sr;VtI{34X<3Ij~_sE~fL@P5Ei_B_332GIk zq9SO7(AEU|vI`bxq&L=B_j_HhcL0iE>BpR{f#juqV{m3cw{`4HY}>YHV%xTDCllM|#CGz; zwr$(CZ{B*p@5lXp`*d}k({<3hx_Y1L-M!YL%(Vv@Z?Qk8e~3bOdUkV_m9;CtCPXCT zSn}A~1YGLeXo|=~JZ}|%X%jnV`P~QwZh?#JcYk|5GpoU15Uslh3!+hoLO_V!R#Ebr zINvM~CbBXTR^^;?6AN+E*3}_y%<^0Z+vw5bUF3CF*UShQbHOIb_y0V1rg z+3{+2l|FoaCxfkIS-9TRsu@Pmc|Dy!JRnR+gsND&3D*x0)+yg_V#mih-5=hh)^d!Y z?x>6+)3TMLaR~DI&VEKKQpujM&V@BKJxNKChwnnadRl)z1T=o%tJD0DGQYWKj0`zf zSVUQC4~+kg%oFb2@O{tt^n@SX84=$K-=`vX;YEpW_dFO;=^LSgz-E(BZQcb+c92fV zQRtlP@Oi&9t_)EqDi!)u|6XxC8|&K{m6VEfShqs8p!H!_do3&M7A z2yD02R=ubKha0P0gtOQvS*5W4DlF~O?}<$mm0}Gc(V;-s@cH706!Kw5O_d2Zs04S1 zn8pfV*R&GR5t7jnDauwU^T5BekyX;xSSPeAVCcwqeXrJO&%(UX-C-O$4#X!PQvdCH zbWh3+Ol?Ud<6IAhuj}Fx&VET91&+Rl%~&2`<+>UNWU!))ZQIc~tWr>w$RGr!-L)2 z%XYOgt8CXyVA)mH>Tx|~BRc{5YQht<1zBKZcE!8o{8Ct^8{5Hl=ymrmuFT7`U+M|eDUNq|JpH>sUXVb1aXciU0K+e@BrM$Cz4m#fu2G&|LH3qUkx#+U(>4@j@3rbZ!(E2ny2fDlV@{$EA<~BZ`k2&}lQQV)<>6~70 zrOn%kKdZ<%b=TfV8-|OBe92-a{bw zuu7jk5H_4Ar@j2AXAiuU!V}YOzBAEse)_tM)6|$Vp zOAwbQF!fS0Rp$$5*{k;0meX09&JsY8aq=a~4yH$GE=y}K^t^>|GYhcqcMW0&zkb!= zmMa@^o#3Sf7WNRNwebh&0ozR8LK1ko^Xpr#_#OAh^12?0>s(F(9r4~RitXU@D=_#Y z{U8YOyna|Kf%gXD&mj{mbQ^)0m7<&|`XU&9D^msIo3x>V&IzDDc#1IwRmXaKAgQx9 z{?P|wuj$P{HnFk5KORo8RPcF*!v+)c3`Hk-WP^x;d2@6iRONdXzME zBM{sI=}2LC7yyp1X2!6oCxl^iszYyF(~*kC1S=fLvBaZxbrCv7XV#2C1gc~T(n;Xz z+5ICws2KxrpPE8ayVEg*?&!+Yd>; z%7(UQE}{YHn(}9RKwj9GI2=*m3VLa|yA+&Qb3fM^Lp_>FZvr!*2(8pmpPiKLm$g|fElhq+JDd)@N3zpl0(Gnk1o zca7tey(WnlX&lY7bF#fJzDw#Vx6{{|HTy{qCX^w% z_c7csci8eV4iO)d;G0h{<#EV0#bjYfJqFzh>#uc`L)~9MF8l-pNQ2OFHM|bvl}m)g ztVhGBuCCf~V`kXw@0F$)7Jp7vv|d0-$}D;khVlt_2{D9_ae3m4nCQoyYKDkM#Ya9a z1(Qqmhd^tx3|~0c)iX!V5Zw(QAMa_=QrL7B7Rmde8vBivh5HlMjnyej>#?t0q6vQo zkgfphGS&fhTY`2E%|9oj#6IeEQb(mhXNv$JSS+8#xFO zed`W+v%+a$<>krcWhhg2*Vb0dFE=3%V8#aULpJ#Lo`%h3c^1HDw%ge`1yCN%Mng$0 zrr~5l#-&%;D2X*f^k9(**%UHu#6ttB>ZgACEIe#9vyvjQl~uW91Y%xoVR`XTXW#gc z$YRcnz^VL{Z&RrdCj{xi;%{4u#3FRV`1F=PLl`(5h%%%$jD_`d*JF(J`KOX)F8M^zt$pw5!TXe_&Dx zsL^d2-o%86aSlz@4FF}Tr{~D;Q>SuK|jx_`&FFWdue87v#7C>u~L@` zUT)e`?YiE&U|^$oB%rb@AfAsebuN}McBkDac z=*%xM5u+5SX-b<_Z>YQTn>o1`eqCF#Od90`ym#c;I6dp@hH8U8pOhD`o!^ zeWrKQ!@HO6ot#jzfv1romiiN6okbRabli~v7YEf|8J;9*l}8OOtHOPf`TQyr?_Tec zTU0neOb?zkjNe)?h5n-lG^KVxhK`QD=YiI4*SQ}PA1)#^C=<*7cJdh-ah4H_$K%>E zCCWvr3Sqi0h49yERUhpGR7Z!eU`v0)BshG(tV_=CZ9Z2wGd4UWA;K|qvgi0HpC{Gj zDJ?6K26o+YQkoK!6PD@qas3GNMm9f#DhDLF%g9to8VP1opKJ?%!Gd|R*d+YUr~b{e zO93c%_y|J<{K<_U`w14cNrUVqbc@G~i7`@g3JI9fUpT-LkeU2-j@rDGhuBZAU*eX8 zR$(H6nnyx8V5k9ey=v0loHjmtQ!K3ivUjY>Cov%>E8TN|&&rWN{DkBR(H8zm==<(t zAZ4>SaAJsQvLq+>4>6Lu`cA*RE`#n;S66P|JMx@GErtM}_%PK?hrkv2KZP>|kYN zMOfa-uH$&OsB~)89oIXEC3efNJ3qGIq9MZZ`xAlh^=04fnp!0mVcY3hmx7#&58KYS zoMV1QlJ=519MbgDAw)xyxMK_AU$knbY=7mWOk9OE3wGfWnigpblta)|HY^nh=<+`m z4;%f1Y_}xB1=zqAEFv2XGRo9}u#663X^MJF?rJKCZr~CLo<38jmcUu=KT+IGaI|X9 z`Aj^?Bx0zB#Ymx{I>=DxdA3lB#>sSS4$!;qN;J$G+Cj=U9}m{Zi9U{|*v*|fJI&6I zvfuANj$dSa9@dBj)Wiq zVa})!t^B3rsxrja7dD%DN>N>ryjv{w_RLU0K>@fwiH9;l2%JPF(P;58rjVHrn1hXZ zn2{u>HQp*rIy4BtBKgqxo(Lw<9tp-ji7sDS9}dJ-lxO#Y5%vA@PSAGcp!RR4gyG*M z#ui)L+Hcmw*@d;V3*=uRk>h=ocDgTk-hMuiQjUpXs;c;jSIi+h8k~qziBD;_I_6yY zkoQZ{N}C@eTgCKEaacIkWCf@S75U$DH7}K;tM9wM2gAlgu~nH=^ShL1=vEvxb&*vV z>hH~3Wk=I}Ftw;sMiVm(hkH|kQK4 zCX+g zHIt17W+01jqIK}_8ro@oAVIQ;)8(-s)|TJr?dAzN+EnP%5gCyaO~ClyBTnFZ+BScg zXKtmVgA`OR?6bSI_7swWtCWxs1Zd~Ro16_mPK~?`Ivtpc$Yz@#y6yS%d2>9AOFO6( z>o;e*eHsyx2DZ^_dGM?yPRr{Ib3S=zxLS&>CH9%~QtaENv5)jG{pPMN^CVK^GEe8c z2(w{xX<=9hBPML8#;sMZ1!ok)YJu)BEAyQj{8Xvxt|9yA(|Bs&IGE1*p}dnbGXm!` zd~elj?b$Y}sa5OwdtOM>Gs#aj6_QiYm{#(*n3x8f#MzTvANgbN8x0CBm$M7*_MUOq zOwRZ~n!AXs;j6lK;gUV&woLder$%pT3Y9msz8&HNd1~ZH+P9B+wRSEl7`~lTjqLyd z(z5qz**6JVv^xgKNq43h^Z*)zz`MTz-bOiCA>Goo_Ar^Ux@iu5Nf0XMoKPd)ome9! zycH?|aJWy}!)CwtsqgQhN05He(NapL4eI{G1!QadV-SK({KU)k&ZoRb`P(yRDNmdp z6P%RHsQm4Zcsm&lQo1KoLWL^3keMa#S!XDN2F7%OH%xpjRic5LFnNb91>GoMo<@1J zwXtimYRif#kA9R=!NJYUeyOL_N-XB!kO!YU-moexPp}p2(GtA6%1PV8eca*HyC_Ic zNB_2rUMC(EY9?0qG?9l(nLnltLRRilBwxit<-hM5Zd?)xifR&|!8k%w&#c|(=KG}K z?0NwMIe^F~Uaj&&sKg{KQ6?z48!ub)=j0Q&sH!E)s5IK4ZwK@h@q$I8uk4a7*wPlA zW`OqC+Sb;U*iWY?_-gMfyyXMb;% zqft0L9jNlfdUUge}RIgR4JD0wg^N@h(qC!?mxkV`nC3cQcp+i!n88O6qL zCut3MU3Wg`cqM_SLNP%cU=}aAaQk3SvDeo2B#YF<5e_cxI*GecCQ)4KG#MBQegd_P^D&tA0<6fbpSxb2z2j$?+3 zxl7`e0^lB*lQ?X)*Ufj)A=l~k&R`w6{;>;j*`EG>9^MaWyClVzX^qz511*TKIj-JR zZz9=0VR2aldy`I5b11{)!(~d5gwPJHsf%*yFc1z1kE zN^;8RdKb2fRW%$OmvK58w-fEPI_`c46C4j)-+pxv zf2k5|c{9Bjtg;@P#d}IwQ$EO8QAO>>DQ;fgeJ>Bs;mx*ZY+~0u|GDSX1y}DE-kka8?gO70L$=s<#5OR$?|z6#lQ<+pd#0O zmo(4$(V1+>O9$w(guern8|41!Ml%L&~9hV_5ChmxjIwW{W;$KG2ZRNgZxGRit-j}=O+3D zU#;gUV+8o(SnJfcX}1C+7je18RIgGW{O$u0=v9JaJR5X!8Wbjz(r~WsouP)2HkHVm zOR>3@wMR{(sVPDANkfM^Hl-;wpuhOF6w3TVS$Z&K4v6m=k`Ep-*{n3M+2}iDmPi-O z6K|9*uWU@D9Me!B#BJ9sMMoD@^dPfU<)=r4ShD;`q-Lp)Bl`u(b}X@fZ%enQtfI0O zOPLx+Au0=_{k^r2y?BN8+D5mI{{eaJ3nYtN1w=TOKY~<(qIkPFfq-ABLJk(yIsKF% zGw0FOUeI5eaYN$f0>V?29c^m1AlHDPPuzmqvYIo=@AK-Ybsammc%{N)yQrMm-LvLU z)XyCec)grdsC8ui$M};rLQr+QaM9RC*94|`SJq)kDSd9Ua5RbjzV5WMvaSOD0$~hvNY1J70Yye!*w>O!2zT}a0ysLPSnV;< z6!c<92ECUSC+7tWZFTho+M;#0YrArmbFR9U-WJjM<#5;8$FCDH_qvJJ^X2Jy-EBQ=Ja=PU8m5fYTO$&n=9ZiJdGHza$40<~8AcPls{DyZjb$T$? zz-teug&EOyM(?TV^f(M zE91n#z~Oj?1N;o2$c39O+O|u=_Dc5n+yv~PTAK7R(fT1wj^2)FquE z7?Pe&Re5PP0;IAWL`8n&xveoNhc&46-%RIe^SGyGsO zCQKu2>5sKMVCePa{iKl?0Mnbh6xNuibG3LsevY{Ap8Sp}I8h-a^rNo+vHb;49{YN9 zB<$2c>uSL|$+&i48aX&WTu0afU3t0fb&Xd-z%N7R@truK*Jj-AEP?(U6B{_+wcL4y zD~QHoZ+p5Qn>v!otS4njL#+vJvR#vC=Pfkk5%O_<@aVQ>vB~JWhziRgajY_trJ^;} z7TBucwmvjd!FrXH*_l36H4&_tGS1wSC8S`kq4~0<%gpMWvR(4=#?iG)yd8v4?zC=W zwrpvT_b^cueC`0Nh&GR* z?bWmjy)K48?diIt2p!Z*&*wNBE&Z%`Dk~VHY^{?!-#KnuAi3uRBbNhw1rjhAmo{M`tfnU_>lN$iPZ<`6PRQk^5 zxaGdsq|jv4r5>+6|K;Wv76fZC$bfhzOF%>t`! zo0sQp>px*k2o?j3#F@R2xBac7f#~2r?YhI!+XCQZh_z#BjxBt6j!#5SP{!dH`SnI8Bs$Eb(yrC~yX} z2rYSEEx8#3(U5YIt7c(y>m`(jk^;VTAuIw(TN2m?#ku5b0?dQ2{Zd&l!yx&OWm`FlCIymY-g6DM6N>3Ra;?`&w%z+>*!en-Yn~9H z^Pb}fOmnW@Jqd1iH~@)OtW^&*8{y*{0+058jAlkQ3TBK@pPbGd9$(s41%&qXjxc%e z8~aL!mmNW%hqJqJT}X@yW+$mA5NK?7bWcz1&T|#@x`yZk*j(KEmHO&Cf#$AlZHV03 zwU$Y8xvtKBuhFq6H;MWj{DWw=vB5EA4EH$SI1$%lI2NTjaW-v`Jx)O`A)s@*uvFe) z{B!b1j;wn0m_tTj1{|WIg|oAn{)mS}qP4P9E6%Ken^S >-Aun5A4Gp>4U0IQJ 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 G)cz05(PHWV literal 0 HcmV?d00001 diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx new file mode 100644 index 0000000..6c53025 --- /dev/null +++ b/apps/web/app/layout.tsx @@ -0,0 +1,64 @@ +import Providers from '@/components/layout/providers'; +import { auth } from '@/lib/auth'; +import { cn } from '@repo/shadcn/lib/utils'; +import '@repo/shadcn/shadcn.css'; +import { Metadata } from 'next'; +import localFont from 'next/font/local'; +import NextTopLoader from 'nextjs-toploader'; +import { ReactNode } from 'react'; +import { Toaster } from 'sonner'; + +const GeistSans = localFont({ + src: './fonts/GeistVF.woff', + variable: '--font-geist-sans', +}); +const GeistMono = localFont({ + src: './fonts/GeistMonoVF.woff', + variable: '--font-geist-mono', +}); + +export const metadata = { + metadataBase: new URL('https://turbo-npn.onrender.com'), + title: { + default: 'Caja de Ahorro', + template: '%s | Caja de Ahorro', + }, + openGraph: { + type: 'website', + title: 'Caja de Ahorro', + description: 'Sistema integral para cajas de ahorro', + url: 'https://turbo-npn.onrender.com', + images: [ + { + url: '/og-bg.png', + width: 1200, + height: 628, + alt: 'Turbo NPN Logo', + }, + ], + }, +} satisfies Metadata; + +const RootLayout = async ({ + children, +}: Readonly<{ + children: ReactNode; +}>) => { + const session = await auth(); + return ( + + + + + + {children} + + + + ); +}; + +export default RootLayout; diff --git a/apps/web/app/not-found.tsx b/apps/web/app/not-found.tsx new file mode 100644 index 0000000..369f635 --- /dev/null +++ b/apps/web/app/not-found.tsx @@ -0,0 +1,29 @@ +'use client'; +import { Button } from '@repo/shadcn/button'; +import { RotateCw } from '@repo/shadcn/icon'; +import { cn } from '@repo/shadcn/lib/utils'; +import { useRouter } from 'next/navigation'; +import { useTransition } from 'react'; + +const NotFound = () => { + const router = useRouter(); + const [isPending, startTransition] = useTransition(); + return ( +
+

404 | Notfound

+

{"Oh no! This page doesn't exist."}

+ +
+ ); +}; + +export default NotFound; diff --git a/apps/web/app/og/mono.ttf b/apps/web/app/og/mono.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a70e69bd21aa901f93ffa280f691cf513d13859d GIT binary patch literal 277092 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
BG=NcYsq`sUJ!Z%im%(hSp)G?d9Hxa%G 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≦fXo=WUuIA1w6wc ze~u{Wb9+aAvsl~X_C7Z%y?9g7;~Om*Y2NJVomZXf_|uD${+1m5eV$(Co5AnzN_)iO zk$!cK{^^8XB#Pf|ReI_g>7U8bAN2I%hmHA_(u)Ts{h=KF;e=lO6#e&@)F53$FBT-#1w4#e0(;y$~(rx=ejI`&Up%eY&ep_joO?K6-ez zZ;d?Mke@c%aGy@gbI z$L}*w?_iv6ru7>UdX)z)CFBucK4MkIU7z>Mf*0FI{6)F)w4$V?KO%liwtvCJ4iDcg z_;DVu;}uA3opm|*k{o|O|GA?ff6V`+eY%R!`k5wvrmK&BobZjlK387X$m+d`=PH@I z1;!toyoHn8@fYEq2fJvuUK2j-C3r;2RiWTX`D8x+2T$P{G9~{PKo*R{csXgi_yk`1;L;*n zu4#}j9#7NcqBqxpaA`dSl2aGmRf-4W=y;mTgpbg>zPP&F$jOQ>C&$L>91@1kp|t3N z?%3F$zRbhesr_e;#?JhVV~%+OK(AA~5aQvF4JLDA#UKi02W>~F;z{`Rx8t1_?57GGZHnfb25qGv4sLJr1xj<;J* zqciQp$t?S)+RpMgw@K(#%d_;qaP;19yxs5vskO00GBWE{AL;y5%O`g0SgJmL6Tsx` z@K9HueoMdv!y5FQL1)mnE4|<-U#ym#UG;WW`YSra{^E>6@syzG5%C3u1Wx3UnrAxa z$RF!G(Ca)SKR=%2V$2)T8gFTS=EhQSe^Q+4g*ojYT|MH4inCsg9fIx`{H#%kbBo-R z+L&t;a=P?sl?V5yls5UPU&L5=U7$ROk=Al7vpB+94p9T9WKJlZ zuSe#nXszbEdW1f|PiRlHz^~sij&6Q4rbXG}om=?y^OQMN#611nq^(VBlh?nGJ)cAa zk18M9O=+DmpCtYEwgMzlm+BDf!cNA?MkAMA0v-Ylb}7=wWKS?7)jl1Ol>aizg<7@I zm=zga`JTh64cO+!&i9eE`2NA3s|9!3tkNf?$lr}iddz)nk8vr#H)VPs>Xh~)O|8=! zc&mYFMh0El*?-B`(|Dy34fgc3Zl0bidhI`aTQXwmS+dq + res.arrayBuffer(), + ); + const fontData = await font; + + return new ImageResponse( + ( +
+
+
+
+
+
+
+
20 ? 64 : 80, + letterSpacing: '-0.04em', + }} + > + {title} +
+
+ {`${description?.slice(0, 100)}`} +
+
+
+ ), + { + width: 1200, + height: 628, + fonts: [ + { + name: 'Jetbrains Mono', + data: fontData, + style: 'normal', + }, + ], + }, + ); +} diff --git a/apps/web/app/opengraph-image.tsx b/apps/web/app/opengraph-image.tsx new file mode 100644 index 0000000..c809e2b --- /dev/null +++ b/apps/web/app/opengraph-image.tsx @@ -0,0 +1,48 @@ +import { ImageResponse } from 'next/og'; + +export const runtime = 'edge'; + +// Image metadata +export const alt = `Opengraph Image`; +export const size = { + width: 800, + height: 400, +}; + +export const contentType = 'image/png'; + +export default async function Image() { + return new ImageResponse( + ( +
+ opengraph logo +
+ ), + { + ...size, + }, + ); +} diff --git a/apps/web/app/register/page.tsx b/apps/web/app/register/page.tsx new file mode 100644 index 0000000..603b7c3 --- /dev/null +++ b/apps/web/app/register/page.tsx @@ -0,0 +1,13 @@ +import { LoginForm } from "@/feactures/auth/components/signup-view"; + +const Page = () => { + return ( +
+
+ +
+
+ ) +} + +export default Page; \ No newline at end of file diff --git a/apps/web/components.json b/apps/web/components.json new file mode 100644 index 0000000..d8b7a3e --- /dev/null +++ b/apps/web/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "../../packages/shadcn/src/shadcn.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "lib": "@/lib", + "hooks": "@/hooks", + "ui": "../../packages/shadcn/components/ui", + "utils": "../../packages/shadcn/lib/utils" + } +} diff --git a/apps/web/components/breadcrumbs.tsx b/apps/web/components/breadcrumbs.tsx new file mode 100644 index 0000000..04939ba --- /dev/null +++ b/apps/web/components/breadcrumbs.tsx @@ -0,0 +1,42 @@ +'use client'; +import { useBreadcrumbs } from '@/hooks/use-breadcrumbs'; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from '@repo/shadcn/breadcrumb'; +import { Slash } from 'lucide-react'; +import { Fragment } from 'react'; + +export function Breadcrumbs() { + const items = useBreadcrumbs(); + if (items.length === 0) return null; + return ( + + + {items.map((item, index) => ( + + {index !== items.length - 1 && ( + + {item.title} + + )} + {index < items.length - 1 && ( + + + + )} + {index === items.length - 1 && ( + + {item.title} + + )} + + ))} + + + ); +} diff --git a/apps/web/components/icons.tsx b/apps/web/components/icons.tsx new file mode 100644 index 0000000..74a63bb --- /dev/null +++ b/apps/web/components/icons.tsx @@ -0,0 +1,92 @@ +import { + AlertTriangle, + ArrowRight, + Check, + ChevronLeft, + ChevronRight, + CircuitBoardIcon, + Command, + CreditCard, + File, + FileText, + HelpCircle, + Image, + Laptop, + LayoutDashboardIcon, + Loader2, + LogIn, + LucideIcon, + LucideProps, + LucideShoppingBag, + Moon, + MoreVertical, + Pizza, + Plus, + Settings, + SunMedium, + Trash, + Twitter, + User, + UserCircle2Icon, + UserPen, + UserX2Icon, + X, + Settings2, + ChartColumn, + NotepadText +} from 'lucide-react'; + +export type Icon = LucideIcon; + +export const Icons = { + dashboard: LayoutDashboardIcon, + logo: Command, + login: LogIn, + close: X, + product: LucideShoppingBag, + spinner: Loader2, + kanban: CircuitBoardIcon, + chevronLeft: ChevronLeft, + chevronRight: ChevronRight, + trash: Trash, + employee: UserX2Icon, + post: FileText, + page: File, + userPen: UserPen, + user2: UserCircle2Icon, + media: Image, + settings: Settings, + billing: CreditCard, + ellipsis: MoreVertical, + add: Plus, + warning: AlertTriangle, + user: User, + arrowRight: ArrowRight, + help: HelpCircle, + pizza: Pizza, + sun: SunMedium, + moon: Moon, + laptop: Laptop, + settings2: Settings2, + chartColumn: ChartColumn, + notepadText: NotepadText, + gitHub: ({ ...props }: LucideProps) => ( + + ), + twitter: Twitter, + check: Check +}; diff --git a/apps/web/components/layout/ThemeToggle/theme-provider.tsx b/apps/web/components/layout/ThemeToggle/theme-provider.tsx new file mode 100644 index 0000000..304b89c --- /dev/null +++ b/apps/web/components/layout/ThemeToggle/theme-provider.tsx @@ -0,0 +1,13 @@ +'use client'; + +import { + ThemeProvider as NextThemesProvider, + ThemeProviderProps +} from 'next-themes'; + +export default function ThemeProvider({ + children, + ...props +}: ThemeProviderProps) { + return {children}; +} diff --git a/apps/web/components/layout/ThemeToggle/theme-toggle.tsx b/apps/web/components/layout/ThemeToggle/theme-toggle.tsx new file mode 100644 index 0000000..1024860 --- /dev/null +++ b/apps/web/components/layout/ThemeToggle/theme-toggle.tsx @@ -0,0 +1,37 @@ +'use client'; +import { MoonIcon, SunIcon } from 'lucide-react'; +import { useTheme } from 'next-themes'; + +import { Button } from '@repo/shadcn/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@repo/shadcn/dropdown-menu'; +type CompProps = {}; +export default function ThemeToggle({}: CompProps) { + const { setTheme } = useTheme(); + return ( + + + + + + setTheme('light')}> + Light + + setTheme('dark')}> + Dark + + setTheme('system')}> + System + + + + ); +} diff --git a/apps/web/components/layout/app-sidebar.tsx b/apps/web/components/layout/app-sidebar.tsx new file mode 100644 index 0000000..cdeffca --- /dev/null +++ b/apps/web/components/layout/app-sidebar.tsx @@ -0,0 +1,58 @@ +'use client'; + +import { NavMain as ConfigMain, NavMain as GeneralMain, NavMain as AdministrationMain, NavMain as StatisticsMain, } from '@/components/nav-main'; +import { GeneralItems, AdministrationItems, StatisticsItems } from '@/constants/data'; +import { + Sidebar, + SidebarContent, + SidebarHeader, + SidebarRail, +} from '@repo/shadcn/sidebar'; +import { GalleryVerticalEnd } from 'lucide-react'; +import * as React from 'react'; +// import { NavItem } from '@/types'; +import { useSession } from 'next-auth/react'; + + +export const company = { + name: 'Sistema', + logo: GalleryVerticalEnd, + plan: 'FONDEMI', +}; + + + +export function AppSidebar({ ...props }: React.ComponentProps) { + const { data: session } = useSession(); + const userRole = session?.user.role[0]?.rol ? session.user.role[0].rol :''; + // console.log(AdministrationItems[0]?.role); + + return ( + + +
+
+ +
+
+ {company.name} + {company.plan} +
+
+
+ + + + {StatisticsItems[0]?.role?.includes(userRole) && + + } + + {AdministrationItems[0]?.role?.includes(userRole) && + + } + {/* */} + + +
+ ); +} diff --git a/apps/web/components/layout/header.tsx b/apps/web/components/layout/header.tsx new file mode 100644 index 0000000..d8a004d --- /dev/null +++ b/apps/web/components/layout/header.tsx @@ -0,0 +1,25 @@ +import { Separator } from '@repo/shadcn/separator'; +import { SidebarTrigger } from '@repo/shadcn/sidebar'; +import { Breadcrumbs } from '../breadcrumbs'; +import ThemeToggle from './ThemeToggle/theme-toggle'; +import { UserNav } from './user-nav'; + +export default function Header() { + return ( +
+
+ + + +
+ +
+ + +
+
+ ); +} diff --git a/apps/web/components/layout/page-container.tsx b/apps/web/components/layout/page-container.tsx new file mode 100644 index 0000000..4e579c5 --- /dev/null +++ b/apps/web/components/layout/page-container.tsx @@ -0,0 +1,22 @@ +import { ScrollArea } from '@repo/shadcn/scroll-area'; +import React from 'react'; + +export default function PageContainer({ + children, + scrollable = true, +}: { + children: React.ReactNode; + scrollable?: boolean; +}) { + return ( + <> + {scrollable ? ( + +
{children}
+
+ ) : ( +
{children}
+ )} + + ); +} diff --git a/apps/web/components/layout/providers.tsx b/apps/web/components/layout/providers.tsx new file mode 100644 index 0000000..b4b5a57 --- /dev/null +++ b/apps/web/components/layout/providers.tsx @@ -0,0 +1,48 @@ +'use client'; +import { ThemeProvider } from '@repo/shadcn/themes-provider'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { SessionProvider, SessionProviderProps } from 'next-auth/react'; +import { NuqsAdapter } from 'nuqs/adapters/next/app'; +import { ReactNode } from 'react'; + +type ProvidersProps = { + children: ReactNode; +}; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 2, + gcTime: 60 * 60 * 1000, // 1 hora para garbage collection + staleTime: 60 * 60 * 1000, // 1 hora para considerar datos obsoletos + refetchOnWindowFocus: false, // No recargar al enfocar la ventana + refetchOnMount: true, // Recargar al montar el componente + }, + }, +}); +const Providers = ({ + session, + children, +}: { + session: SessionProviderProps['session']; + children: ReactNode; +}) => { + return ( + <> + + + + {children} + + + + + ); +}; + +export default Providers; diff --git a/apps/web/components/layout/user-nav.tsx b/apps/web/components/layout/user-nav.tsx new file mode 100644 index 0000000..3360c1a --- /dev/null +++ b/apps/web/components/layout/user-nav.tsx @@ -0,0 +1,52 @@ +'use client'; +import { Avatar, AvatarFallback, AvatarImage } from '@repo/shadcn/avatar'; +import { Button } from '@repo/shadcn/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@repo/shadcn/dropdown-menu'; +import { signOut, useSession } from 'next-auth/react'; +import { redirect } from 'next/navigation'; + +export function UserNav() { + const { data: session } = useSession(); + if (session) { + return ( + + + + + + +
+

+ {session.user?.fullname} +

+

+ {session.user?.email} +

+
+
+ + + redirect('/dashboard/profile')}>Perfil + + + signOut()}> + Cerrar Sessión + +
+
+ ); + } +} diff --git a/apps/web/components/modal/alert-modal.tsx b/apps/web/components/modal/alert-modal.tsx new file mode 100644 index 0000000..4e35f1b --- /dev/null +++ b/apps/web/components/modal/alert-modal.tsx @@ -0,0 +1,50 @@ +'use client'; +import { Button } from '@repo/shadcn/button'; +import { Modal } from '@repo/shadcn/modal'; +import { useEffect, useState } from 'react'; + +interface AlertModalProps { + isOpen: boolean; + title?: string; + description?: string; + onClose: () => void; + onConfirm: () => void; + loading: boolean; +} + +export const AlertModal: React.FC = ({ + title = 'Are you sure?', + description = 'This action cannot be undone.', + isOpen, + onClose, + onConfirm, + loading, +}) => { + const [isMounted, setIsMounted] = useState(false); + + useEffect(() => { + setIsMounted(true); + }, []); + + if (!isMounted) { + return null; + } + + return ( + +
+ + +
+
+ ); +}; diff --git a/apps/web/components/nav-main.tsx b/apps/web/components/nav-main.tsx new file mode 100644 index 0000000..e9b3d21 --- /dev/null +++ b/apps/web/components/nav-main.tsx @@ -0,0 +1,113 @@ +'use client'; + +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from '@repo/shadcn/collapsible'; +import { + SidebarGroup, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, +} from '@repo/shadcn/sidebar'; +import { ChevronRightIcon } from 'lucide-react'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { Icons } from './icons'; +// import { useSession } from 'next-auth/react'; + +export function NavMain({ + titleGroup, + items, + role +}: { + titleGroup: string, + role: string, + items: { + title: string; + url: string; + icon?: keyof typeof Icons; + isActive?: boolean; + items?: { + title: string; + url: string; + icon?: keyof typeof Icons; + role?: string[]; + }[]; + }[]; +}) { + const pathname = usePathname(); + // const { data: session } = useSession(); + + // const userRole = session?.user.role[0]?.rol ? session.user.role[0].rol :''; + + // console.log(session?.user.role[0]?.rol); + return ( + + + {titleGroup} + + {items.map((item) => { + const Icon = item.icon ? Icons[item.icon] : Icons.logo; + return item?.items && item?.items?.length > 0 ? ( + + + + + {item.icon && } + {item.title} + + + + + + + {item.items?.map((subItem) => ( + subItem.role?.includes(role) && + + + + {subItem.title} + + + + ))} + + + + + ) : ( + + + + + {item.title} + + + + ); + })} + + + ); +} diff --git a/apps/web/components/nav-projects.tsx b/apps/web/components/nav-projects.tsx new file mode 100644 index 0000000..327469f --- /dev/null +++ b/apps/web/components/nav-projects.tsx @@ -0,0 +1,111 @@ +'use client'; + +import { + Folder, + Forward, + Frame, + PanelLeft, + PieChart, + Trash2, + type LucideIcon, +} from 'lucide-react'; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@repo/shadcn/dropdown-menu'; +import { + SidebarGroup, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuAction, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from '@repo/shadcn/sidebar'; + +const data = { + projects: [ + { + name: 'Design Engineering', + url: '#', + icon: Frame, + }, + { + name: 'Sales & Marketing', + url: '#', + icon: PieChart, + }, + { + name: 'Travel', + url: '#', + icon: Map, + }, + ], +}; + +export function NavProjects({ + projects, +}: { + projects: { + name: string; + url: string; + icon: LucideIcon; + }[]; +}) { + const { isMobile } = useSidebar(); + + return ( + + Projects + + {projects.map((item) => ( + + +
+ + {item.name} + + + + + + + More + + + + + + View Project + + + + Share Project + + + + + Delete Project + + + + + ))} + + + + More + + + + + ); +} diff --git a/apps/web/components/team-switcher.tsx b/apps/web/components/team-switcher.tsx new file mode 100644 index 0000000..d68e480 --- /dev/null +++ b/apps/web/components/team-switcher.tsx @@ -0,0 +1,117 @@ +'use client'; + +import { + AudioWaveform, + ChevronsUpDownIcon, + Command, + GalleryVerticalEnd, + PlusIcon, +} from 'lucide-react'; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from '@repo/shadcn/dropdown-menu'; +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from '@repo/shadcn/sidebar'; +import * as React from 'react'; + +const data = { + teams: [ + { + name: 'Acme Inc', + logo: GalleryVerticalEnd, + plan: 'Enterprise', + }, + { + name: 'Acme Corp.', + logo: AudioWaveform, + plan: 'Startup', + }, + { + name: 'Evil Corp.', + logo: Command, + plan: 'Free', + }, + ], +}; + +export function TeamSwitcher({ + teams, +}: { + teams: { + name: string; + logo: React.ElementType; + plan: string; + }[]; +}) { + const { isMobile } = useSidebar(); + const [activeTeam, setActiveTeam] = React.useState(teams[0]); + + if (!activeTeam) { + return null; + } + + return ( + + + + + +
+ +
+
+ {activeTeam.name} + {activeTeam.plan} +
+ +
+
+ + + Teams + + {teams.map((team, index) => ( + setActiveTeam(team)} + className="gap-2 p-2" + > +
+ +
+ {team.name} + ⌘{index + 1} +
+ ))} + + +
+ +
+
Add team
+
+
+
+
+
+ ); +} diff --git a/apps/web/constants/data.ts b/apps/web/constants/data.ts new file mode 100644 index 0000000..72ce81c --- /dev/null +++ b/apps/web/constants/data.ts @@ -0,0 +1,74 @@ +import { NavItem } from '@/types'; + +//Info: The following data is used for the sidebar navigation and Cmd K bar. +export const GeneralItems: NavItem[] = [ + { + title: 'Encuestas', + url: '/dashboard/encuestas/', + icon: 'notepadText', + shortcut: ['p', 'p'], + isActive: false, + items: [], // No child items + }, + +]; + + +export const AdministrationItems: NavItem[] = [ + { + title: 'Administracion', + url: '#', // Placeholder as there is no direct link for the parent + icon: 'settings2', + isActive: true, + role:['admin','superadmin','manager','user'], // sumatoria de los roles que si tienen acceso + + items: [ + { + title: 'Usuarios', + url: '/dashboard/administracion/usuario', + icon: 'userPen', + shortcut: ['m', 'm'], + role:['admin','superadmin'], + }, + { + title: 'Encuestas', + shortcut: ['l', 'l'], + url: '/dashboard/administracion/encuestas', + icon: 'login', + role:['admin','superadmin','manager','user'], + }, + ], + }, +]; + +export const StatisticsItems: NavItem[] = [ + { + title: 'Estadísticas', + url: '#', // Placeholder as there is no direct link for the parent + icon: 'chartColumn', + isActive: true, + role:['admin','superadmin','autoridad'], + + items: [ + // { + // title: 'Usuarios', + // url: '/dashboard/estadisticas/usuarios', + // icon: 'userPen', + // shortcut: ['m', 'm'], + // role:['admin','superadmin','autoridad'], + // }, + { + title: 'Encuestas', + shortcut: ['l', 'l'], + url: '/dashboard/estadisticas/encuestas', + icon: 'notepadText', + role:['admin','superadmin','autoridad'], + }, + ], + }, +]; + + + + + diff --git a/apps/web/eslint.config.js b/apps/web/eslint.config.js new file mode 100644 index 0000000..fdd04a6 --- /dev/null +++ b/apps/web/eslint.config.js @@ -0,0 +1,4 @@ +import { nextJsConfig } from '@repo/eslint-config/next-js'; + +/** @type {import("eslint").Linter.Config} */ +export default nextJsConfig; diff --git a/apps/web/feactures/auth/actions/login-action.ts b/apps/web/feactures/auth/actions/login-action.ts new file mode 100644 index 0000000..23caf96 --- /dev/null +++ b/apps/web/feactures/auth/actions/login-action.ts @@ -0,0 +1,17 @@ +'use server'; +import { safeFetchApi } from '@/lib'; +import { loginResponseSchema, UserFormValue } from '../schemas/login'; + +export const SignInAction = async (payload: UserFormValue) => { + const [error, data] = await safeFetchApi( + loginResponseSchema, + '/auth/sign-in', + 'POST', + payload, + ); + if (error) { + return error; + } else { + return data; + } +}; diff --git a/apps/web/feactures/auth/actions/refresh-token-action.ts b/apps/web/feactures/auth/actions/refresh-token-action.ts new file mode 100644 index 0000000..316d5ae --- /dev/null +++ b/apps/web/feactures/auth/actions/refresh-token-action.ts @@ -0,0 +1,20 @@ +'use server'; +import { safeFetchApi } from '@/lib'; +import { + RefreshTokenResponseSchema, + RefreshTokenValue, +} from '../schemas/refreshToken'; + +export const resfreshTokenAction = async (refreshToken: RefreshTokenValue) => { + const [error, data] = await safeFetchApi( + RefreshTokenResponseSchema, + '/auth/refreshToken', + 'POST', + refreshToken, + ); + if (error) { + console.error('Error:', error); + } else { + return data; + } +}; diff --git a/apps/web/feactures/auth/actions/register.ts b/apps/web/feactures/auth/actions/register.ts new file mode 100644 index 0000000..ee62723 --- /dev/null +++ b/apps/web/feactures/auth/actions/register.ts @@ -0,0 +1,27 @@ +'use server'; +import { safeFetchApi } from '@/lib/fetch.api'; +import { createUserValue, UsersMutate } from '../schemas/register'; + +export const registerUserAction = async (payload: createUserValue) => { + const { confirmPassword, ...payloadWithoutId } = payload; + + const [error, data] = await safeFetchApi( + UsersMutate, + '/auth/sing-up', + 'POST', + payloadWithoutId, + ); + + if (error) { + // console.error(error); + if (error.message === 'Username already exists') { + throw new Error('Ese usuario ya existe'); + } + if (error.message === 'Email already exists') { + throw new Error('Ese correo ya está en uso'); + } + throw new Error('Error al crear el usuario'); + } + + return payloadWithoutId; +}; \ No newline at end of file diff --git a/apps/web/feactures/auth/components/sigin-view.tsx b/apps/web/feactures/auth/components/sigin-view.tsx new file mode 100644 index 0000000..4ff9f94 --- /dev/null +++ b/apps/web/feactures/auth/components/sigin-view.tsx @@ -0,0 +1,35 @@ +import { + Card, + CardContent, +} from '@repo/shadcn/card'; + +import { cn } from '@repo/shadcn/lib/utils'; +import UserAuthForm from './user-auth-form'; + +export function LoginForm({ + className, + ...props +}: React.ComponentPropsWithoutRef<'div'>) { + + return ( +
+ + + +
+ Image +
+
+
+ {/*
+ By clicking continue, you agree to our Terms of Service{" "} + and Privacy Policy. +
*/} +
+ ) + +} diff --git a/apps/web/feactures/auth/components/signup-view.tsx b/apps/web/feactures/auth/components/signup-view.tsx new file mode 100644 index 0000000..93543ed --- /dev/null +++ b/apps/web/feactures/auth/components/signup-view.tsx @@ -0,0 +1,32 @@ +import { + Card, + CardContent, +} from '@repo/shadcn/card'; + +import { cn } from '@repo/shadcn/lib/utils'; +// import UserAuthForm from './user-auth-form'; +import UserAuthForm from './user-register-form'; + +export function LoginForm({ + className, + ...props +}: React.ComponentPropsWithoutRef<'div'>) { + + return ( +
+ + + + {/*
+ Image +
*/} +
+
+
+ ) + +} diff --git a/apps/web/feactures/auth/components/user-auth-form.tsx b/apps/web/feactures/auth/components/user-auth-form.tsx new file mode 100644 index 0000000..4cec084 --- /dev/null +++ b/apps/web/feactures/auth/components/user-auth-form.tsx @@ -0,0 +1,139 @@ +'use client'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Button } from '@repo/shadcn/button'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@repo/shadcn/form'; +import { Input } from '@repo/shadcn/input'; +import { signIn } from 'next-auth/react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { useState, useTransition } from 'react'; +import { useForm } from 'react-hook-form'; +import { toast } from 'sonner'; +import { UserFormValue, formSchema } from '../schemas/login'; + +export default function UserAuthForm() { + const router = useRouter(); + const searchParams = useSearchParams(); + const callbackUrl = searchParams.get('callbackUrl'); + const [loading, startTransition] = useTransition(); + const [error, SetError] = useState(null); + const defaultValues = { + username: '', + password: '', + }; + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues, + }); + + const onSubmit = async (data: UserFormValue) => { + SetError(null); // Limpia cualquier error previo al intentar iniciar sesión + startTransition(async () => { + try { + const login = await signIn('credentials', { + username: data.username, + password: data.password, + redirect: false, // No queremos una redirección automática aquí + }); + + + + if (login?.error) { + const errorMessage = + login.error === 'CredentialsSignin' + ? 'Usuario o contraseña incorrectos' + : 'Contacte al Administrador'; + SetError(errorMessage); + toast.error(errorMessage); + } + + // Si la autenticación es exitosa y `redirect: false`, necesitamos redirigir manualmente + if (login?.ok && !login?.error) { + toast.success('Ingreso Exitoso!'); + router.push(callbackUrl ?? '/dashboard'); + } + } catch (error) { + console.error('Error durante el inicio de sesión:', error); + toast.error('Hubo un error inesperado'); + } + }); + }; + + return ( + <> +
+ + +
+
+

Sistema Integral Fondemi

+

+ Ingresa tus datos +

+
+
+ ( + + Usuario + + + + + + )} + /> +
+
+ + ( + + Contraseña + + + + + + )} + /> + +
+ {error && ( + {error} + )}{' '} + +
+ No tienes una cuenta?{" "} + + Registrate + +
+
+
+ + + ); +} diff --git a/apps/web/feactures/auth/components/user-register-form.tsx b/apps/web/feactures/auth/components/user-register-form.tsx new file mode 100644 index 0000000..98e485e --- /dev/null +++ b/apps/web/feactures/auth/components/user-register-form.tsx @@ -0,0 +1,321 @@ +'use client'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Button } from '@repo/shadcn/button'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@repo/shadcn/form'; +import { Input } from '@repo/shadcn/input'; +import { signIn } from 'next-auth/react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { useState, useTransition } from 'react'; +import { useForm } from 'react-hook-form'; +import { toast } from 'sonner'; +import { SelectSearchable } from '@repo/shadcn/select-searchable' + +import { createUserValue, createUser } from '../schemas/register'; +import { useRegisterUser } from "../hooks/use-mutation-users"; + +import React from 'react'; +import { useStateQuery, useMunicipalityQuery, useParishQuery } from '@/feactures/location/hooks/use-query-location'; + +export default function UserAuthForm() { + const router = useRouter(); + const searchParams = useSearchParams(); + const callbackUrl = searchParams.get('callbackUrl'); + const [loading, startTransition] = useTransition(); + const [error, SetError] = useState(null); + + const [state, setState] = React.useState(0); + const [municipality, setMunicipality] = React.useState(0); + const [parish, setParish] = React.useState(0); + + const [disabledMunicipality, setDisabledMunicipality] = React.useState(true); + const [disabledParish, setDisabledParish] = React.useState(true); + + const { data : dataState } = useStateQuery() + const { data : dataMunicipality } = useMunicipalityQuery(state) + const { data : dataParish } = useParishQuery(municipality) + + const stateOptions = dataState?.data || [{id:0,name:'Sin estados'}] + const municipalityOptions = dataMunicipality?.data || [{id:0,stateId:0,name:'Sin Municipios'}] + const parishOptions = dataParish?.data || [{id:0,municipalityId:0,name:'Sin Parroquias'}] + + + const defaultValues = { + username: '', + password: '', + confirmPassword: '', + fullname: '', + lastname: '', + email: '', + phone: '', + role: 5 + }; + const form = useForm({ + resolver: zodResolver(createUser), + defaultValues, + }); + + const { + mutate: saveAccountingAccounts, + isPending: isSaving, + isError, + } = useRegisterUser(); + + const onSubmit = async (data: createUserValue) => { + SetError(null); + + const formData = {role: 5, ...data } + + saveAccountingAccounts(formData, { + onSuccess: () => { + form.reset(); + toast.success('Registro Exitoso!'); + router.push(callbackUrl ?? '/'); + }, + onError: (e) => { + // form.setError('root', { + // type: 'manual', + // message: 'Error al guardar la cuenta contable', + // }); + SetError(e.message); + toast.error(e.message); + }, + }) + } + + return ( + <> +
+ + +
+
+

Sistema Integral Fondemi

+

+ Ingresa tus datos +

+ { error ? ( +

+ {error} +

+ ): null } +
+ +
+ + ( + + Usuario + + + + + + )} + /> + + ( + + Nombre Completo + + + + + + )} + /> + + ( + + Teléfono + + + + + + )} + /> + + ( + + Correo + + + + + + )} + /> + + ( + + Estado + + ({ + value: item.id.toString(), + label: item.name, + })) || [] + } + onValueChange={(value : any) => + {field.onChange(Number(value)); setState(value); setDisabledMunicipality(false); setDisabledParish(true)} + } + placeholder="Selecciona un estado" + defaultValue={field.value?.toString()} + // disabled={readOnly} + /> + + + )} + /> + + ( + + Municipio + + ({ + value: item.id.toString(), + label: item.name, + })) || [] + } + onValueChange={(value : any) => + {field.onChange(Number(value)); setMunicipality(value); setDisabledParish(false)} + } + placeholder="Selecciona un Municipio" + defaultValue={field.value?.toString()} + disabled={disabledMunicipality} + /> + + + )} + /> + + ( + + Parroquia + + ({ + value: item.id.toString(), + label: item.name, + })) || [] + } + onValueChange={(value : any) => + field.onChange(Number(value)) + } + placeholder="Selecciona una Parroquia" + defaultValue={field.value?.toString()} + disabled={disabledParish} + /> + + + )} + /> + + ( + + Contraseña + + + + + + )} + /> + + ( + + Repita la contraseña + + + + + + )} + /> + +
+ + {error && ( + {error} + )}{' '} + +
+ ¿Ya tienes una cuenta?{" "} + Inicia Sesión +
+
+
+ + + ); +} diff --git a/apps/web/feactures/auth/hooks/use-mutation-users.ts b/apps/web/feactures/auth/hooks/use-mutation-users.ts new file mode 100644 index 0000000..0de393c --- /dev/null +++ b/apps/web/feactures/auth/hooks/use-mutation-users.ts @@ -0,0 +1,14 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { createUserValue } from "../schemas/register"; +import { registerUserAction } from "../actions/register"; + +// Create mutation +export function useRegisterUser() { + const queryClient = useQueryClient(); + const mutation = useMutation({ + mutationFn: (data: createUserValue) => registerUserAction(data), + // onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }), + // onError: (e) => + }) + return mutation +} \ No newline at end of file diff --git a/apps/web/feactures/auth/schemas/login.ts b/apps/web/feactures/auth/schemas/login.ts new file mode 100644 index 0000000..119eae1 --- /dev/null +++ b/apps/web/feactures/auth/schemas/login.ts @@ -0,0 +1,46 @@ +import { z } from 'zod'; + +// Definir esquema de validación con Zod para el formulario +export const formSchema = z.object({ + username: z + .string() + .min(5, { message: 'Usuario debe tener minimo 5 caracteres' }), + password: z + .string() + .min(6, { message: 'La contraseña debe tener al menos 6 caracteres' }), +}); + +export type UserFormValue = z.infer; + +// Esquema para el rol +const rolSchema = z.object({ + id: z.number(), + rol: z.string(), +}); + +// Esquema para el usuario +const userSchema = z.object({ + id: z.number(), + username: z.string(), + fullname: z.string(), + email: z.string().email(), + rol: z.array(rolSchema), +}); + +// Esquema para los tokens +export const tokensSchema = z.object({ + access_token: z.string(), + access_expire_in: z.number(), + refresh_token: z.string(), + refresh_expire_in: z.number(), +}); + +// Esquema final para la respuesta del backend +export const loginResponseSchema = z.object({ + message: z.string(), + user: userSchema, + tokens: tokensSchema, +}); + +// Tipo TypeScript basado en el esquema de Zod +export type LoginResponse = z.infer; diff --git a/apps/web/feactures/auth/schemas/refreshToken.ts b/apps/web/feactures/auth/schemas/refreshToken.ts new file mode 100644 index 0000000..4590648 --- /dev/null +++ b/apps/web/feactures/auth/schemas/refreshToken.ts @@ -0,0 +1,14 @@ +import { z } from 'zod'; +import { tokensSchema } from './login'; + +// Esquema para el refresh token +export const refreshTokenSchema = z.object({ + token: z.string(), +}); + +export type RefreshTokenValue = z.infer; + +// Esquema final para la respuesta del backend +export const RefreshTokenResponseSchema = z.object({ + tokens: tokensSchema, +}); diff --git a/apps/web/feactures/auth/schemas/register.ts b/apps/web/feactures/auth/schemas/register.ts new file mode 100644 index 0000000..5bef38e --- /dev/null +++ b/apps/web/feactures/auth/schemas/register.ts @@ -0,0 +1,35 @@ +import { z } from 'zod'; +// Definir esquema de validación con Zod para el formulario +export const createUser = z.object({ + username: z.string().min(5, { message: "Debe de tener 5 o más caracteres" }), + password: z.string().min(8, { message: "Debe de tener 8 o más caracteres" }), + email: z.string().email({ message: "Correo no válido" }), + fullname: z.string(), + phone: z.string(), + state: z.number(), + municipality: z.number(), + parish: z.number(), + confirmPassword: z.string(), +}) + .refine((data) => data.password === data.confirmPassword, { + message: 'La contraseña no coincide', + path: ['confirmPassword'], +}) + +export type createUserValue = z.infer; + +export const user = z.object({ + id: z.number().optional(), + username: z.string(), + email: z.string(), + fullname: z.string(), + phone: z.string().nullable(), + isActive: z.boolean(), + role: z.string() +}); + +export const UsersMutate = z.object({ + message: z.string(), + data: user, +}) + diff --git a/apps/web/feactures/location/actions/actions.ts b/apps/web/feactures/location/actions/actions.ts new file mode 100644 index 0000000..a344717 --- /dev/null +++ b/apps/web/feactures/location/actions/actions.ts @@ -0,0 +1,36 @@ +'use server'; +import { safeFetchApi } from '@/lib/fetch.api'; +import {responseStates, responseMunicipalities, responseParishes} from '../schemas/users'; + +// import { auth } from '@/lib/auth'; + + +export const getStateAction = async () => { + const [error, response] = await safeFetchApi( + responseStates, + `/location/state/`, + 'GET' + ); + if (error) throw new Error(error.message); + return response; +}; + +export const getMunicipalityAction = async (id : number) => { + const [error, response] = await safeFetchApi( + responseMunicipalities, + `/location/municipality/${id}`, + 'GET' + ); + if (error) throw new Error(error.message); + return response; +}; + +export const getParishAction = async (id : number) => { + const [error, response] = await safeFetchApi( + responseParishes, + `/location/parish/${id}`, + 'GET' + ); + if (error) throw new Error(error.message); + return response; +}; \ No newline at end of file diff --git a/apps/web/feactures/location/hooks/use-query-location.ts b/apps/web/feactures/location/hooks/use-query-location.ts new file mode 100644 index 0000000..c9e0e36 --- /dev/null +++ b/apps/web/feactures/location/hooks/use-query-location.ts @@ -0,0 +1,16 @@ +'use client' +import { useSafeQuery } from "@/hooks/use-safe-query"; +import { getStateAction, getMunicipalityAction, getParishAction } from "../actions/actions"; + +// Hook for users +export function useStateQuery() { + return useSafeQuery(['state'], () => getStateAction()) +} + +export function useMunicipalityQuery( stateId : number ) { + return useSafeQuery(['municipality', stateId], () => getMunicipalityAction(stateId)) +} + +export function useParishQuery(municipalityId : number) { + return useSafeQuery(['parish', municipalityId], () => getParishAction(municipalityId)) +} \ No newline at end of file diff --git a/apps/web/feactures/location/schemas/users.ts b/apps/web/feactures/location/schemas/users.ts new file mode 100644 index 0000000..e35aaff --- /dev/null +++ b/apps/web/feactures/location/schemas/users.ts @@ -0,0 +1,94 @@ +import { z } from 'zod'; + +export type SurveyTable = z.infer; +export type CreateUser = z.infer; +export type UpdateUser = z.infer; + +export const user = z.object({ + id: z.number().optional(), + username: z.string(), + email: z.string(), + fullname: z.string(), + phone: z.string().nullable(), + isActive: z.boolean(), + role: z.string(), + state: z.string().optional().nullable(), + municipality: z.string().optional().nullable(), + parish: z.string().optional().nullable(), +}); + +export const createUser = z.object({ + id: z.number().optional(), + username: z.string().min(5, { message: "Debe de tener 5 o más caracteres" }), + password: z.string().min(8, { message: "Debe de tener 8 o más caracteres" }), + email: z.string().email({ message: "Correo no válido" }), + fullname: z.string(), + phone: z.string(), + confirmPassword: z.string(), + role: z.number() +}) +.refine((data) => data.password === data.confirmPassword, { + message: 'La contraseña no coincide', + path: ['confirmPassword'], +}) + +export const updateUser = z.object({ + id: z.number(), + username: z.string().min(5, { message: "Debe de tener 5 o más caracteres" }).or(z.literal('')), + password: z.string().min(6, { message: "Debe de tener 6 o más caracteres" }).or(z.literal('')), + email: z.string().email({ message: "Correo no válido" }).or(z.literal('')), + fullname: z.string().optional(), + phone: z.string().optional(), + role: z.number().optional(), + isActive: z.boolean().optional(), + state: z.number().optional().nullable(), + municipality: z.number().optional().nullable(), + parish: z.number().optional().nullable(), +}) + +export const surveysApiResponseSchema = z.object({ + message: z.string(), + data: z.array(user), + meta: z.object({ + page: z.number(), + limit: z.number(), + totalCount: z.number(), + totalPages: z.number(), + hasNextPage: z.boolean(), + hasPreviousPage: z.boolean(), + nextPage: z.number().nullable(), + previousPage: z.number().nullable(), + }), +}) + +export const states = z.object({ + id: z.number(), + name: z.string() +}) + +export const municipalities = z.object({ + id: z.number(), + stateId: z.number(), + name: z.string() +}) + +export const parishes = z.object({ + id: z.number(), + municipalityId: z.number(), + name: z.string() +}) + +export const responseStates = z.object({ + message: z.string(), + data: z.array(states), +}) + +export const responseMunicipalities = z.object({ + message: z.string(), + data: z.array(municipalities), +}) + +export const responseParishes = z.object({ + message: z.string(), + data: z.array(parishes), +}) \ No newline at end of file diff --git a/apps/web/feactures/statistics/actions/surveys-statistics-actions.ts b/apps/web/feactures/statistics/actions/surveys-statistics-actions.ts new file mode 100644 index 0000000..31bfbee --- /dev/null +++ b/apps/web/feactures/statistics/actions/surveys-statistics-actions.ts @@ -0,0 +1,27 @@ +'use server'; +import { safeFetchApi } from '@/lib/fetch.api'; +import { SurveyStatisticsData } from '../schemas/statistics'; +import { SurveyStatisticsSchema } from '../schemas/statistics-schema'; + + +export const getSurveysStatistics = async (): Promise => { + + const [error, data] = await safeFetchApi( + SurveyStatisticsSchema, + `surveys/statistics`, + 'GET', + ); + + if (error) { + console.log(error); + // console.log(error.details); + throw new Error('Ocurrio un error'); + } + + if (!data) { + throw new Error('No statistics data available'); + } + + return data?.data; +}; + diff --git a/apps/web/feactures/statistics/components/survey-details.tsx b/apps/web/feactures/statistics/components/survey-details.tsx new file mode 100644 index 0000000..ac65d34 --- /dev/null +++ b/apps/web/feactures/statistics/components/survey-details.tsx @@ -0,0 +1,127 @@ +'use client'; + +import { useState } from 'react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@repo/shadcn/card'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@repo/shadcn/select'; +import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from 'recharts'; +import { SurveyStatisticsData } from '../schemas/statistics'; + +interface SurveyDetailsProps { + data: SurveyStatisticsData | undefined; +} + +export function SurveyDetails({ data }: SurveyDetailsProps) { + const [selectedSurvey, setSelectedSurvey] = useState(''); + + if (!data || !data.surveyDetails || data.surveyDetails.length === 0) { + return ( + + +

No hay datos detallados disponibles

+
+
+ ); + } + + // Set default selected survey if none is selected + if (!selectedSurvey && data.surveyDetails.length > 0) { + setSelectedSurvey(data.surveyDetails?.[0]?.id.toString() ?? ''); + } + + const currentSurvey = data.surveyDetails.find( + (survey) => survey.id.toString() === selectedSurvey + ); + + const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8', '#82ca9d', '#ffc658']; + + return ( +
+ + + Detalles por Encuesta + Análisis detallado de respuestas por encuesta + + + + + + + {currentSurvey && ( + <> +
+ + + {currentSurvey.title} + {currentSurvey.description} + + +
+
+ Total de respuestas: + {currentSurvey.totalResponses} +
+
+ Audiencia objetivo: + {currentSurvey.targetAudience} +
+
+ Fecha de creación: + {new Date(currentSurvey.createdAt).toLocaleDateString()} +
+ {currentSurvey.closingDate && ( +
+ Fecha de cierre: + {new Date(currentSurvey.closingDate).toLocaleDateString()} +
+ )} +
+
+
+ + {currentSurvey.questionStats && currentSurvey.questionStats.length > 0 && ( + + + Distribución de Respuestas + + + + + `${name}: ${(percent * 100).toFixed(0)}%`} + outerRadius={80} + fill="#8884d8" + dataKey="count" + nameKey="label" + > + {currentSurvey.questionStats.map((entry, index) => ( + + ))} + + [`${value}`, name]} /> + {/* */} + + + + + )} +
+ + )} +
+ ); +} \ No newline at end of file diff --git a/apps/web/feactures/statistics/components/survey-overview.tsx b/apps/web/feactures/statistics/components/survey-overview.tsx new file mode 100644 index 0000000..6233f80 --- /dev/null +++ b/apps/web/feactures/statistics/components/survey-overview.tsx @@ -0,0 +1,82 @@ +'use client'; + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@repo/shadcn/card'; +import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts'; +import { SurveyStatisticsData } from '../schemas/statistics'; + +interface SurveyOverviewProps { + data: SurveyStatisticsData | undefined; +} + +export function SurveyOverview({ data }: SurveyOverviewProps) { + if (!data) return null; + + const { totalSurveys, totalResponses, completionRate, surveysByMonth } = data; + + const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8']; + + return ( +
+ + + Total de Encuestas + + +
{totalSurveys}
+

+ Encuestas creadas en la plataforma +

+
+
+ + + Total de Respuestas + + +
{totalResponses}
+

+ Respuestas recibidas en todas las encuestas +

+
+
+ + {/* + Tasa de Completado + + +
{totalSurveys/totalResponses}
+

+ Porcentaje de encuestas completadas +

+
*/} +
+ + + + Encuestas por Mes + Distribución de encuestas creadas por mes + + + + + + + + + + + + + + +
+ ); +} \ No newline at end of file diff --git a/apps/web/feactures/statistics/components/survey-responses.tsx b/apps/web/feactures/statistics/components/survey-responses.tsx new file mode 100644 index 0000000..894e3d9 --- /dev/null +++ b/apps/web/feactures/statistics/components/survey-responses.tsx @@ -0,0 +1,78 @@ +'use client'; + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@repo/shadcn/card'; +import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts'; +import { SurveyStatisticsData } from '../schemas/statistics'; + +interface SurveyResponsesProps { + data: SurveyStatisticsData | undefined; +} + +export function SurveyResponses({ data }: SurveyResponsesProps) { + if (!data) return null; + + const { responsesByAudience, responseDistribution } = data; + + const COLORS = ['#0088FE', '#8884d8', '#00C49F', '#FFBB28', '#FF8042']; + + return ( +
+ + + Respuestas por Audiencia + Distribución de respuestas según el tipo de audiencia + + + + + `${name}: ${(percent * 100).toFixed(0)}%`} + outerRadius={80} + fill="#8884d8" + dataKey="value" + > + {responsesByAudience.map((entry, index) => ( + + ))} + + [`${value} respuestas`, name]} /> + + + + + + + + + Distribución de Respuestas + Cantidad de respuestas por encuesta + + + + + + + + + + + + + + +
+ ); +} \ No newline at end of file diff --git a/apps/web/feactures/statistics/components/survey-statistics.tsx b/apps/web/feactures/statistics/components/survey-statistics.tsx new file mode 100644 index 0000000..71f759f --- /dev/null +++ b/apps/web/feactures/statistics/components/survey-statistics.tsx @@ -0,0 +1,35 @@ +'use client'; + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@repo/shadcn/card'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@repo/shadcn/tabs'; +import { useSurveysStatsQuery } from '../hooks/use-query-statistics'; +import { SurveyOverview } from './survey-overview'; +import { SurveyResponses } from './survey-responses'; +import { SurveyDetails } from './survey-details'; + +export function SurveyStatistics() { + const { data, isLoading } = useSurveysStatsQuery(); + + if (isLoading) { + return
Cargando estadísticas...
; + } + + return ( + + + Resumen General + Respuestas + Detalles por Encuesta + + + + + + + + + + + + ); +} \ No newline at end of file diff --git a/apps/web/feactures/statistics/hooks/use-query-statistics.ts b/apps/web/feactures/statistics/hooks/use-query-statistics.ts new file mode 100644 index 0000000..31b4f10 --- /dev/null +++ b/apps/web/feactures/statistics/hooks/use-query-statistics.ts @@ -0,0 +1,8 @@ +import { useSafeQuery } from '@/hooks/use-safe-query'; +import { getSurveysStatistics } from '../actions/surveys-statistics-actions'; + + +// Hook for all survesys +export function useSurveysStatsQuery() { + return useSafeQuery(['surveys-statistics'], () => getSurveysStatistics()) +} diff --git a/apps/web/feactures/statistics/schemas/statistics-schema.ts b/apps/web/feactures/statistics/schemas/statistics-schema.ts new file mode 100644 index 0000000..8dc73ed --- /dev/null +++ b/apps/web/feactures/statistics/schemas/statistics-schema.ts @@ -0,0 +1,59 @@ +import { z } from 'zod'; + +// Esquema para QuestionStat +export const QuestionStatSchema = z.object({ + questionId: z.string(), + label: z.string(), + count: z.number(), +}); + +// Esquema para SurveyDetail +export const SurveyDetailSchema = z.object({ + id: z.number(), + title: z.string(), + description: z.string(), + totalResponses: z.number(), + targetAudience: z.string(), + createdAt: z.string(), + closingDate: z.string().optional(), + questionStats: z.array(QuestionStatSchema), +}); + + +// Esquema para SurveyStatisticsData +export const SurveyStatisticsDataSchema = z.object({ + totalSurveys: z.number(), + totalResponses: z.number(), + completionRate: z.number(), + surveysByMonth: z.array( + z.object({ + month: z.string(), + count: z.number(), + }) + ), + responsesByAudience: z.array( + z.object({ + name: z.string(), + value: z.number(), + }) + ), + responseDistribution: z.array( + z.object({ + title: z.string(), + responses: z.number(), + }) + ), + surveyDetails: z.array(SurveyDetailSchema), + // surveyDetails: z.array(z.any()), +}); + +// Response schemas for the API create, update +export const SurveyStatisticsSchema = z.object({ + message: z.string(), + data: SurveyStatisticsDataSchema, +}); + +// Tipos inferidos de Zod +export type SurveyStatisticsType = z.infer; +export type SurveyDetailType = z.infer; +export type QuestionStatType = z.infer; \ No newline at end of file diff --git a/apps/web/feactures/statistics/schemas/statistics.ts b/apps/web/feactures/statistics/schemas/statistics.ts new file mode 100644 index 0000000..d278d64 --- /dev/null +++ b/apps/web/feactures/statistics/schemas/statistics.ts @@ -0,0 +1,35 @@ +export interface SurveyStatisticsData { + totalSurveys: number; + totalResponses: number; + completionRate: number; + surveysByMonth: { + month: string; + count: number; + }[]; + responsesByAudience: { + name: string; + value: number; + }[]; + responseDistribution: { + title: string; + responses: number; + }[]; + surveyDetails: SurveyDetail[]; +} + +export interface SurveyDetail { + id: number; + title: string; + description: string; + totalResponses: number; + targetAudience: string; + createdAt: string; + closingDate?: string; + questionStats: QuestionStat[]; +} + +export interface QuestionStat { + questionId: string; + label: string; + count: number; +} \ No newline at end of file diff --git a/apps/web/feactures/surveys/actions/surveys-actions.ts b/apps/web/feactures/surveys/actions/surveys-actions.ts new file mode 100644 index 0000000..e680510 --- /dev/null +++ b/apps/web/feactures/surveys/actions/surveys-actions.ts @@ -0,0 +1,216 @@ +'use server'; +import { safeFetchApi } from '@/lib/fetch.api'; +import { SurveyAnswerMutate, Survey, SurveyResponse, surveysApiResponseSchema, suveryApiMutationResponseSchema, suveryResponseDeleteSchema, surveysApiResponseForUserSchema } from '../schemas/survey'; +import { auth } from '@/lib/auth'; + + + +const transformSurvey = (survey: any) => { + + return survey.map((survey: any) => { + return { + ...survey, + published: survey.published ? 'Publicada': 'Borrador', + } + }) +}; + + +export const getSurveysAction = async (params: { + page?: number; + limit?: number; + search?: string; + sortBy?: string; + sortOrder?: 'asc' | 'desc'; +}) => { + + const searchParams = new URLSearchParams({ + page: (params.page || 1).toString(), + limit: (params.limit || 10).toString(), + ...(params.search && { search: params.search }), + ...(params.sortBy && { sortBy: params.sortBy }), + ...(params.sortOrder && { sortOrder: params.sortOrder }), + }); + + const [error, response] = await safeFetchApi( + surveysApiResponseSchema, + `/surveys?${searchParams}`, + 'GET', + ); + + if (error) { + console.error('Error:', error); + throw new Error(error.message); + } + + +const transformedData = response?.data ? transformSurvey(response?.data) : undefined; + + return { + data: transformedData, + meta: response?.meta || { + page: 1, + limit: 10, + totalCount: 0, + totalPages: 1, + hasNextPage: false, + hasPreviousPage: false, + nextPage: null, + previousPage: null, + }, + }; +}; + + +export const getSurveysForUserAction = async (params: { + page?: number; + limit?: number; + search?: string; + sortBy?: string; + sortOrder?: 'asc' | 'desc'; +}) => { + const session = await auth() + const searchParams = new URLSearchParams({ + page: (params.page || 1).toString(), + limit: (params.limit || 10).toString(), + ...(params.search && { search: params.search }), + ...(params.sortBy && { sortBy: params.sortBy }), + ...(params.sortOrder && { sortOrder: params.sortOrder }), + }); + const rol = { + rol: session?.user.role + } + const [error, response] = await safeFetchApi( + surveysApiResponseForUserSchema, + `/surveys/for-user?${searchParams}`, + 'POST', + rol + ); + + if (error) { + console.error('Error:', error); + throw new Error(error.message); + } + + +const transformedData = response?.data ? transformSurvey(response?.data) : undefined; + + return { + data: transformedData, + meta: response?.meta || { + page: 1, + limit: 10, + totalCount: 0, + totalPages: 1, + hasNextPage: false, + hasPreviousPage: false, + nextPage: null, + previousPage: null, + }, + }; +}; + +export const createSurveyAction = async (payload: Survey) => { + const { id, ...payloadWithoutId } = payload; + + const [error, data] = await safeFetchApi( + suveryApiMutationResponseSchema, + '/surveys', + 'POST', + payloadWithoutId, + ); + + if (error) { + if (error.message === 'Survey already exists') { + throw new Error('Ya existe una encuesta con ese titulo'); + } + // console.error('Error:', error); + throw new Error('Error al crear la encuesta'); + } + + return data; +}; + +export const updateSurveyAction = async (payload: Survey) => { + const { id, ...payloadWithoutId } = payload; + + + const [error, data] = await safeFetchApi( + suveryApiMutationResponseSchema, + `/surveys/${id}`, + 'PATCH', + payloadWithoutId, + ); + + if (error) { + if (error.message === 'Survey already exists') { + throw new Error('Ya existe otra encuesta con ese titulo'); + } + if (error.message === 'Survey not found') { + throw new Error('No se encontró la encuesta'); + } + // console.error('Error:', error); + // throw new Error(error.message); + throw new Error('Error al actualizar la encuesta'); + } + + return data; +}; + +export const deleteSurveyAction = async (id: number) => { + const [error, data] = await safeFetchApi( + suveryResponseDeleteSchema, + `/surveys/${id}`, + 'DELETE', + ); + + if (error) { + console.error('Error:', error); + throw new Error(error.message); + } + + return data; +}; + +export const getSurveyByIdAction = async (id: number) => { + const [error, data] = await safeFetchApi( + suveryApiMutationResponseSchema, + `/surveys/${id}`, + 'GET', + ); + + if (error) { + console.error('❌ Error en la API:', error); + throw new Error(error.message); + } + + return data; +}; + +export const saveSurveysAction = async (payload: Survey) => { + try { + if (payload.id) { + return await updateSurveyAction(payload); + } else { + return await createSurveyAction(payload); + } + } catch (error: any) { + throw new Error(error.message || 'Error saving account surveys'); + } +}; + +export const saveSurveyAnswer = async (payload: SurveyResponse) => { + const [error, data] = await safeFetchApi( + SurveyAnswerMutate, + '/surveys/answers', + 'POST', + payload, + ) + + if (error) { + console.error('Error:', error); + throw new Error(error.message); + } + + return data; +} \ No newline at end of file diff --git a/apps/web/feactures/surveys/components/admin/question-config-modal.tsx b/apps/web/feactures/surveys/components/admin/question-config-modal.tsx new file mode 100644 index 0000000..bdf0bf4 --- /dev/null +++ b/apps/web/feactures/surveys/components/admin/question-config-modal.tsx @@ -0,0 +1,245 @@ +// Modal para configurar cada pregunta individual +// Funcionalidades: +// - Configuración específica según el tipo de pregunta +// - Para títulos: solo contenido +// - Para preguntas simples: texto de la pregunta +// - Para preguntas con opciones: texto y lista de opciones +// - Switch para hacer la pregunta obligatoria/opcional +'use client'; + +import { Button } from '@repo/shadcn/button'; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@repo/shadcn/dialog'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@repo/shadcn/form'; +import { Input } from '@repo/shadcn/input'; +import { Switch } from '@repo/shadcn/switch'; +import { useEffect } from 'react'; +import { useFieldArray, useForm } from 'react-hook-form'; +import { QuestionType } from '../../schemas/survey'; +import { Plus, Trash2 } from 'lucide-react'; + +interface QuestionConfigModalProps { + isOpen: boolean; + onClose: () => void; + question: any; + onSave: (config: any) => void; +} + +export function QuestionConfigModal({ + isOpen, + onClose, + question, + onSave, +}: QuestionConfigModalProps) { + const form = useForm({ + defaultValues: { + content: '', + question: '', + required: false, + options: [{ id: '1', text: '' }], + }, + }); + + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: 'options', + }); + + useEffect(() => { + if (question) { + form.reset({ + content: question.content || '', + question: question.question || '', + required: question.required || false, + options: question.options || [{ id: '1', text: '' }], + }); + } + }, [question, form]); + + const handleSubmit = (data: any) => { + const config = { + ...question, + ...data, + }; + + // Remove options if not needed + if (![ + QuestionType.MULTIPLE_CHOICE, + QuestionType.SINGLE_CHOICE, + QuestionType.SELECT + ].includes(question.type)) { + delete config.options; + } + + // Remove content if not a title + if (question.type !== QuestionType.TITLE) { + delete config.content; + } + + onSave(config); + }; + + const renderFields = () => { + switch (question?.type) { + case QuestionType.TITLE: + return ( + ( + + Contenido del Título + + + + + + )} + /> + ); + + case QuestionType.SIMPLE: + return ( + ( + + Pregunta + + + + + + )} + /> + ); + + case QuestionType.MULTIPLE_CHOICE: + case QuestionType.SINGLE_CHOICE: + case QuestionType.SELECT: + return ( + <> + ( + + Pregunta + + + + + + )} + /> + +
+
+ Opciones + +
+ +
+ {fields.map((field, index) => ( +
+ ( + + + + + + + )} + /> + {fields.length > 1 && ( + + )} +
+ ))} +
+
+ + ); + } + }; + + return ( + + +
+ Configuración de la pregunta de la encuesta +
+ + Configurar Pregunta + + +
+ + {renderFields()} + + {question?.type !== QuestionType.TITLE && ( + ( + +
+ Respuesta Obligatoria +
+ + + +
+ )} + /> + )} + + + + + + + +
+
+ ); +} \ No newline at end of file diff --git a/apps/web/feactures/surveys/components/admin/question-toolbox.tsx b/apps/web/feactures/surveys/components/admin/question-toolbox.tsx new file mode 100644 index 0000000..7bd96ba --- /dev/null +++ b/apps/web/feactures/surveys/components/admin/question-toolbox.tsx @@ -0,0 +1,88 @@ +// Caja de herramientas con tipos de preguntas disponibles +// Funcionalidades: +// - Lista de elementos arrastrables +// - Tipos disponibles: Título, Pregunta Simple, Opción Múltiple, Opción Única, Selección +// - Cada elemento es arrastrable al área de construcción +'use client'; + +import { Card, CardContent } from '@repo/shadcn/card'; +import { QuestionType } from '../../schemas/survey'; +import { useDraggable } from '@dnd-kit/core'; +import { CSS } from '@dnd-kit/utilities'; + +const questionTypes = [ + { + type: QuestionType.TITLE, + label: 'Título', + icon: '📝', + }, + { + type: QuestionType.SIMPLE, + label: 'Pregunta Simple', + icon: '✏️', + }, + { + type: QuestionType.MULTIPLE_CHOICE, + label: 'Opción Múltiple', + icon: '☑️', + }, + { + type: QuestionType.SINGLE_CHOICE, + label: 'Opción Única', + icon: '⭕', + }, + { + type: QuestionType.SELECT, + label: 'Selección', + icon: '📋', + }, +]; + +function DraggableItem({ type, label, icon }: { type: string; label: string; icon: string }) { + const { attributes, listeners, setNodeRef, transform } = useDraggable({ + id: type, + data: { + type, + isTemplate: true, + }, + }); + + const style = transform ? { + transform: CSS.Translate.toString(transform), + } : undefined; + + return ( +
+
+ {icon} + {label} +
+
+ ); +} + +export function QuestionToolbox() { + return ( + + +

Elementos Disponibles

+
+ {questionTypes.map((item) => ( + + ))} +
+
+
+ ); +} \ No newline at end of file diff --git a/apps/web/feactures/surveys/components/admin/survey-builder.tsx b/apps/web/feactures/surveys/components/admin/survey-builder.tsx new file mode 100644 index 0000000..750968c --- /dev/null +++ b/apps/web/feactures/surveys/components/admin/survey-builder.tsx @@ -0,0 +1,454 @@ +// Componente principal para crear/editar encuestas +// Funcionalidades: +// - Formulario para datos básicos (título, descripción, fecha de cierre) +// - Sistema de drag & drop para agregar preguntas +// - Reordenamiento de preguntas existentes +// - Guardado como borrador o publicación directa'use client'; +'use client'; + +import { Button } from '@repo/shadcn/button'; +import { Card, CardContent } from '@repo/shadcn/card'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@repo/shadcn/form'; +import { Input } from '@repo/shadcn/input'; +import { Textarea } from '@repo/shadcn/textarea'; +import { CalendarIcon } from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { toast } from 'sonner'; +import { QuestionType, Survey } from '../../schemas/survey'; +import { QuestionConfigModal } from './question-config-modal'; +import { QuestionToolbox } from './question-toolbox'; +import { cn } from '@repo/shadcn/lib/utils'; +import { Calendar } from '@repo/shadcn/calendar'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@repo/shadcn/popover'; +import { format } from 'date-fns'; +import { DndContext, DragEndEvent, useSensor, useSensors, PointerSensor } from '@dnd-kit/core'; +import { SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import { useDroppable } from '@dnd-kit/core'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@repo/shadcn/select"; +import { useParams, useRouter } from 'next/navigation'; + +// Añade el import de Trash2 +import { Trash2 } from 'lucide-react'; +import { useSurveysByIdQuery } from '../../hooks/use-query-surveys'; +import { useSurveyMutation } from '../../hooks/use-mutation-surveys'; + + +function SortableQuestion({ + question, + index, + onDelete, + onEdit +}: { + question: any; + index: number; + onDelete: (id: string) => void; + onEdit: (question: any) => void; +}) { + const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ + id: question.id, + }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + + return ( +
+ + +
+
onEdit(question)} + > + {question.question || question.content} +
+ +
+
+
+
+ ); +} + +function DroppableArea({ children }: { children: React.ReactNode }) { + const { setNodeRef } = useDroppable({ + id: 'questions-container', + }); + + return ( +
+ {children} +
+ ); +} + +export function SurveyBuilder() { + const [questions, setQuestions] = useState([]); + const [selectedQuestion, setSelectedQuestion] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const params = useParams(); + const router = useRouter(); + const surveyId = params?.id as string; + const isEditing = Boolean(surveyId); + + + const form = useForm({ + defaultValues: { + title: '', + description: '', + closingDate: undefined as Date | undefined, + targetAudience: '', // Nuevo campo + }, + }); + + const { + mutate: MutateSurvey, + } = useSurveyMutation() + + + // Remove the loadSurvey function and use the query hook at component level + if (isEditing) { + const { data: surveyById, isLoading } = useSurveysByIdQuery(parseInt(surveyId)) + + // Use useEffect to handle the form reset when data is available + useEffect(() => { + // console.log(isEditing ? parseInt(surveyId) : 0); + if (surveyById?.data && !isLoading) { + form.reset({ + title: surveyById.data.title, + description: surveyById.data.description, + closingDate: surveyById.data.closingDate || undefined, + targetAudience: surveyById.data.targetAudience, + }); + // Fix: Set the questions directly without wrapping in array + setQuestions(surveyById.data.questions || []); + } + }, [surveyById, isLoading, form]); + } + + + // Remove the loadSurvey() call from the component body + + + + // Procesa la configuración de una pregunta después de cerrar el modal + // Actualiza o agrega la pregunta al listado + const handleQuestionConfig = (questionConfig: any) => { + if (selectedQuestion) { + const updatedQuestions = [...questions]; + const index = updatedQuestions.findIndex(q => q.id === selectedQuestion.id); + + if (index === -1) { + updatedQuestions.push({ + ...selectedQuestion, + ...questionConfig, + }); + } else { + updatedQuestions[index] = { + ...selectedQuestion, + ...questionConfig, + }; + } + + setQuestions(updatedQuestions); + } + setIsModalOpen(false); + }; + + // Maneja el guardado de la encuesta + // Valida campos requeridos y guarda como borrador o publicada + const handleSave = async (status: 'draft' | 'published') => { + const formData = form.getValues(); + + // validar que los campos no esten vacíos + if (!formData.title) return toast.error('El título es obligatorio') + if (!formData.description) return toast.error('La descripción es obligatorio') + if (!formData.targetAudience) return toast.error('El público objetivo es obligatorio') + if (!formData.closingDate) return toast.error('La fecha de cierre es obligatorio') + if (questions.length === 0) return toast.error('Debe agregar al menos una pregunta'); + + const surveyData: Omit = { + title: formData.title, + description: formData.description, + closingDate: formData.closingDate, + targetAudience: formData.targetAudience, + published: status === 'published', + questions: questions.map((q, index) => ({ ...q, position: index })), + }; + + try { + await MutateSurvey({ + ...surveyData, + id: isEditing ? parseInt(surveyId) : undefined, + }, { + onSuccess: () => { + toast.success( + isEditing + ? 'Encuesta actualizada exitosamente' + : status === 'published' + ? 'Encuesta publicada' + : 'Encuesta guardada como borrador' + ); + router.push('/dashboard/administracion/encuestas'); + }, + onError: (e) => { + toast.error(e.message) + } + }); + } catch (error) { + toast.error( `Error al ${isEditing ? 'actualizar' : 'guardar'} la encuesta`) + } + }; + + + // Configuración de los sensores para el drag and drop + // Define la distancia mínima para activar el arrastre + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + distance: 8, + }, + }) + ); + + // Manejador del evento cuando se termina de arrastrar un elemento + // Gestiona tanto nuevas preguntas como reordenamiento + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + + if (!over) return; + + if (active.data.current?.isTemplate) { + // Handle new question from toolbox + const questionType = active.data.current.type; + const newQuestion = { + id: `q-${questions.length + 1}`, + type: questionType as QuestionType, + position: questions.length, + required: false, + }; + setSelectedQuestion(newQuestion); + setIsModalOpen(true); + } else { + // Handle reordering of existing questions + const oldIndex = questions.findIndex(q => q.id === active.id); + const newIndex = questions.findIndex(q => q.id === over.id); + + if (oldIndex !== newIndex) { + const updatedQuestions = [...questions]; + const [movedQuestion] = updatedQuestions.splice(oldIndex, 1); + updatedQuestions.splice(newIndex, 0, movedQuestion); + setQuestions(updatedQuestions); + } + } + }; + + // Añade estas funciones manejadoras + const handleDeleteQuestion = (id: string) => { + setQuestions(questions.filter(q => q.id !== id)); + }; + + const handleEditQuestion = (question: any) => { + setSelectedQuestion(question); + setIsModalOpen(true); + }; + + return ( + +
+
+ +
+ +
+ + +
+ + ( + + Título de la Encuesta + + + + + + )} + /> + + ( + + Descripción + +