From 365cbd0d7a9ce3ddfa6aee4efb5cf3ec64f79182 Mon Sep 17 00:00:00 2001 From: Sergio Ramirez Date: Wed, 2 Jul 2025 15:10:54 -0400 Subject: [PATCH] Vista (intefaz y bd), esquema y endpoints de store agregados --- .../migrations/0003_icy_gertrude_yorkes.sql | 5 + .../migrations/meta/0003_snapshot.json | 1498 +++++++++++++++++ .../database/migrations/meta/_journal.json | 7 + apps/api/src/database/schema/inventory.ts | 19 +- .../inventory/entities/inventory.entity.ts | 52 +- .../inventory/inventory.controller.ts | 24 +- .../features/inventory/inventory.service.ts | 79 +- .../web/app/dashboard/productos/[id]/page.tsx | 33 + apps/web/app/dashboard/productos/page.tsx | 21 + .../feactures/inventory/actions/actions.ts | 72 +- .../inventory/create-product-form.tsx | 30 +- .../inventory/product-inventory-list.tsx | 2 +- .../inventory/update-product-form.tsx | 30 +- .../components/inventory/users-header.tsx | 2 +- .../components/products/product-list.tsx | 71 + ...e-query-users.ts => use-query-products.ts} | 6 +- .../feactures/inventory/schemas/inventory.ts | 31 + apps/web/lib/fetch.api.ts | 1 + .../src/components/ui/table/data-table.tsx | 2 +- 19 files changed, 1909 insertions(+), 76 deletions(-) create mode 100644 apps/api/src/database/migrations/0003_icy_gertrude_yorkes.sql create mode 100644 apps/api/src/database/migrations/meta/0003_snapshot.json create mode 100644 apps/web/app/dashboard/productos/[id]/page.tsx create mode 100644 apps/web/app/dashboard/productos/page.tsx create mode 100644 apps/web/feactures/inventory/components/products/product-list.tsx rename apps/web/feactures/inventory/hooks/{use-query-users.ts => use-query-products.ts} (51%) diff --git a/apps/api/src/database/migrations/0003_icy_gertrude_yorkes.sql b/apps/api/src/database/migrations/0003_icy_gertrude_yorkes.sql new file mode 100644 index 0000000..34910cc --- /dev/null +++ b/apps/api/src/database/migrations/0003_icy_gertrude_yorkes.sql @@ -0,0 +1,5 @@ +ALTER TABLE "products" ALTER COLUMN "user_id" SET NOT NULL;--> statement-breakpoint +CREATE VIEW "public"."v_product_store" AS ( + select p.id as product_id, p.title, p.description, p.price, p.stock, p.url_img, p.user_id, u.fullname + from products p + left join auth.users as u on u.id = p.user_id); \ No newline at end of file diff --git a/apps/api/src/database/migrations/meta/0003_snapshot.json b/apps/api/src/database/migrations/meta/0003_snapshot.json new file mode 100644 index 0000000..e1adda3 --- /dev/null +++ b/apps/api/src/database/migrations/meta/0003_snapshot.json @@ -0,0 +1,1498 @@ +{ + "id": "35c786b6-b0ae-4cf9-b748-9f1b51f17a10", + "prevId": "e3f08a0a-764c-4a4d-8473-c666398b8722", + "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": 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": {}, + "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_product_store": { + "columns": { + "product_id": { + "name": "product_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 + }, + "price": { + "name": "price", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "stock": { + "name": "stock", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "url_img": { + "name": "url_img", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "fullname": { + "name": "fullname", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "definition": "\n select p.id as product_id, p.title, p.description, p.price, p.stock, p.url_img, p.user_id, u.fullname\n from products p\n left join auth.users as u on u.id = p.user_id", + "name": "v_product_store", + "schema": "public", + "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 2ef8b9a..88a1ed8 100644 --- a/apps/api/src/database/migrations/meta/_journal.json +++ b/apps/api/src/database/migrations/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1750442271575, "tag": "0002_polite_franklin_richards", "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1751482400155, + "tag": "0003_icy_gertrude_yorkes", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/api/src/database/schema/inventory.ts b/apps/api/src/database/schema/inventory.ts index 9cd06e6..300d452 100644 --- a/apps/api/src/database/schema/inventory.ts +++ b/apps/api/src/database/schema/inventory.ts @@ -1,6 +1,7 @@ import * as t from 'drizzle-orm/pg-core'; import { timestamps } from '../timestamps'; import { users } from './auth'; +import { sql } from 'drizzle-orm'; export const products = t.pgTable( 'products', @@ -11,7 +12,21 @@ export const products = t.pgTable( price: t.numeric('price').notNull(), stock: t.integer('stock').notNull(), urlImg: t.text('url_img').notNull(), - userId: t.integer('user_id').references(() => users.id, { onDelete: 'cascade' }), + userId: t.integer('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(), ...timestamps, } -); \ No newline at end of file +); + +export const viewProductsStore = t.pgView('v_product_store', { + id: t.integer('product_id'), + title: t.text('title'), + description: t.text('description'), + price: t.numeric('price'), + stock: t.integer('stock'), + urlImg: t.text('url_img'), + userId: t.integer('user_id'), + fullname: t.text('fullname') +}).as(sql` + select p.id as product_id, p.title, p.description, p.price, p.stock, p.url_img, p.user_id, u.fullname + from products p + left join auth.users as u on u.id = p.user_id`); \ No newline at end of file diff --git a/apps/api/src/features/inventory/entities/inventory.entity.ts b/apps/api/src/features/inventory/entities/inventory.entity.ts index 3f511e8..cbd6420 100644 --- a/apps/api/src/features/inventory/entities/inventory.entity.ts +++ b/apps/api/src/features/inventory/entities/inventory.entity.ts @@ -1,19 +1,39 @@ export class Product { - id?: number; - title: string; - description: string; - price: string; - stock: number; - urlImg: string; - UserId?: number; + id?: number | null; + title: string | null; + description: string | null; + price: string | null; + stock: number | null; + urlImg: string | null; + userId?: number | null; } -export class CreateProduct { - id: number; - title: string; - description: string; - price: string; - stock: string; - urlImg: string; - UserId: number; -} \ No newline at end of file +export class Inventory { + id: number | null; + title: string | null; + description: string | null; + price: string | null; + stock: number | null; + urlImg: string | null; +} + +export class Store { + id: number | null; + title: string | null; + description: string | null; + price: string | null; + stock: number | null; + urlImg: string | null; + userId: number | null; + fullname?: string | null; +} + +// export class CreateProduct { +// id: number; +// title: string; +// description: string; +// price: string; +// stock: string; +// urlImg: string; +// UserId: number; +// } \ No newline at end of file diff --git a/apps/api/src/features/inventory/inventory.controller.ts b/apps/api/src/features/inventory/inventory.controller.ts index 6078262..80779c5 100644 --- a/apps/api/src/features/inventory/inventory.controller.ts +++ b/apps/api/src/features/inventory/inventory.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common'; +import { Controller, Get, Post, Body, Patch, Param, Delete, Query, Req } from '@nestjs/common'; import { InventoryService } from './inventory.service'; import { CreateProductDto } from './dto/create-product.dto'; import { UpdateProductDto } from './dto/update-product.dto'; @@ -6,12 +6,12 @@ import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; // import { Roles } from '../../common/decorators/roles.decorator'; import { PaginationDto } from '../../common/dto/pagination.dto'; -@ApiTags('inventory') -@Controller('inventory') +@ApiTags('products') +@Controller('products') export class UsersController { constructor(private readonly inventoryService: InventoryService) {} - @Get() + @Get('/store') // @Roles('admin') @ApiOperation({ summary: 'Get all products with pagination and filters' }) @ApiResponse({ status: 200, description: 'Return paginated products.' }) @@ -24,6 +24,22 @@ export class UsersController { }; } + @Get('/inventory') + // @Roles('admin') + @ApiOperation({ summary: 'Get all products with pagination and filters' }) + @ApiResponse({ status: 200, description: 'Return paginated products.' }) + async findAllByUserId(@Req() req: Request, @Query() paginationDto: PaginationDto) { + console.log(req['user'].id) + const id = 1 + // const id = Number(req['user'].id); + const result = await this.inventoryService.findAllByUserId(id,paginationDto); + return { + message: 'products fetched successfully', + data: result.data, + meta: result.meta + }; + } + @Get(':id') // @Roles('admin') @ApiOperation({ summary: 'Get a product by ID' }) diff --git a/apps/api/src/features/inventory/inventory.service.ts b/apps/api/src/features/inventory/inventory.service.ts index 732d41b..b0bcf9d 100644 --- a/apps/api/src/features/inventory/inventory.service.ts +++ b/apps/api/src/features/inventory/inventory.service.ts @@ -1,12 +1,12 @@ import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider'; -import { Inject, Injectable, HttpException, HttpStatus, NotFoundException, UnauthorizedException } from '@nestjs/common'; +import { Inject, Injectable, HttpException, HttpStatus } from '@nestjs/common'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import * as schema from 'src/database/index'; -import { products } from 'src/database/index'; +import { products, viewProductsStore } from 'src/database/index'; import { eq, like, or, SQL, sql, and, not } from 'drizzle-orm'; import { CreateProductDto } from './dto/create-product.dto'; import { UpdateProductDto } from './dto/update-product.dto'; -import { Product, CreateProduct } from './entities/inventory.entity'; +import { Product, Store, Inventory } from './entities/inventory.entity'; import { PaginationDto } from '../../common/dto/pagination.dto'; @Injectable() @@ -15,7 +15,7 @@ export class InventoryService { @Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase, ) { } - async findAll(paginationDto?: PaginationDto): Promise<{ data: Product[], meta: any }> { + async findAllByUserId(id: number, paginationDto?: PaginationDto): Promise<{ data: Inventory[], meta: any }> { const { page = 1, limit = 10, search = '', sortBy = 'id', sortOrder = 'asc' } = paginationDto || {}; // Calculate offset @@ -23,12 +23,14 @@ export class InventoryService { // Build search condition let searchCondition: SQL | undefined; + if (search) { - searchCondition = or( + searchCondition = and(or( like(products.title, `%${search}%`), like(products.description, `%${search}%`), - // like(users.fullname, `%${search}%`) - ); + ), eq(products.userId, id)); + }else{ + searchCondition = eq(products.userId, id); } // Build sort condition @@ -52,8 +54,8 @@ export class InventoryService { title: products.title, description: products.description, price: products.price, - urlImg: products.urlImg, stock: products.stock, + urlImg: products.urlImg }) .from(products) .where(searchCondition) @@ -75,6 +77,67 @@ export class InventoryService { return { data, meta }; } + async findAll(paginationDto?: PaginationDto): Promise<{ data: Store[], 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(viewProductsStore.title, `%${search}%`), + like(viewProductsStore.description, `%${search}%`), + ); + } + + // Build sort condition + const orderBy = sortOrder === 'asc' + ? sql`${viewProductsStore[sortBy as keyof typeof viewProductsStore]} asc` + : sql`${viewProductsStore[sortBy as keyof typeof viewProductsStore]} desc`; + + // Get total count for pagination + const totalCountResult = await this.drizzle + .select({ count: sql`count(*)` }) + .from(viewProductsStore) + .where(searchCondition); + + const totalCount = Number(totalCountResult[0].count); + const totalPages = Math.ceil(totalCount / limit); + + // Get paginated data + const data = await this.drizzle + .select({ + id: viewProductsStore.id, + title: viewProductsStore.title, + description: viewProductsStore.description, + price: viewProductsStore.price, + urlImg: viewProductsStore.urlImg, + stock: viewProductsStore.stock, + userId: viewProductsStore.userId, + fullname: viewProductsStore.fullname + }) + .from(viewProductsStore) + .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): Promise { const find = await this.drizzle .select({ diff --git a/apps/web/app/dashboard/productos/[id]/page.tsx b/apps/web/app/dashboard/productos/[id]/page.tsx new file mode 100644 index 0000000..fb4037e --- /dev/null +++ b/apps/web/app/dashboard/productos/[id]/page.tsx @@ -0,0 +1,33 @@ +import PageContainer from '@/components/layout/page-container'; +import { getSurveyByIdAction } from '@/feactures/surveys/actions/surveys-actions'; +import { SurveyResponse } from '@/feactures/surveys/components/survey-response'; + + + +export default async function SurveyResponsePage({ + params, +}: { + params: Promise<{ id: string }> +}) { + const { id } = await params; // You can still destructure id from params + + if (!id || id === '') { + // Handle the case where no id is provided + return null; + } + + // Call the function passing the dynamic id + const data = await getSurveyByIdAction(Number(id)); + + if (!data?.data) { + return
Encuesta no encontrada
; + } + + return ( + +
+ +
+
+ ); +} \ No newline at end of file diff --git a/apps/web/app/dashboard/productos/page.tsx b/apps/web/app/dashboard/productos/page.tsx new file mode 100644 index 0000000..03de14f --- /dev/null +++ b/apps/web/app/dashboard/productos/page.tsx @@ -0,0 +1,21 @@ +import PageContainer from '@/components/layout/page-container'; +import { SurveyList } from '@/feactures/inventory/components/products/product-list'; +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Productos - Fondemi', + description: 'Listado de productos disponibles', +}; + +export default function SurveysPage() { + + return ( + +
+

Productos Disponibles

+ +
+
+ + ); +} \ No newline at end of file diff --git a/apps/web/feactures/inventory/actions/actions.ts b/apps/web/feactures/inventory/actions/actions.ts index e5220dc..f9a0eb7 100644 --- a/apps/web/feactures/inventory/actions/actions.ts +++ b/apps/web/feactures/inventory/actions/actions.ts @@ -4,7 +4,8 @@ import { ApiResponseSchema, InventoryTable, productMutate, - editInventory + // editInventory, + productApiResponseSchema } from '../schemas/inventory'; import { auth } from '@/lib/auth'; @@ -27,8 +28,8 @@ export const getInventoryAction = async (params: { const [error, response] = await safeFetchApi( ApiResponseSchema, - `/inventory?${searchParams}`, - 'GET', + `/products/inventory?${searchParams}`, + 'GET' ); if (error) { @@ -53,6 +54,52 @@ export const getInventoryAction = async (params: { }; } +export const getAllProducts = 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 id = session?.user.id + + const [error, response] = await safeFetchApi( + productApiResponseSchema, + `/products/store?${searchParams}`, + 'GET' + ); + + if (error) { + console.error('Error:', error); + throw new Error(error.message); + } + + 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 createProductAction = async (payload: InventoryTable) => { const session = await auth() const userId = session?.user?.id @@ -62,14 +109,14 @@ export const createProductAction = async (payload: InventoryTable) => { const [error, data] = await safeFetchApi( productMutate, - '/inventory', + '/products', 'POST', payloadWithoutId, ); if (error) { console.error(error); - throw new Error('Error al crear el usuario'); + throw new Error('Error al crear el producto'); } return payloadWithoutId; @@ -81,16 +128,14 @@ export const updateUserAction = async (payload: InventoryTable) => { const [error, data] = await safeFetchApi( productMutate, - `/inventory/${id}`, + `/products/${id}`, 'PATCH', payloadWithoutId, ); - // console.log(data); if (error) { console.error(error); - - throw new Error(error?.message || 'Error al actualizar el usuario'); + throw new Error(error?.message || 'Error al actualizar el producto'); } return data; } catch (error) { @@ -98,17 +143,14 @@ export const updateUserAction = async (payload: InventoryTable) => { } } -export const deleteUserAction = async (id: Number) => { +export const deleteProductAction = async (id: Number) => { const [error] = await safeFetchApi( productMutate, - `/users/${id}`, + `/products/${id}`, 'DELETE' ) - console.log(error); - - - // if (error) throw new Error(error.message || 'Error al eliminar el usuario') + 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-product-form.tsx b/apps/web/feactures/inventory/components/inventory/create-product-form.tsx index b241d9c..98693c1 100644 --- a/apps/web/feactures/inventory/components/inventory/create-product-form.tsx +++ b/apps/web/feactures/inventory/components/inventory/create-product-form.tsx @@ -9,6 +9,7 @@ import { FormLabel, FormMessage, } from '@repo/shadcn/form'; +import { Textarea } from '@repo/shadcn/textarea'; import { Input } from '@repo/shadcn/input'; import { useForm } from 'react-hook-form'; import { useCreateUser } from "@/feactures/inventory/hooks/use-mutation"; @@ -83,20 +84,6 @@ export function CreateForm({ )} /> - ( - - Descripción - - - - - - )} - /> - + ( + + Descripción + + {/* */} +