diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index ac53a17..d9c59be 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -18,7 +18,7 @@ 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'; - +import {InventoryModule} from './features/inventory/inventory.module' @Module({ providers: [ @@ -58,7 +58,8 @@ import { SurveysModule } from './features/surveys/surveys.module'; UserRolesModule, ConfigurationsModule, SurveysModule, - LocationModule + LocationModule, + InventoryModule ], }) export class AppModule {} diff --git a/apps/api/src/database/migrations/0002_polite_franklin_richards.sql b/apps/api/src/database/migrations/0002_polite_franklin_richards.sql new file mode 100644 index 0000000..a4b57f2 --- /dev/null +++ b/apps/api/src/database/migrations/0002_polite_franklin_richards.sql @@ -0,0 +1,13 @@ +CREATE TABLE "products" ( + "id" serial PRIMARY KEY NOT NULL, + "title" text NOT NULL, + "description" text NOT NULL, + "price" numeric NOT NULL, + "stock" integer NOT NULL, + "url_img" text NOT NULL, + "user_id" integer, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp (3) +); +--> statement-breakpoint +ALTER TABLE "products" ADD CONSTRAINT "products_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/apps/api/src/database/migrations/meta/0002_snapshot.json b/apps/api/src/database/migrations/meta/0002_snapshot.json new file mode 100644 index 0000000..8424e82 --- /dev/null +++ b/apps/api/src/database/migrations/meta/0002_snapshot.json @@ -0,0 +1,1441 @@ +{ + "id": "e3f08a0a-764c-4a4d-8473-c666398b8722", + "prevId": "cdbb3495-688f-4b2d-ab8e-e62e42328fd5", + "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.products": { + "name": "products", + "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 + }, + "price": { + "name": "price", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "stock": { + "name": "stock", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "url_img": { + "name": "url_img", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_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": {}, + "foreignKeys": { + "products_user_id_users_id_fk": { + "name": "products_user_id_users_id_fk", + "tableFrom": "products", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "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 index 0f874c3..2ef8b9a 100644 --- a/apps/api/src/database/migrations/meta/_journal.json +++ b/apps/api/src/database/migrations/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1747665408016, "tag": "0001_massive_kylun", "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1750442271575, + "tag": "0002_polite_franklin_richards", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/api/src/features/inventory/inventory.service.ts b/apps/api/src/features/inventory/inventory.service.ts index ff1f222..115fcdc 100644 --- a/apps/api/src/features/inventory/inventory.service.ts +++ b/apps/api/src/features/inventory/inventory.service.ts @@ -1,13 +1,11 @@ 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 { products } from 'src/database/index'; import { eq, like, or, SQL, sql, and, not } from 'drizzle-orm'; -import * as bcrypt from 'bcryptjs'; import { CreateProductDto } from './dto/create-product.dto'; -import { UpdateUserDto } from './dto/update-product.dto'; +import { UpdateProductDto } from './dto/update-product.dto'; import { Product } from './entities/inventory.entity'; import { PaginationDto } from '../../common/dto/pagination.dto'; @@ -53,8 +51,9 @@ export class InventoryService { id: products.id, title: products.title, description: products.description, - valuePerUnit: products.valuePerUnit, + price: products.price, urlImg: products.urlImg, + stock: products.stock, // price: products.price, // quantity: products.quantity, // isActive: products.isActive @@ -79,7 +78,7 @@ export class InventoryService { previousPage: page > 1 ? page - 1 : null, }; - // console.log(data); + console.log(data); return { data, meta }; } @@ -90,8 +89,9 @@ export class InventoryService { id: products.id, title: products.title, description: products.description, - valuePerUnit: products.valuePerUnit, + price: products.price, urlImg: products.urlImg, + stock: products.stock }) .from(products) // .leftJoin(usersRole, eq(usersRole.userId, users.id)) @@ -124,8 +124,9 @@ export class InventoryService { .values({ title: createProductDto.title, description: createProductDto.description, - valuePerUnit: createProductDto.valuePerUnit, + price: createProductDto.price, urlImg: createProductDto.urlImg, + stock: createProductDto.stock }) .returning(); diff --git a/apps/web/app/dashboard/inventario/page.tsx b/apps/web/app/dashboard/inventario/page.tsx new file mode 100644 index 0000000..2534d48 --- /dev/null +++ b/apps/web/app/dashboard/inventario/page.tsx @@ -0,0 +1,37 @@ +import PageContainer from '@/components/layout/page-container'; +import UsersAdminList from '@/feactures/inventory/components/inventory/product-inventory-list'; +import { UsersHeader } from '@/feactures/inventory/components/inventory/users-header'; +import UsersTableAction from '@/feactures/inventory/components/inventory/product-tables/users-table-action'; +import { searchParamsCache, serialize } from '@/feactures/inventory/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/components/icons.tsx b/apps/web/components/icons.tsx index 74a63bb..082e650 100644 --- a/apps/web/components/icons.tsx +++ b/apps/web/components/icons.tsx @@ -1,6 +1,7 @@ import { AlertTriangle, ArrowRight, + Blocks, Check, ChevronLeft, ChevronRight, @@ -40,6 +41,7 @@ export type Icon = LucideIcon; export const Icons = { dashboard: LayoutDashboardIcon, + blocks: Blocks, logo: Command, login: LogIn, close: X, diff --git a/apps/web/constants/data.ts b/apps/web/constants/data.ts index 72ce81c..de2edfe 100644 --- a/apps/web/constants/data.ts +++ b/apps/web/constants/data.ts @@ -10,7 +10,14 @@ export const GeneralItems: NavItem[] = [ isActive: false, items: [], // No child items }, - + { + title: 'Inventario', + url: '/dashboard/inventario/', + icon: 'blocks', + shortcut: ['p', 'p'], + isActive: false, + items: [], // No child items + }, ]; diff --git a/apps/web/feactures/inventory/actions/actions.ts b/apps/web/feactures/inventory/actions/actions.ts new file mode 100644 index 0000000..ec0e850 --- /dev/null +++ b/apps/web/feactures/inventory/actions/actions.ts @@ -0,0 +1,153 @@ +'use server'; +import { safeFetchApi } from '@/lib/fetch.api'; +import { + surveysApiResponseSchema, + CreateUser, + productMutate, + UpdateUser +} from '../schemas/inventory'; + +import { auth } from '@/lib/auth'; + + +export const getProfileAction = async () => { + const session = await auth() + const id = session?.user?.id + + const [error, response] = await safeFetchApi( + productMutate, + `/users/${id}`, + 'GET' + ); + if (error) throw new Error(error.message); + return response; +}; + +export const updateProfileAction = async (payload: UpdateUser) => { + const { id, ...payloadWithoutId } = payload; + + const [error, data] = await safeFetchApi( + productMutate, + `/users/profile/${id}`, + 'PATCH', + payloadWithoutId, + ); + + console.log(payload); + if (error) { + if (error.message === 'Email already exists') { + throw new Error('Ese correo ya está en uso'); + } + // console.error('Error:', error); + throw new Error('Error al crear el usuario'); + } + return data; +}; + +export const getInventoryAction = 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, + `/inventory?${searchParams}`, + 'GET', + ); + + if (error) { + console.error(error); + + throw new Error(error.message); + } + + + // const transformedData = response?.data ? transformSurvey(response?.data) : undefined; + + return { + data: response?.data || [], + meta: response?.meta || { + page: 1, + limit: 10, + totalCount: 0, + totalPages: 1, + hasNextPage: false, + hasPreviousPage: false, + nextPage: null, + previousPage: null, + }, + }; +} + +export const createUserAction = async (payload: CreateUser) => { + const { id, confirmPassword, ...payloadWithoutId } = payload; + + const [error, data] = await safeFetchApi( + productMutate, + '/users', + 'POST', + payloadWithoutId, + ); + + if (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'); + } + // console.error('Error:', error); + throw new Error('Error al crear el usuario'); + } + + return payloadWithoutId; +}; + +export const updateUserAction = async (payload: UpdateUser) => { + try { + const { id, ...payloadWithoutId } = payload; + + const [error, data] = await safeFetchApi( + productMutate, + `/users/${id}`, + 'PATCH', + payloadWithoutId, + ); + + // console.log(data); + if (error) { + console.error(error); + + throw new Error(error?.message || 'Error al actualizar el usuario'); + } + return data; + } catch (error) { + console.error(error); + } +} + +export const deleteUserAction = async (id: Number) => { + const [error] = await safeFetchApi( + productMutate, + `/users/${id}`, + 'DELETE' + ) + + console.log(error); + + + // if (error) throw new Error(error.message || 'Error al eliminar el usuario') + + return true; +} \ No newline at end of file diff --git a/apps/web/feactures/inventory/components/inventory/create-user-form.tsx b/apps/web/feactures/inventory/components/inventory/create-user-form.tsx new file mode 100644 index 0000000..86cbc86 --- /dev/null +++ b/apps/web/feactures/inventory/components/inventory/create-user-form.tsx @@ -0,0 +1,222 @@ +'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 { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@repo/shadcn/select'; +import { useForm } from 'react-hook-form'; +import { useCreateUser } from "../../hooks/use-mutation-users"; +import { CreateUser, createUser } from '../../schemas/inventory'; + +const ROLES = { + // 1: 'Superadmin', + 2: 'Administrador', + 3: 'autoridad', + 4: 'Gerente', + 5: 'Usuario', + 6: 'Productor', + 7: 'Organización' +} + +interface CreateUserFormProps { + onSuccess?: () => void; + onCancel?: () => void; + defaultValues?: Partial; +} + +export function CreateUserForm({ + onSuccess, + onCancel, + defaultValues, +}: CreateUserFormProps) { + const { + mutate: saveAccountingAccounts, + isPending: isSaving, + isError, + } = useCreateUser(); + + // const { data: AccoutingAccounts } = useSurveyMutation(); + + const defaultformValues = { + username: defaultValues?.username || '', + fullname: defaultValues?.fullname || '', + email: defaultValues?.email || '', + password: '', + confirmPassword: '', + id: defaultValues?.id, + phone: defaultValues?.phone || '', + role: defaultValues?.role, + } + + const form = useForm({ + resolver: zodResolver(createUser), + defaultValues: defaultformValues, + mode: 'onChange', // Enable real-time validation + }); + + const onSubmit = async (data: CreateUser) => { + + const formData = data + + saveAccountingAccounts(formData, { + onSuccess: () => { + form.reset(); + onSuccess?.(); + }, + onError: (e) => { + form.setError('root', { + type: 'manual', + message: e.message, + }); + }, + }); + }; + + return ( +
+ + {form.formState.errors.root && ( +
+ {form.formState.errors.root.message} +
+ )} +
+ ( + + Usuario + + + + + + )} + /> + + ( + + Nombre completo + + + + + + )} + /> + + ( + + Correo + + + + + + )} + /> + + ( + + Teléfono + + + + + + )} + /> + + ( + + Contraseña + + + + + + )} + /> + + ( + + Confirmar Contraseña + + + + + + )} + /> + + ( + + Rol + + + + )} + /> +
+ +
+ + +
+
+ + ); +} diff --git a/apps/web/feactures/inventory/components/inventory/product-inventory-list.tsx b/apps/web/feactures/inventory/components/inventory/product-inventory-list.tsx new file mode 100644 index 0000000..3fcb913 --- /dev/null +++ b/apps/web/feactures/inventory/components/inventory/product-inventory-list.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { DataTable } from '@repo/shadcn/table/data-table'; +import { DataTableSkeleton } from '@repo/shadcn/table/data-table-skeleton'; +import { columns } from './product-tables/columns'; +import { useProductQuery } from '../../hooks/use-query-users'; + +interface SurveysAdminListProps { + initialPage: number; + initialSearch?: string | null; + initialLimit: number; + initialType?: string | null; +} + +export default function UsersAdminList({ + initialPage, + initialSearch, + initialLimit, + initialType, +}: SurveysAdminListProps) { + const filters = { + page: initialPage, + limit: initialLimit, + ...(initialSearch && { search: initialSearch }), + ...(initialType && { type: initialType }), + }; + + const {data, isLoading} = useProductQuery(filters) + + + // const {data, isLoading} = useUsersQuery(filters) + + console.log(data?.data); + + if (isLoading) { + return ; + } + + return ( + + ); +} diff --git a/apps/web/feactures/inventory/components/inventory/product-tables/cell-action.tsx b/apps/web/feactures/inventory/components/inventory/product-tables/cell-action.tsx new file mode 100644 index 0000000..53fac03 --- /dev/null +++ b/apps/web/feactures/inventory/components/inventory/product-tables/cell-action.tsx @@ -0,0 +1,90 @@ +'use client'; +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { AlertModal } from '@/components/modal/alert-modal'; +import { Button } from '@repo/shadcn/button'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@repo/shadcn/tooltip'; +import { Edit, Trash, User } from 'lucide-react'; +import { InventoryTable } from '@/feactures/inventory/schemas/inventory'; +import { useDeleteUser } from '@/feactures/users/hooks/use-mutation-users'; +import { AccountPlanModal } from '../user-modal'; + +interface CellActionProps { + data: InventoryTable; +} + +export const CellAction: React.FC = ({ data }) => { + const [loading, setLoading] = useState(false); + const [open, setOpen] = useState(false); + const [edit, setEdit] = useState(false); + const { mutate: deleteUser } = useDeleteUser(); + const router = useRouter(); + + const onConfirm = async () => { + try { + setLoading(true); + deleteUser(data.id!); + setOpen(false); + } catch (error) { + console.error('Error:', error); + } finally { + setLoading(false); + } + }; + + return ( + <> + setOpen(false)} + onConfirm={onConfirm} + loading={loading} + title="¿Estás seguro que desea deshabilitar este usuario?" + description="Esta acción no se puede deshacer." + /> + + + +
+ + + + + + +

Editar

+
+
+
+ + + + + + + +

Deshabilitar

+
+
+
+
+ + ); +}; diff --git a/apps/web/feactures/inventory/components/inventory/product-tables/columns.tsx b/apps/web/feactures/inventory/components/inventory/product-tables/columns.tsx new file mode 100644 index 0000000..a9b47bf --- /dev/null +++ b/apps/web/feactures/inventory/components/inventory/product-tables/columns.tsx @@ -0,0 +1,41 @@ +import { Badge } from "@repo/shadcn/badge"; + +import { ColumnDef } from '@tanstack/react-table'; +import { CellAction } from './cell-action'; +import { InventoryTable } from '../../../schemas/inventory'; + +export const columns: ColumnDef[] = [ + { + accessorKey: 'urlImg', + header: 'img', + cell: ({ row }) => { + const status = row.getValue("urlImg") as string | undefined; + return ( + Image + ) + }, + }, + { + accessorKey: 'title', + header: 'Producto', + }, + { + accessorKey: "description", + header: "Descripcion", + }, + { + accessorKey: 'price', + header: 'Precio', + cell: ({ row }) => `${row.original.price}$` + }, + { + accessorKey: 'stock', + header: 'Stock', + }, + + { + id: 'actions', + header: 'Acciones', + cell: ({ row }) => , + }, +]; diff --git a/apps/web/feactures/inventory/components/inventory/product-tables/use-survey-table-filters.tsx b/apps/web/feactures/inventory/components/inventory/product-tables/use-survey-table-filters.tsx new file mode 100644 index 0000000..65162d4 --- /dev/null +++ b/apps/web/feactures/inventory/components/inventory/product-tables/use-survey-table-filters.tsx @@ -0,0 +1,59 @@ +'use client'; + +import { PUBLISHED_TYPES } from '@/feactures/surveys/schemas/surveys-options'; +import { searchParams } from '@repo/shadcn/lib/searchparams'; +import { useQueryState } from 'nuqs'; +import { useCallback, useMemo } from 'react'; + + +export const TYPE_OPTIONS = Object.entries(PUBLISHED_TYPES).map( + ([value, label]) => ({ + value, + label, + }), +); + + +export function useSurveyTableFilters() { + const [searchQuery, setSearchQuery] = useQueryState( + 'q', + searchParams.q + .withOptions({ + shallow: false, + throttleMs: 500, // Add 500ms delay + // Removed dedupingInterval as it's not a valid option + }) + .withDefault(''), + ); + + const [typeFilter, setTypeFilter] = useQueryState( + 'published', + searchParams.q.withOptions({ shallow: false }).withDefault(''), + ); + + const [page, setPage] = useQueryState( + 'page', + searchParams.page.withDefault(1), + ); + + const resetFilters = useCallback(() => { + setSearchQuery(null); + setTypeFilter(null); + setPage(1); + }, [setSearchQuery, setPage]); + + const isAnyFilterActive = useMemo(() => { + return !!searchQuery || !!typeFilter; + }, [searchQuery]); + + return { + searchQuery, + setSearchQuery, + page, + setPage, + resetFilters, + isAnyFilterActive, + typeFilter, + setTypeFilter + }; +} diff --git a/apps/web/feactures/inventory/components/inventory/product-tables/users-table-action.tsx b/apps/web/feactures/inventory/components/inventory/product-tables/users-table-action.tsx new file mode 100644 index 0000000..bc714d0 --- /dev/null +++ b/apps/web/feactures/inventory/components/inventory/product-tables/users-table-action.tsx @@ -0,0 +1,36 @@ +'use client'; + +import { DataTableFilterBox } from '@repo/shadcn/table/data-table-filter-box'; +import { DataTableSearch } from '@repo/shadcn/table/data-table-search'; +import { + TYPE_OPTIONS, + useSurveyTableFilters, +} from './use-survey-table-filters'; + +export default function UserTableAction() { + const { + typeFilter, + searchQuery, + setPage, + setTypeFilter, + setSearchQuery, + } = useSurveyTableFilters(); + + return ( +
+ + {/* */} +
+ ); +} diff --git a/apps/web/feactures/inventory/components/inventory/update-user-form.tsx b/apps/web/feactures/inventory/components/inventory/update-user-form.tsx new file mode 100644 index 0000000..b96d14f --- /dev/null +++ b/apps/web/feactures/inventory/components/inventory/update-user-form.tsx @@ -0,0 +1,227 @@ +'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 { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@repo/shadcn/select'; +import { useForm } from 'react-hook-form'; +import { useUpdateUser } from "@/feactures/users/hooks/use-mutation-users"; +import { UpdateUser, updateUser } from '@/feactures/users/schemas/users'; + +const ROLES = { + // 1: 'Superadmin', + 2: 'Administrador', + 3: 'autoridad', + 4: 'Gerente', + 5: 'Usuario', + 6: 'Productor', + 7: 'Organización' +} + +interface UserFormProps { + onSuccess?: () => void; + onCancel?: () => void; + defaultValues?: Partial; +} + +export function UpdateUserForm({ + onSuccess, + onCancel, + defaultValues, +}: UserFormProps) { + const { + mutate: saveAccountingAccounts, + isPending: isSaving, + isError, + } = useUpdateUser(); + + const defaultformValues = { + username: defaultValues?.username || '', + fullname: defaultValues?.fullname || '', + email: defaultValues?.email || '', + password: '', + id: defaultValues?.id, + phone: defaultValues?.phone || '', + role: undefined, + isActive: defaultValues?.isActive + } + + // console.log(defaultValues); + + const form = useForm({ + resolver: zodResolver(updateUser), + defaultValues: defaultformValues, + mode: 'onChange', // Enable real-time validation + }); + + const onSubmit = async (data: UpdateUser) => { + + const formData = data + + saveAccountingAccounts(formData, { + onSuccess: () => { + form.reset(); + onSuccess?.(); + }, + onError: () => { + form.setError('root', { + type: 'manual', + message: 'Error al guardar la cuenta contable', + }); + }, + }); + }; + + return ( +
+ + {form.formState.errors.root && ( +
+ {form.formState.errors.root.message} +
+ )} +
+ ( + + Usuario + + + + + + )} + /> + + ( + + Nombre completo + + + + + + )} + /> + + ( + + Correo + + + + + + )} + /> + + ( + + Teléfono + + + + + + )} + /> + + ( + + Nueva Contraseña + + + + + + )} + /> + + ( + + Rol + + + + )} + /> + + ( + + Estatus + + + + )} + /> +
+ +
+ + +
+
+ + ); +} diff --git a/apps/web/feactures/inventory/components/inventory/user-modal.tsx b/apps/web/feactures/inventory/components/inventory/user-modal.tsx new file mode 100644 index 0000000..b4230f8 --- /dev/null +++ b/apps/web/feactures/inventory/components/inventory/user-modal.tsx @@ -0,0 +1,69 @@ +'use client'; + +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@repo/shadcn/dialog'; +import { AccountPlan } from '@/feactures/users/schemas/account-plan.schema'; +import { CreateUserForm } from './create-user-form'; +import { UpdateUserForm } from './update-user-form'; + +interface AccountPlanModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + defaultValues?: Partial; +} + +export function AccountPlanModal({ + open, + onOpenChange, + defaultValues, +}: AccountPlanModalProps) { + const handleSuccess = () => { + onOpenChange(false); + }; + + const handleCancel = () => { + onOpenChange(false); + }; + + return ( + { + if (!open) { + onOpenChange(false); + } + }} + > + + + + {defaultValues?.id + ? 'Actualizar usuario' + : 'Crear usuario'} + + + Complete los campos para {defaultValues?.id ? 'actualizar' : 'crear'} un usuario + + + {defaultValues?.id ? ( + + ): ( + + )} + + + ); +} diff --git a/apps/web/feactures/inventory/components/inventory/users-header.tsx b/apps/web/feactures/inventory/components/inventory/users-header.tsx new file mode 100644 index 0000000..891c9dc --- /dev/null +++ b/apps/web/feactures/inventory/components/inventory/users-header.tsx @@ -0,0 +1,27 @@ +'use client'; +import { useRouter } from 'next/navigation'; +import { Button } from '@repo/shadcn/button'; +import { Heading } from '@repo/shadcn/heading'; +import { Plus } from 'lucide-react'; +import { useState } from 'react'; +import { AccountPlanModal } from './user-modal'; + +export function UsersHeader() { + const [open, setOpen] = useState(false); + // const router = useRouter(); + return ( + <> +
+ + +
+ + + + ); +} \ No newline at end of file diff --git a/apps/web/feactures/inventory/components/modal-profile.tsx b/apps/web/feactures/inventory/components/modal-profile.tsx new file mode 100644 index 0000000..4f1853a --- /dev/null +++ b/apps/web/feactures/inventory/components/modal-profile.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@repo/shadcn/dialog'; +import { AccountPlan } from '@/feactures/users/schemas/account-plan.schema'; +import { ModalForm } from './update-user-form'; + +interface AccountPlanModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + defaultValues?: Partial; +} + +export function AccountPlanModal({ + open, + onOpenChange, + defaultValues, +}: AccountPlanModalProps) { + const handleSuccess = () => { + onOpenChange(false); + }; + + const handleCancel = () => { + onOpenChange(false); + }; + + return ( + { + if (!open) { + onOpenChange(false); + } + }} + > + + + Actualizar Perfil + + Complete los campos para actualizar sus datos.
Los campos vacios no seran actualizados. +
+
+ + +
+
+ ); +} diff --git a/apps/web/feactures/inventory/components/selectList.tsx b/apps/web/feactures/inventory/components/selectList.tsx new file mode 100644 index 0000000..4150cd5 --- /dev/null +++ b/apps/web/feactures/inventory/components/selectList.tsx @@ -0,0 +1,85 @@ +// 'use client'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@repo/shadcn/select'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@repo/shadcn/form'; +import { UpdateUser, updateUser } from '@/feactures/users/schemas/users'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; + +interface SelectListProps { + label: string + // values: + values: Array + form: any + name: string + handleChange: any +} + +export function SelectList({ label, values, form, name, handleChange }: SelectListProps) { + // const { label, values, form, name } = props; + // handleChange + + // const defaultformValues = { + // username: '', + // fullname: '', + // email: '', + // password: '', + // id: 0, + // phone: '', + // role: undefined, + // isActive: false + // } + + // const form = useForm({ + // resolver: zodResolver(updateUser), + // defaultValues: defaultformValues, + // mode: 'onChange', // Enable real-time validation + // }); + + + return ( + + {label} + + + + )} + /> +} \ No newline at end of file diff --git a/apps/web/feactures/inventory/components/survey.tsx b/apps/web/feactures/inventory/components/survey.tsx new file mode 100644 index 0000000..b4b5eb9 --- /dev/null +++ b/apps/web/feactures/inventory/components/survey.tsx @@ -0,0 +1,28 @@ +'use client' + +import { SurveyResponse } from '@/feactures/surveys/components/survey-response'; +import { useSurveysByIdQuery } from '@/feactures/surveys/hooks/use-query-surveys'; + +import { notFound, useParams } from 'next/navigation'; + + +export default function SurveyPage() { + const params = useParams(); + const surveyId = params?.id as string | undefined; + + + if (!surveyId || surveyId === '') { + notFound(); + } + + const { data: survey, isLoading } = useSurveysByIdQuery(Number(surveyId)); + console.log('🎯 useSurveysByIdQuery ejecutado, data:', survey, 'isLoading:', isLoading); + + if (!survey?.data || !survey?.data.published) { + notFound(); + } + + return ( + + ); +} \ No newline at end of file diff --git a/apps/web/feactures/inventory/components/update-user-form.tsx b/apps/web/feactures/inventory/components/update-user-form.tsx new file mode 100644 index 0000000..6398864 --- /dev/null +++ b/apps/web/feactures/inventory/components/update-user-form.tsx @@ -0,0 +1,268 @@ +'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 { +// Select, +// SelectContent, +// SelectItem, +// SelectTrigger, +// SelectValue, +// } from '@repo/shadcn/select'; +import { SelectSearchable } from '@repo/shadcn/select-searchable' +import { useForm } from 'react-hook-form'; +import { useUpdateProfile } from "@/feactures/users/hooks/use-mutation-users"; +import { UpdateUser, updateUser } from '@/feactures/users/schemas/users'; +import { toast } from 'sonner'; + +import React from 'react'; +import { useStateQuery, useMunicipalityQuery, useParishQuery } from '@/feactures/location/hooks/use-query-location'; + +interface UserFormProps { + onSuccess?: () => void; + onCancel?: () => void; + defaultValues?: Partial; +} + +export function ModalForm({ + onSuccess, + onCancel, + defaultValues, +}: UserFormProps) { + const { + mutate: saveAccountingAccounts, + isPending: isSaving, + isError, + } = useUpdateProfile(); + + 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 = Array.isArray(dataMunicipality?.data) && dataMunicipality.data.length > 0 + ? dataMunicipality.data + : [{id:0,stateId:0,name:'Sin Municipios'}] + // const parishOptions = dataParish?.data || [{id:0,municipalityId:0,name:'Sin Parroquias'}] + const parishOptions = Array.isArray(dataParish?.data) && dataParish.data.length > 0 + ? dataParish.data + : [{id:0,stateId:0,name:'Sin Parroquias'}] + + + const defaultformValues = { + username: defaultValues?.username || '', + fullname: defaultValues?.fullname || '', + email: defaultValues?.email || '', + password: '', + id: defaultValues?.id, + phone: defaultValues?.phone || '', + role: undefined, + isActive: defaultValues?.isActive, + state: defaultValues?.state, + municipality: defaultValues?.municipality, + parish: defaultValues?.parish + } + + + + // console.log(defaultValues); + + const form = useForm({ + resolver: zodResolver(updateUser), + defaultValues: defaultformValues, + mode: 'onChange', // Enable real-time validation + }); + + const onSubmit = async (data: UpdateUser) => { + + const formData = data + + saveAccountingAccounts(formData, { + onSuccess: () => { + form.reset(); + onSuccess?.(); + toast.success('Actualizado exitosamente!'); + }, + onError: (e) => { + form.setError('root', { + type: 'manual', + message: e.message, + }); + // toast.error(e.message); + }, + }); + }; + + return ( +
+ + {form.formState.errors.root && ( +
+ {form.formState.errors.root.message} +
+ )} +
+ ( + + Nombre completo + + + + + + )} + /> + + ( + + Correo + + + + + + )} + /> + + ( + + Teléfono + + + + + + )} + /> + + ( + + 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} + /> + + + )} + /> + + ( + + Nueva Contraseña + + + + + + )} + /> +
+ +
+ + +
+
+ + ); +} diff --git a/apps/web/feactures/inventory/components/user-profile.tsx b/apps/web/feactures/inventory/components/user-profile.tsx new file mode 100644 index 0000000..72c621c --- /dev/null +++ b/apps/web/feactures/inventory/components/user-profile.tsx @@ -0,0 +1,75 @@ +'use client'; +import { useUserByProfile } from '@/feactures/users/hooks/use-query-users'; +import { Button } from '@repo/shadcn/button'; +import { Edit, Edit2 } from 'lucide-react'; +import { useState } from 'react'; +import { AccountPlanModal } from './modal-profile'; + +export function Profile() { + const [open, setOpen] = useState(false); + + const { data } = useUserByProfile(); + + // console.log("🎯 data:", data); + + return ( +
+ + + + +

Datos del usuario

+
+
+

Usuario:

+

{data?.data.username || 'Sin Nombre de Usuario'}

+
+ +
+

Rol:

+

{data?.data.role || 'Sin Rol'}

+
+
+ +

Información personal

+
+
+

Nombre completo:

+

{data?.data.fullname || 'Sin nombre y apellido'}

+
+ +
+

Correo:

+

{data?.data.email || 'Sin correo'}

+
+ +
+

Teléfono:

+

{data?.data.phone || 'Sin teléfono'}

+
+
+ +

Información de ubicación

+
+
+

Estado:

+

{data?.data.state || 'Sin Estado'}

+
+ +
+

Municipio:

+

{data?.data.municipality || 'Sin Municipio'}

+
+ +
+

Parroquia:

+

{data?.data.parish || 'Sin Parroquia'}

+
+
+ +
+ ); +} + diff --git a/apps/web/feactures/inventory/hooks/use-mutation-users.ts b/apps/web/feactures/inventory/hooks/use-mutation-users.ts new file mode 100644 index 0000000..dbbf733 --- /dev/null +++ b/apps/web/feactures/inventory/hooks/use-mutation-users.ts @@ -0,0 +1,45 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { CreateUser, UpdateUser } from "../schemas/inventory"; +import { updateUserAction, createUserAction, deleteUserAction, updateProfileAction } from "../actions/actions"; + +// Create mutation +export function useCreateUser() { + const queryClient = useQueryClient(); + const mutation = useMutation({ + mutationFn: (data: CreateUser) => createUserAction(data), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }), + // onError: (e) => console.error('Error:', e), + }) + return mutation +} + +// Update mutation +export function useUpdateUser() { + const queryClient = useQueryClient(); + const mutation = useMutation({ + mutationFn: (data: UpdateUser) => updateUserAction(data), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }), + onError: (e) => console.error('Error:', e) + }) + return mutation; +} + +export function useUpdateProfile() { + const queryClient = useQueryClient(); + const mutation = useMutation({ + mutationFn: (data: UpdateUser) => updateProfileAction(data), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }), + // onError: (e) => console.error('Error:', e) + }) + return mutation; +} + +// Delete mutation +export function useDeleteUser() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: number) => deleteUserAction(id), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }), + onError: (e) => console.error('Error:', e) + }) +} \ No newline at end of file diff --git a/apps/web/feactures/inventory/hooks/use-query-surveys.ts b/apps/web/feactures/inventory/hooks/use-query-surveys.ts new file mode 100644 index 0000000..6610191 --- /dev/null +++ b/apps/web/feactures/inventory/hooks/use-query-surveys.ts @@ -0,0 +1,12 @@ +'use client' +import { useSafeQuery } from "@/hooks/use-safe-query"; +import { getUsersAction,getProfileAction} from "../actions/actions"; + +// Hook for users +export function useUsersQuery(params = {}) { + return useSafeQuery(['users',params], () => getUsersAction(params)) +} + +export function useUserByProfile() { + return useSafeQuery(['users'], () => getProfileAction()) +} diff --git a/apps/web/feactures/inventory/hooks/use-query-users.ts b/apps/web/feactures/inventory/hooks/use-query-users.ts new file mode 100644 index 0000000..2572cfd --- /dev/null +++ b/apps/web/feactures/inventory/hooks/use-query-users.ts @@ -0,0 +1,8 @@ +'use client' +import { useSafeQuery } from "@/hooks/use-safe-query"; +import { getInventoryAction} from "../actions/actions"; + +// Hook for users +export function useProductQuery(params = {}) { + return useSafeQuery(['product',params], () => getInventoryAction(params)) +} \ No newline at end of file diff --git a/apps/web/feactures/inventory/schemas/account-plan-options.ts b/apps/web/feactures/inventory/schemas/account-plan-options.ts new file mode 100644 index 0000000..b4d997d --- /dev/null +++ b/apps/web/feactures/inventory/schemas/account-plan-options.ts @@ -0,0 +1,19 @@ +export const ACCOUNT_TYPES = { + activo: 'Activo', + pasivo: 'Pasivo', + patrimonio: 'Patrimonio', + ingreso: 'Ingreso', + gasto: 'Gasto', + costo: 'Costo', + cuenta_orden: 'Cuenta de Orden', +} as const; + +export const ACCOUNT_LEVELS = { + 1: 'Nivel 1 - Cuenta Principal', + 2: 'Nivel 2 - Subcuenta', + 3: 'Nivel 3 - Cuenta Detallada', + 4: 'Nivel 4 - Cuenta Auxiliar', +} as const; + +export type AccountType = keyof typeof ACCOUNT_TYPES; +export type AccountLevel = keyof typeof ACCOUNT_LEVELS; \ No newline at end of file diff --git a/apps/web/feactures/inventory/schemas/account-plan.schema.ts b/apps/web/feactures/inventory/schemas/account-plan.schema.ts new file mode 100644 index 0000000..8edc268 --- /dev/null +++ b/apps/web/feactures/inventory/schemas/account-plan.schema.ts @@ -0,0 +1,83 @@ +import { z } from 'zod'; + +export const accountPlanSchema = z + .object({ + id: z.number().optional(), + savingBankId: z.number(), + code: z + .string() + .min(1, 'El código es requerido') + .max(50, 'El código no puede tener más de 50 caracteres') + .regex(/^[\d.]+$/, 'El código debe contener solo números y puntos'), + name: z + .string() + .min(1, 'El nombre es requerido') + .max(100, 'El nombre no puede tener más de 100 caracteres'), + type: z.enum( + [ + 'activo', + 'pasivo', + 'patrimonio', + 'ingreso', + 'gasto', + 'costo', + 'cuenta_orden', + ], + { + required_error: 'El tipo de cuenta es requerido', + invalid_type_error: 'Tipo de cuenta inválido', + }, + ), + description: z.string().optional().nullable(), + level: z + .number() + .min(1, 'El nivel debe ser mayor a 0') + .max(4, 'El nivel no puede ser mayor a 4'), + parent_account_id: z.number().nullable(), + created_at: z.string().optional(), + updated_at: z.string().optional(), + }) + .refine( + (data) => { + if (data.level > 1 && !data.parent_account_id) { + return false; + } + return true; + }, + { + message: 'Las cuentas de nivel superior a 1 requieren una cuenta padre', + path: ['parent_account_id'], + }, + ); + +export type AccountPlan = z.infer; + +// Response schemas for the API +export const accountPlanResponseSchema = z.object({ + message: z.string(), + data: accountPlanSchema, +}); + +export const accountPlanDeleteResponseSchema = z.object({ + message: z.string(), +}); + +export const accountPlanListResponseSchema = z.object({ + message: z.string(), + data: z.array(accountPlanSchema), +}); + +export const accountPlanPaginationResponseSchema = z.object({ + message: z.string(), + data: z.array(accountPlanSchema), + 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(), + }), +}); diff --git a/apps/web/feactures/inventory/schemas/inventory.ts b/apps/web/feactures/inventory/schemas/inventory.ts new file mode 100644 index 0000000..640dd85 --- /dev/null +++ b/apps/web/feactures/inventory/schemas/inventory.ts @@ -0,0 +1,69 @@ +import { title } from 'process'; +import { z } from 'zod'; + +export type InventoryTable = z.infer; +export type CreateUser = z.infer; +export type UpdateUser = z.infer; + +export const product = z.object({ + id: z.number().optional(), + title: z.string(), + description: z.string(), + // price: z.number(), + // quantity: z.number(), + // category: z.string(), + // image: z.string().optional(), + stock: z.number(), + price: z.string(), + urlImg: z.string(), + userId: z.number().optional(), +}); + +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(product), + 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 productMutate = z.object({ + message: z.string(), + data: product, +}) \ No newline at end of file diff --git a/apps/web/feactures/inventory/schemas/surveys-options.ts b/apps/web/feactures/inventory/schemas/surveys-options.ts new file mode 100644 index 0000000..06c4c94 --- /dev/null +++ b/apps/web/feactures/inventory/schemas/surveys-options.ts @@ -0,0 +1,6 @@ +export const PUBLISHED_TYPES = { + published: 'Publicada', + draft: 'Borrador', +} as const; + +export type PublishedType = keyof typeof PUBLISHED_TYPES; diff --git a/apps/web/feactures/inventory/utils/date-utils.ts b/apps/web/feactures/inventory/utils/date-utils.ts new file mode 100644 index 0000000..ec39a0e --- /dev/null +++ b/apps/web/feactures/inventory/utils/date-utils.ts @@ -0,0 +1,11 @@ +export function formatDate(date: Date): string { + return new Intl.DateTimeFormat('es-ES', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }).format(date); +} + + diff --git a/apps/web/feactures/inventory/utils/searchparams.ts b/apps/web/feactures/inventory/utils/searchparams.ts new file mode 100644 index 0000000..2d79317 --- /dev/null +++ b/apps/web/feactures/inventory/utils/searchparams.ts @@ -0,0 +1,16 @@ +import { + createSearchParamsCache, + createSerializer, + parseAsInteger, + parseAsString, +} from 'nuqs/server'; + +export const searchParams = { + page: parseAsInteger.withDefault(1), + limit: parseAsInteger.withDefault(10), + q: parseAsString, + type: parseAsString, +}; + +export const searchParamsCache = createSearchParamsCache(searchParams); +export const serialize = createSerializer(searchParams); diff --git a/apps/web/public/apple.avif b/apps/web/public/apple.avif new file mode 100644 index 0000000..60b2ba0 Binary files /dev/null and b/apps/web/public/apple.avif differ