From d363662548150c6e9c417b847ec5e6272df31cd1 Mon Sep 17 00:00:00 2001 From: Felipe Coutinho Date: Sun, 21 Jun 2026 11:54:05 -0300 Subject: [PATCH] =?UTF-8?q?feat(anota=C3=A7=C3=B5es):=20permite=20anexos?= =?UTF-8?q?=20em=20notas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- drizzle/0031_lame_cerise.sql | 9 + drizzle/meta/0031_snapshot.json | 2988 +++++++++++++++++ drizzle/meta/_journal.json | 7 + src/app/(dashboard)/notes/page.tsx | 12 +- src/db/schema.ts | 37 +- src/features/dashboard/notes/notes-mappers.ts | 1 + src/features/notes/actions.ts | 80 +- src/features/notes/actions/attachments.ts | 279 ++ .../components/note-attachments-field.tsx | 345 ++ src/features/notes/components/note-card.tsx | 19 +- .../notes/components/note-details-dialog.tsx | 17 +- src/features/notes/components/note-dialog.tsx | 112 +- src/features/notes/components/notes-page.tsx | 9 +- src/features/notes/components/types.ts | 8 + src/features/notes/queries.ts | 78 +- .../transactions/lib/attachments-config.ts | 20 +- src/shared/lib/attachments/config.ts | 11 + 17 files changed, 3942 insertions(+), 90 deletions(-) create mode 100644 drizzle/0031_lame_cerise.sql create mode 100644 drizzle/meta/0031_snapshot.json create mode 100644 src/features/notes/actions/attachments.ts create mode 100644 src/features/notes/components/note-attachments-field.tsx create mode 100644 src/shared/lib/attachments/config.ts diff --git a/drizzle/0031_lame_cerise.sql b/drizzle/0031_lame_cerise.sql new file mode 100644 index 0000000..e47ea5c --- /dev/null +++ b/drizzle/0031_lame_cerise.sql @@ -0,0 +1,9 @@ +CREATE TABLE "anotacao_anexos" ( + "anotacao_id" uuid NOT NULL, + "anexo_id" uuid NOT NULL, + CONSTRAINT "anotacao_anexos_anotacao_id_anexo_id_pk" PRIMARY KEY("anotacao_id","anexo_id") +); +--> statement-breakpoint +ALTER TABLE "anotacao_anexos" ADD CONSTRAINT "anotacao_anexos_anotacao_id_anotacoes_id_fk" FOREIGN KEY ("anotacao_id") REFERENCES "public"."anotacoes"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "anotacao_anexos" ADD CONSTRAINT "anotacao_anexos_anexo_id_anexos_id_fk" FOREIGN KEY ("anexo_id") REFERENCES "public"."anexos"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "anotacao_anexos_anexo_id_idx" ON "anotacao_anexos" USING btree ("anexo_id"); \ No newline at end of file diff --git a/drizzle/meta/0031_snapshot.json b/drizzle/meta/0031_snapshot.json new file mode 100644 index 0000000..35b5b0a --- /dev/null +++ b/drizzle/meta/0031_snapshot.json @@ -0,0 +1,2988 @@ +{ + "id": "8df17863-7825-4ae3-8181-d3646e092240", + "prevId": "ae034355-a8bc-474f-a5ff-451b38697828", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "accountId": { + "name": "accountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerId": { + "name": "providerId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idToken": { + "name": "idToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessTokenExpiresAt": { + "name": "accessTokenExpiresAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refreshTokenExpiresAt": { + "name": "refreshTokenExpiresAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tokens_api": { + "name": "tokens_api", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_used_ip": { + "name": "last_used_ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tokens_api_token_hash_idx": { + "name": "tokens_api_token_hash_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tokens_api_user_id_user_id_fk": { + "name": "tokens_api_user_id_user_id_fk", + "tableFrom": "tokens_api", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.anexos": { + "name": "anexos", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chave_arquivo": { + "name": "chave_arquivo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nome_arquivo": { + "name": "nome_arquivo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tamanho_bytes": { + "name": "tamanho_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "anexos_user_id_idx": { + "name": "anexos_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "anexos_user_id_user_id_fk": { + "name": "anexos_user_id_user_id_fk", + "tableFrom": "anexos", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "anexos_chave_arquivo_unique": { + "name": "anexos_chave_arquivo_unique", + "nullsNotDistinct": false, + "columns": ["chave_arquivo"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.orcamentos": { + "name": "orcamentos", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "valor": { + "name": "valor", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "periodo": { + "name": "periodo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "categoria_id": { + "name": "categoria_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "orcamentos_user_id_period_idx": { + "name": "orcamentos_user_id_period_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "periodo", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "orcamentos_user_id_categoria_id_periodo_key": { + "name": "orcamentos_user_id_categoria_id_periodo_key", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "categoria_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "periodo", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "orcamentos_categoria_id_idx": { + "name": "orcamentos_categoria_id_idx", + "columns": [ + { + "expression": "categoria_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "orcamentos_user_id_user_id_fk": { + "name": "orcamentos_user_id_user_id_fk", + "tableFrom": "orcamentos", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "orcamentos_categoria_id_categorias_id_fk": { + "name": "orcamentos_categoria_id_categorias_id_fk", + "tableFrom": "orcamentos", + "tableTo": "categorias", + "columnsFrom": ["categoria_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cartoes": { + "name": "cartoes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "nome": { + "name": "nome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dt_fechamento": { + "name": "dt_fechamento", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dt_vencimento": { + "name": "dt_vencimento", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anotacao": { + "name": "anotacao", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "limite": { + "name": "limite", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "bandeira": { + "name": "bandeira", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conta_id": { + "name": "conta_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "cartoes_conta_id_idx": { + "name": "cartoes_conta_id_idx", + "columns": [ + { + "expression": "conta_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cartoes_user_id_user_id_fk": { + "name": "cartoes_user_id_user_id_fk", + "tableFrom": "cartoes", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cartoes_conta_id_contas_id_fk": { + "name": "cartoes_conta_id_contas_id_fk", + "tableFrom": "cartoes", + "tableTo": "contas", + "columnsFrom": ["conta_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.categorias": { + "name": "categorias", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "nome": { + "name": "nome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tipo": { + "name": "tipo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "icone": { + "name": "icone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "categorias_user_id_type_idx": { + "name": "categorias_user_id_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tipo", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "categorias_user_id_user_id_fk": { + "name": "categorias_user_id_user_id_fk", + "tableFrom": "categorias", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_notification_states": { + "name": "dashboard_notification_states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notification_key": { + "name": "notification_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "read_at": { + "name": "read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "dashboard_notification_states_user_id_key_unique": { + "name": "dashboard_notification_states_user_id_key_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "notification_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dashboard_notification_states_user_id_user_id_fk": { + "name": "dashboard_notification_states_user_id_user_id_fk", + "tableFrom": "dashboard_notification_states", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.establishment_logos": { + "name": "establishment_logos", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_key": { + "name": "name_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "establishment_logos_user_id_user_id_fk": { + "name": "establishment_logos_user_id_user_id_fk", + "tableFrom": "establishment_logos", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "establishment_logos_user_id_name_key_pk": { + "name": "establishment_logos_user_id_name_key_pk", + "columns": ["user_id", "name_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contas": { + "name": "contas", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "nome": { + "name": "nome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tipo_conta": { + "name": "tipo_conta", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anotacao": { + "name": "anotacao", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "saldo_inicial": { + "name": "saldo_inicial", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "excluir_do_saldo": { + "name": "excluir_do_saldo", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "excluir_saldo_inicial_receitas": { + "name": "excluir_saldo_inicial_receitas", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "contas_user_id_user_id_fk": { + "name": "contas_user_id_user_id_fk", + "tableFrom": "contas", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.import_category_mappings": { + "name": "import_category_mappings", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description_key": { + "name": "description_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category_id": { + "name": "category_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "import_category_mappings_category_id_idx": { + "name": "import_category_mappings_category_id_idx", + "columns": [ + { + "expression": "category_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "import_category_mappings_user_id_user_id_fk": { + "name": "import_category_mappings_user_id_user_id_fk", + "tableFrom": "import_category_mappings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "import_category_mappings_category_id_categorias_id_fk": { + "name": "import_category_mappings_category_id_categorias_id_fk", + "tableFrom": "import_category_mappings", + "tableTo": "categorias", + "columnsFrom": ["category_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "import_category_mappings_user_id_description_key_pk": { + "name": "import_category_mappings_user_id_description_key_pk", + "columns": ["user_id", "description_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pre_lancamentos": { + "name": "pre_lancamentos", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_app": { + "name": "source_app", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_app_name": { + "name": "source_app_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "original_title": { + "name": "original_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "original_text": { + "name": "original_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notification_timestamp": { + "name": "notification_timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "parsed_name": { + "name": "parsed_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parsed_amount": { + "name": "parsed_amount", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "lancamento_id": { + "name": "lancamento_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "discarded_at": { + "name": "discarded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pre_lancamentos_user_id_status_idx": { + "name": "pre_lancamentos_user_id_status_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pre_lancamentos_user_id_created_at_idx": { + "name": "pre_lancamentos_user_id_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pre_lancamentos_lancamento_id_idx": { + "name": "pre_lancamentos_lancamento_id_idx", + "columns": [ + { + "expression": "lancamento_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pre_lancamentos_user_id_user_id_fk": { + "name": "pre_lancamentos_user_id_user_id_fk", + "tableFrom": "pre_lancamentos", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pre_lancamentos_lancamento_id_lancamentos_id_fk": { + "name": "pre_lancamentos_lancamento_id_lancamentos_id_fk", + "tableFrom": "pre_lancamentos", + "tableTo": "lancamentos", + "columnsFrom": ["lancamento_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.antecipacoes_parcelas": { + "name": "antecipacoes_parcelas", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "series_id": { + "name": "series_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "periodo_antecipacao": { + "name": "periodo_antecipacao", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data_antecipacao": { + "name": "data_antecipacao", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "parcelas_antecipadas": { + "name": "parcelas_antecipadas", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "valor_total": { + "name": "valor_total", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": true + }, + "qtde_parcelas": { + "name": "qtde_parcelas", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "desconto": { + "name": "desconto", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "lancamento_id": { + "name": "lancamento_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pagador_id": { + "name": "pagador_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "categoria_id": { + "name": "categoria_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "anotacao": { + "name": "anotacao", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "antecipacoes_parcelas_user_id_idx": { + "name": "antecipacoes_parcelas_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "antecipacoes_parcelas_lancamento_id_idx": { + "name": "antecipacoes_parcelas_lancamento_id_idx", + "columns": [ + { + "expression": "lancamento_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "antecipacoes_parcelas_pagador_id_idx": { + "name": "antecipacoes_parcelas_pagador_id_idx", + "columns": [ + { + "expression": "pagador_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "antecipacoes_parcelas_categoria_id_idx": { + "name": "antecipacoes_parcelas_categoria_id_idx", + "columns": [ + { + "expression": "categoria_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "antecipacoes_parcelas_lancamento_id_lancamentos_id_fk": { + "name": "antecipacoes_parcelas_lancamento_id_lancamentos_id_fk", + "tableFrom": "antecipacoes_parcelas", + "tableTo": "lancamentos", + "columnsFrom": ["lancamento_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "antecipacoes_parcelas_pagador_id_pagadores_id_fk": { + "name": "antecipacoes_parcelas_pagador_id_pagadores_id_fk", + "tableFrom": "antecipacoes_parcelas", + "tableTo": "pagadores", + "columnsFrom": ["pagador_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "antecipacoes_parcelas_categoria_id_categorias_id_fk": { + "name": "antecipacoes_parcelas_categoria_id_categorias_id_fk", + "tableFrom": "antecipacoes_parcelas", + "tableTo": "categorias", + "columnsFrom": ["categoria_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "antecipacoes_parcelas_user_id_user_id_fk": { + "name": "antecipacoes_parcelas_user_id_user_id_fk", + "tableFrom": "antecipacoes_parcelas", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.faturas": { + "name": "faturas", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "status_pagamento": { + "name": "status_pagamento", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "periodo": { + "name": "periodo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cartao_id": { + "name": "cartao_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "faturas_user_id_period_idx": { + "name": "faturas_user_id_period_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "periodo", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "faturas_cartao_id_period_idx": { + "name": "faturas_cartao_id_period_idx", + "columns": [ + { + "expression": "cartao_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "periodo", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "faturas_user_id_cartao_id_periodo_key": { + "name": "faturas_user_id_cartao_id_periodo_key", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cartao_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "periodo", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "faturas_user_id_user_id_fk": { + "name": "faturas_user_id_user_id_fk", + "tableFrom": "faturas", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "faturas_cartao_id_cartoes_id_fk": { + "name": "faturas_cartao_id_cartoes_id_fk", + "tableFrom": "faturas", + "tableTo": "cartoes", + "columnsFrom": ["cartao_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.anotacao_anexos": { + "name": "anotacao_anexos", + "schema": "", + "columns": { + "anotacao_id": { + "name": "anotacao_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "anexo_id": { + "name": "anexo_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "anotacao_anexos_anexo_id_idx": { + "name": "anotacao_anexos_anexo_id_idx", + "columns": [ + { + "expression": "anexo_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "anotacao_anexos_anotacao_id_anotacoes_id_fk": { + "name": "anotacao_anexos_anotacao_id_anotacoes_id_fk", + "tableFrom": "anotacao_anexos", + "tableTo": "anotacoes", + "columnsFrom": ["anotacao_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "anotacao_anexos_anexo_id_anexos_id_fk": { + "name": "anotacao_anexos_anexo_id_anexos_id_fk", + "tableFrom": "anotacao_anexos", + "tableTo": "anexos", + "columnsFrom": ["anexo_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "anotacao_anexos_anotacao_id_anexo_id_pk": { + "name": "anotacao_anexos_anotacao_id_anexo_id_pk", + "columns": ["anotacao_id", "anexo_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.anotacoes": { + "name": "anotacoes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "titulo": { + "name": "titulo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "descricao": { + "name": "descricao", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tipo": { + "name": "tipo", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'nota'" + }, + "tasks": { + "name": "tasks", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "arquivada": { + "name": "arquivada", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "anotacoes_user_id_idx": { + "name": "anotacoes_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "anotacoes_user_id_user_id_fk": { + "name": "anotacoes_user_id_user_id_fk", + "tableFrom": "anotacoes", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkey": { + "name": "passkey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "publicKey": { + "name": "publicKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credentialID": { + "name": "credentialID", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deviceType": { + "name": "deviceType", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backedUp": { + "name": "backedUp", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "passkey_user_id_idx": { + "name": "passkey_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "passkey_userId_user_id_fk": { + "name": "passkey_userId_user_id_fk", + "tableFrom": "passkey", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.compartilhamentos_pagador": { + "name": "compartilhamentos_pagador", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "pagador_id": { + "name": "pagador_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "shared_with_user_id": { + "name": "shared_with_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'read'" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "compartilhamentos_pagador_unique": { + "name": "compartilhamentos_pagador_unique", + "columns": [ + { + "expression": "pagador_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "shared_with_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "compartilhamentos_pagador_shared_with_user_id_idx": { + "name": "compartilhamentos_pagador_shared_with_user_id_idx", + "columns": [ + { + "expression": "shared_with_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "compartilhamentos_pagador_created_by_user_id_idx": { + "name": "compartilhamentos_pagador_created_by_user_id_idx", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "compartilhamentos_pagador_pagador_id_pagadores_id_fk": { + "name": "compartilhamentos_pagador_pagador_id_pagadores_id_fk", + "tableFrom": "compartilhamentos_pagador", + "tableTo": "pagadores", + "columnsFrom": ["pagador_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "compartilhamentos_pagador_shared_with_user_id_user_id_fk": { + "name": "compartilhamentos_pagador_shared_with_user_id_user_id_fk", + "tableFrom": "compartilhamentos_pagador", + "tableTo": "user", + "columnsFrom": ["shared_with_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "compartilhamentos_pagador_created_by_user_id_user_id_fk": { + "name": "compartilhamentos_pagador_created_by_user_id_user_id_fk", + "tableFrom": "compartilhamentos_pagador", + "tableTo": "user", + "columnsFrom": ["created_by_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pagadores": { + "name": "pagadores", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "nome": { + "name": "nome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anotacao": { + "name": "anotacao", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_auto_send": { + "name": "is_auto_send", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "share_code": { + "name": "share_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_mail": { + "name": "last_mail", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "pagadores_share_code_key": { + "name": "pagadores_share_code_key", + "columns": [ + { + "expression": "share_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pagadores_user_id_user_id_fk": { + "name": "pagadores_user_id_user_id_fk", + "tableFrom": "pagadores", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.insights_salvos": { + "name": "insights_salvos", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period": { + "name": "period", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "insights_salvos_user_period_idx": { + "name": "insights_salvos_user_period_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "insights_salvos_user_id_user_id_fk": { + "name": "insights_salvos_user_id_user_id_fk", + "tableFrom": "insights_salvos", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ipAddress": { + "name": "ipAddress", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userAgent": { + "name": "userAgent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lancamento_anexos": { + "name": "lancamento_anexos", + "schema": "", + "columns": { + "lancamento_id": { + "name": "lancamento_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "anexo_id": { + "name": "anexo_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "lancamento_anexos_anexo_id_idx": { + "name": "lancamento_anexos_anexo_id_idx", + "columns": [ + { + "expression": "anexo_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "lancamento_anexos_lancamento_id_lancamentos_id_fk": { + "name": "lancamento_anexos_lancamento_id_lancamentos_id_fk", + "tableFrom": "lancamento_anexos", + "tableTo": "lancamentos", + "columnsFrom": ["lancamento_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "lancamento_anexos_anexo_id_anexos_id_fk": { + "name": "lancamento_anexos_anexo_id_anexos_id_fk", + "tableFrom": "lancamento_anexos", + "tableTo": "anexos", + "columnsFrom": ["anexo_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "lancamento_anexos_lancamento_id_anexo_id_pk": { + "name": "lancamento_anexos_lancamento_id_anexo_id_pk", + "columns": ["lancamento_id", "anexo_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lancamentos": { + "name": "lancamentos", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "condicao": { + "name": "condicao", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nome": { + "name": "nome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "forma_pagamento": { + "name": "forma_pagamento", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anotacao": { + "name": "anotacao", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "valor": { + "name": "valor", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": true + }, + "data_compra": { + "name": "data_compra", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "tipo_transacao": { + "name": "tipo_transacao", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "qtde_parcela": { + "name": "qtde_parcela", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "periodo": { + "name": "periodo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parcela_atual": { + "name": "parcela_atual", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "qtde_recorrencia": { + "name": "qtde_recorrencia", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "data_vencimento": { + "name": "data_vencimento", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "dt_pagamento_boleto": { + "name": "dt_pagamento_boleto", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "realizado": { + "name": "realizado", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "dividido": { + "name": "dividido", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "antecipado": { + "name": "antecipado", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "antecipacao_id": { + "name": "antecipacao_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cartao_id": { + "name": "cartao_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "conta_id": { + "name": "conta_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "categoria_id": { + "name": "categoria_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "pagador_id": { + "name": "pagador_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "series_id": { + "name": "series_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "split_group_id": { + "name": "split_group_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "transfer_id": { + "name": "transfer_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "ofx_fit_id": { + "name": "ofx_fit_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "import_batch_id": { + "name": "import_batch_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "lancamentos_user_id_period_idx": { + "name": "lancamentos_user_id_period_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "periodo", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "lancamentos_user_id_period_type_idx": { + "name": "lancamentos_user_id_period_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "periodo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tipo_transacao", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "lancamentos_pagador_id_period_idx": { + "name": "lancamentos_pagador_id_period_idx", + "columns": [ + { + "expression": "pagador_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "periodo", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "lancamentos_user_id_pagador_id_period_idx": { + "name": "lancamentos_user_id_pagador_id_period_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pagador_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "periodo", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "lancamentos_user_id_purchase_date_idx": { + "name": "lancamentos_user_id_purchase_date_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "data_compra", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "lancamentos_series_id_idx": { + "name": "lancamentos_series_id_idx", + "columns": [ + { + "expression": "series_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "lancamentos_user_id_split_group_id_idx": { + "name": "lancamentos_user_id_split_group_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "split_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "lancamentos_transfer_id_idx": { + "name": "lancamentos_transfer_id_idx", + "columns": [ + { + "expression": "transfer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "lancamentos_user_id_condition_idx": { + "name": "lancamentos_user_id_condition_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "condicao", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "lancamentos_cartao_id_period_idx": { + "name": "lancamentos_cartao_id_period_idx", + "columns": [ + { + "expression": "cartao_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "periodo", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "lancamentos_conta_id_idx": { + "name": "lancamentos_conta_id_idx", + "columns": [ + { + "expression": "conta_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "lancamentos_categoria_id_idx": { + "name": "lancamentos_categoria_id_idx", + "columns": [ + { + "expression": "categoria_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "lancamentos_antecipacao_id_idx": { + "name": "lancamentos_antecipacao_id_idx", + "columns": [ + { + "expression": "antecipacao_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "lancamentos_ofx_fit_id_user_id_idx": { + "name": "lancamentos_ofx_fit_id_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ofx_fit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "ofx_fit_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "lancamentos_antecipacao_id_antecipacoes_parcelas_id_fk": { + "name": "lancamentos_antecipacao_id_antecipacoes_parcelas_id_fk", + "tableFrom": "lancamentos", + "tableTo": "antecipacoes_parcelas", + "columnsFrom": ["antecipacao_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "lancamentos_user_id_user_id_fk": { + "name": "lancamentos_user_id_user_id_fk", + "tableFrom": "lancamentos", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "lancamentos_cartao_id_cartoes_id_fk": { + "name": "lancamentos_cartao_id_cartoes_id_fk", + "tableFrom": "lancamentos", + "tableTo": "cartoes", + "columnsFrom": ["cartao_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "lancamentos_conta_id_contas_id_fk": { + "name": "lancamentos_conta_id_contas_id_fk", + "tableFrom": "lancamentos", + "tableTo": "contas", + "columnsFrom": ["conta_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "lancamentos_categoria_id_categorias_id_fk": { + "name": "lancamentos_categoria_id_categorias_id_fk", + "tableFrom": "lancamentos", + "tableTo": "categorias", + "columnsFrom": ["categoria_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "lancamentos_pagador_id_pagadores_id_fk": { + "name": "lancamentos_pagador_id_pagadores_id_fk", + "tableFrom": "lancamentos", + "tableTo": "pagadores", + "columnsFrom": ["pagador_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.preferencias_usuario": { + "name": "preferencias_usuario", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "extrato_note_as_column": { + "name": "extrato_note_as_column", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "lancamentos_column_order": { + "name": "lancamentos_column_order", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "attachment_max_size_mb": { + "name": "attachment_max_size_mb", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 50 + }, + "mostrar_resumo_lancamento": { + "name": "mostrar_resumo_lancamento", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "dashboard_widgets": { + "name": "dashboard_widgets", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "preferencias_usuario_user_id_user_id_fk": { + "name": "preferencias_usuario_user_id_user_id_fk", + "tableFrom": "preferencias_usuario", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "preferencias_usuario_user_id_unique": { + "name": "preferencias_usuario_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index b298429..ccfd391 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -211,6 +211,13 @@ "when": 1780150535055, "tag": "0030_complete_umar", "breakpoints": true + }, + { + "idx": 31, + "version": "7", + "when": 1782051007412, + "tag": "0031_lame_cerise", + "breakpoints": true } ] } diff --git a/src/app/(dashboard)/notes/page.tsx b/src/app/(dashboard)/notes/page.tsx index 7e98206..e3e84ca 100644 --- a/src/app/(dashboard)/notes/page.tsx +++ b/src/app/(dashboard)/notes/page.tsx @@ -1,16 +1,24 @@ import { connection } from "next/server"; import { NotesPage } from "@/features/notes/components/notes-page"; import { fetchAllNotesForUser } from "@/features/notes/queries"; +import { fetchUserPreferences } from "@/features/settings/queries"; import { getUserId } from "@/shared/lib/auth/server"; export default async function Page() { await connection(); const userId = await getUserId(); - const { activeNotes, archivedNotes } = await fetchAllNotesForUser(userId); + const [{ activeNotes, archivedNotes }, preferences] = await Promise.all([ + fetchAllNotesForUser(userId), + fetchUserPreferences(userId), + ]); return (
- +
); } diff --git a/src/db/schema.ts b/src/db/schema.ts index f29f735..50ca232 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -847,11 +847,12 @@ export const budgetsRelations = relations(budgets, ({ one }) => ({ }), })); -export const notesRelations = relations(notes, ({ one }) => ({ +export const notesRelations = relations(notes, ({ one, many }) => ({ user: one(user, { fields: [notes.userId], references: [user.id], }), + noteAttachments: many(noteAttachments), })); export const savedInsightsRelations = relations(savedInsights, ({ one }) => ({ @@ -972,6 +973,24 @@ export const transactionAttachments = pgTable( }), ); +export const noteAttachments = pgTable( + "anotacao_anexos", + { + noteId: uuid("anotacao_id") + .notNull() + .references(() => notes.id, { onDelete: "cascade" }), + attachmentId: uuid("anexo_id") + .notNull() + .references(() => attachments.id, { onDelete: "cascade" }), + }, + (table) => ({ + pk: primaryKey({ columns: [table.noteId, table.attachmentId] }), + attachmentIdIdx: index("anotacao_anexos_anexo_id_idx").on( + table.attachmentId, + ), + }), +); + export const importCategoryMappings = pgTable( "import_category_mappings", { @@ -1044,6 +1063,7 @@ export const attachmentsRelations = relations(attachments, ({ one, many }) => ({ references: [user.id], }), transactionAttachments: many(transactionAttachments), + noteAttachments: many(noteAttachments), })); export const transactionAttachmentsRelations = relations( @@ -1060,8 +1080,23 @@ export const transactionAttachmentsRelations = relations( }), ); +export const noteAttachmentsRelations = relations( + noteAttachments, + ({ one }) => ({ + note: one(notes, { + fields: [noteAttachments.noteId], + references: [notes.id], + }), + attachment: one(attachments, { + fields: [noteAttachments.attachmentId], + references: [attachments.id], + }), + }), +); + export type Attachment = typeof attachments.$inferSelect; export type TransactionAttachment = typeof transactionAttachments.$inferSelect; +export type NoteAttachment = typeof noteAttachments.$inferSelect; export const establishmentLogosRelations = relations( establishmentLogos, diff --git a/src/features/dashboard/notes/notes-mappers.ts b/src/features/dashboard/notes/notes-mappers.ts index d743920..b548f96 100644 --- a/src/features/dashboard/notes/notes-mappers.ts +++ b/src/features/dashboard/notes/notes-mappers.ts @@ -9,6 +9,7 @@ const mapDashboardNoteToNote = (note: DashboardNote): Note => ({ tasks: note.tasks, archived: note.archived, createdAt: note.createdAt, + attachments: [], }); export const mapDashboardNotesToNotes = (notes: DashboardNote[]) => diff --git a/src/features/notes/actions.ts b/src/features/notes/actions.ts index 9aef54b..61e1aa1 100644 --- a/src/features/notes/actions.ts +++ b/src/features/notes/actions.ts @@ -1,8 +1,8 @@ "use server"; -import { and, eq } from "drizzle-orm"; +import { and, eq, inArray } from "drizzle-orm"; import { z } from "zod"; -import { notes } from "@/db/schema"; +import { attachments, noteAttachments, notes } from "@/db/schema"; import { handleActionError, revalidateForEntity, @@ -10,6 +10,7 @@ import { import { getUser } from "@/shared/lib/auth/server"; import { db } from "@/shared/lib/db"; import { uuidSchema } from "@/shared/lib/schemas/common"; +import { deleteS3Object } from "@/shared/lib/storage/presign"; import type { ActionResult } from "@/shared/lib/types/actions"; const taskSchema = z.object({ @@ -70,25 +71,42 @@ type NoteDeleteInput = z.infer; export async function createNoteAction( input: NoteCreateInput, -): Promise { +): Promise> { try { const user = await getUser(); const data = createNoteSchema.parse(input); - await db.insert(notes).values({ - title: data.title, - description: data.description, - type: data.type, - tasks: - data.tasks && data.tasks.length > 0 ? JSON.stringify(data.tasks) : null, - userId: user.id, - }); + const [created] = await db + .insert(notes) + .values({ + title: data.title, + description: data.description, + type: data.type, + tasks: + data.tasks && data.tasks.length > 0 + ? JSON.stringify(data.tasks) + : null, + userId: user.id, + }) + .returning({ id: notes.id }); + + if (!created) { + return { success: false, error: "Não foi possível criar a anotação." }; + } revalidateForEntity("notes", user.id); - return { success: true, message: "Anotação criada com sucesso." }; + return { + success: true, + message: "Anotação criada com sucesso.", + data: { noteId: created.id }, + }; } catch (error) { - return handleActionError(error); + const result = handleActionError(error); + return { + success: false, + error: result.success ? "Ocorreu um erro inesperado." : result.error, + }; } } @@ -135,6 +153,25 @@ export async function deleteNoteAction( const user = await getUser(); const data = deleteNoteSchema.parse(input); + const linkedAttachments = await db + .select({ id: attachments.id, fileKey: attachments.fileKey }) + .from(noteAttachments) + .innerJoin( + attachments, + and( + eq(noteAttachments.attachmentId, attachments.id), + eq(attachments.userId, user.id), + ), + ) + .innerJoin( + notes, + and( + eq(noteAttachments.noteId, notes.id), + eq(notes.id, data.id), + eq(notes.userId, user.id), + ), + ); + const [deleted] = await db .delete(notes) .where(and(eq(notes.id, data.id), eq(notes.userId, user.id))) @@ -147,6 +184,23 @@ export async function deleteNoteAction( }; } + if (linkedAttachments.length > 0) { + await Promise.all( + linkedAttachments.map((attachment) => + deleteS3Object(attachment.fileKey), + ), + ); + await db.delete(attachments).where( + and( + eq(attachments.userId, user.id), + inArray( + attachments.id, + linkedAttachments.map((attachment) => attachment.id), + ), + ), + ); + } + revalidateForEntity("notes", user.id); return { success: true, message: "Anotação removida com sucesso." }; diff --git a/src/features/notes/actions/attachments.ts b/src/features/notes/actions/attachments.ts new file mode 100644 index 0000000..75a7e5a --- /dev/null +++ b/src/features/notes/actions/attachments.ts @@ -0,0 +1,279 @@ +"use server"; + +import crypto, { randomUUID } from "node:crypto"; +import { and, eq } from "drizzle-orm"; +import { z } from "zod/v4"; +import { + attachments, + noteAttachments, + notes, + userPreferences, +} from "@/db/schema"; +import { + handleActionError, + revalidateForEntity, +} from "@/shared/lib/actions/helpers"; +import { + ALLOWED_MIME_TYPES, + ATTACHMENT_SIZE_OPTIONS, +} from "@/shared/lib/attachments/config"; +import { getUser } from "@/shared/lib/auth/server"; +import { db } from "@/shared/lib/db"; +import { + createPresignedPutUrl, + deleteS3Object, + headS3Object, +} from "@/shared/lib/storage/presign"; +import type { ActionResult } from "@/shared/lib/types/actions"; + +const UPLOAD_TOKEN_EXPIRY_SECONDS = 10 * 60; +const MAX_NOTE_FILE_SIZE = Math.max(...ATTACHMENT_SIZE_OPTIONS) * 1024 * 1024; + +const presignSchema = z.object({ + noteId: z.string().uuid(), + fileName: z.string().min(1).max(255), + mimeType: z.enum(ALLOWED_MIME_TYPES), + fileSize: z.number().positive().max(MAX_NOTE_FILE_SIZE), +}); + +const tokenPayloadSchema = presignSchema.extend({ + userId: z.string().min(1), + fileKey: z.string().min(1), + exp: z.number().int(), +}); + +type UploadTokenPayload = z.infer; + +type PresignResult = + | { success: true; presignedUrl: string; uploadToken: string } + | { success: false; error: string }; + +export type NoteAttachmentData = { + attachmentId: string; + fileName: string; + fileSize: number; + mimeType: string; +}; + +function getUploadTokenSecret(): string { + const secret = process.env.BETTER_AUTH_SECRET; + if (!secret) throw new Error("BETTER_AUTH_SECRET is required."); + return secret; +} + +function encode(value: string): string { + return Buffer.from(value).toString("base64url"); +} + +function signUploadToken(payload: UploadTokenPayload): string { + const encoded = encode(JSON.stringify(payload)); + const signature = crypto + .createHmac("sha256", getUploadTokenSecret()) + .update(encoded) + .digest("base64url"); + return `${encoded}.${signature}`; +} + +function verifyUploadToken(token: string): UploadTokenPayload | null { + try { + const [encoded, signature] = token.split("."); + if (!encoded || !signature) return null; + const expected = crypto + .createHmac("sha256", getUploadTokenSecret()) + .update(encoded) + .digest("base64url"); + if ( + signature.length !== expected.length || + !crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)) + ) { + return null; + } + const parsed = tokenPayloadSchema.safeParse( + JSON.parse(Buffer.from(encoded, "base64url").toString("utf8")), + ); + if (!parsed.success || parsed.data.exp < Math.floor(Date.now() / 1000)) { + return null; + } + if (!parsed.data.fileKey.startsWith(`${parsed.data.userId}/`)) return null; + return parsed.data; + } catch { + return null; + } +} + +async function findOwnedNote(noteId: string, userId: string) { + const [note] = await db + .select({ id: notes.id, type: notes.type }) + .from(notes) + .where(and(eq(notes.id, noteId), eq(notes.userId, userId))); + return note?.type === "nota" ? note : null; +} + +async function getAttachmentLimitBytes(userId: string): Promise { + const [preferences] = await db + .select({ maxSizeMb: userPreferences.attachmentMaxSizeMb }) + .from(userPreferences) + .where(eq(userPreferences.userId, userId)); + return (preferences?.maxSizeMb ?? 50) * 1024 * 1024; +} + +export async function getPresignedNoteAttachmentUploadUrlAction(input: { + noteId: string; + fileName: string; + mimeType: string; + fileSize: number; +}): Promise { + try { + const user = await getUser(); + const data = presignSchema.parse(input); + if (data.fileSize > (await getAttachmentLimitBytes(user.id))) { + return { + success: false, + error: "O arquivo excede o limite configurado para anexos.", + }; + } + if (!(await findOwnedNote(data.noteId, user.id))) { + return { success: false, error: "Nota não encontrada." }; + } + + const extensions: Record<(typeof ALLOWED_MIME_TYPES)[number], string> = { + "application/pdf": "pdf", + "image/jpeg": "jpg", + "image/png": "png", + "image/webp": "webp", + }; + const extension = extensions[data.mimeType]; + const fileKey = `${user.id}/${randomUUID()}.${extension}`; + const presignedUrl = await createPresignedPutUrl(fileKey, data.mimeType); + const uploadToken = signUploadToken({ + ...data, + userId: user.id, + fileKey, + exp: Math.floor(Date.now() / 1000) + UPLOAD_TOKEN_EXPIRY_SECONDS, + }); + return { success: true, presignedUrl, uploadToken }; + } catch (error) { + const result = handleActionError(error); + return { + success: false, + error: result.success ? "Algo deu errado." : result.error, + }; + } +} + +export async function confirmNoteAttachmentUploadAction(input: { + uploadToken: string; +}): Promise> { + try { + const user = await getUser(); + const payload = verifyUploadToken(input.uploadToken); + if (!payload || payload.userId !== user.id) { + return { success: false, error: "Upload de anexo inválido ou expirado." }; + } + if (!(await findOwnedNote(payload.noteId, user.id))) { + return { success: false, error: "Nota não encontrada." }; + } + + const metadata = await headS3Object(payload.fileKey); + if ( + !metadata.contentLength || + metadata.contentLength !== payload.fileSize || + metadata.contentLength > MAX_NOTE_FILE_SIZE || + metadata.contentType !== payload.mimeType + ) { + return { + success: false, + error: "O arquivo enviado não confere com o upload autorizado.", + }; + } + + const [attachment] = await db + .insert(attachments) + .values({ + userId: user.id, + fileKey: payload.fileKey, + fileName: payload.fileName, + fileSize: payload.fileSize, + mimeType: payload.mimeType, + }) + .returning({ id: attachments.id }); + if (!attachment) + return { success: false, error: "Não foi possível salvar o anexo." }; + + await db.insert(noteAttachments).values({ + noteId: payload.noteId, + attachmentId: attachment.id, + }); + revalidateForEntity("notes", user.id); + return { + success: true, + message: "Anexo enviado.", + data: { + attachmentId: attachment.id, + fileName: payload.fileName, + fileSize: payload.fileSize, + mimeType: payload.mimeType, + }, + }; + } catch (error) { + const result = handleActionError(error); + return { + success: false, + error: result.success ? "Ocorreu um erro inesperado." : result.error, + }; + } +} + +export async function removeNoteAttachmentAction(input: { + noteId: string; + attachmentId: string; +}): Promise { + try { + const user = await getUser(); + const data = z + .object({ noteId: z.string().uuid(), attachmentId: z.string().uuid() }) + .parse(input); + if (!(await findOwnedNote(data.noteId, user.id))) { + return { success: false, error: "Nota não encontrada." }; + } + const [attachment] = await db + .select({ fileKey: attachments.fileKey }) + .from(noteAttachments) + .innerJoin( + attachments, + and( + eq(noteAttachments.attachmentId, attachments.id), + eq(attachments.userId, user.id), + ), + ) + .where( + and( + eq(noteAttachments.noteId, data.noteId), + eq(noteAttachments.attachmentId, data.attachmentId), + ), + ); + if (!attachment) return { success: false, error: "Anexo não encontrado." }; + + await db + .delete(noteAttachments) + .where( + and( + eq(noteAttachments.noteId, data.noteId), + eq(noteAttachments.attachmentId, data.attachmentId), + ), + ); + await deleteS3Object(attachment.fileKey); + await db + .delete(attachments) + .where( + and( + eq(attachments.id, data.attachmentId), + eq(attachments.userId, user.id), + ), + ); + revalidateForEntity("notes", user.id); + return { success: true, message: "Anexo removido." }; + } catch (error) { + return handleActionError(error); + } +} diff --git a/src/features/notes/components/note-attachments-field.tsx b/src/features/notes/components/note-attachments-field.tsx new file mode 100644 index 0000000..c07c2e7 --- /dev/null +++ b/src/features/notes/components/note-attachments-field.tsx @@ -0,0 +1,345 @@ +"use client"; + +import { + RiAttachment2, + RiCloseLine, + RiDeleteBinLine, + RiDownloadLine, + RiFileImageLine, + RiFilePdf2Line, +} from "@remixicon/react"; +import { useRef, useState } from "react"; +import { toast } from "sonner"; +import { + confirmNoteAttachmentUploadAction, + getPresignedNoteAttachmentUploadUrlAction, + removeNoteAttachmentAction, +} from "@/features/notes/actions/attachments"; +import type { NoteAttachment } from "@/features/notes/components/types"; +import { Button } from "@/shared/components/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/shared/components/ui/dialog"; +import { + ALLOWED_MIME_TYPES, + DEFAULT_MAX_FILE_SIZE_MB, +} from "@/shared/lib/attachments/config"; + +type UploadResult = + | { success: true; attachment: NoteAttachment } + | { success: false; error: string }; + +export async function uploadNoteAttachment( + noteId: string, + file: File, +): Promise { + try { + const presign = await getPresignedNoteAttachmentUploadUrlAction({ + noteId, + fileName: file.name, + fileSize: file.size, + mimeType: file.type, + }); + if (!presign.success) return presign; + + const uploaded = await fetch(presign.presignedUrl, { + method: "PUT", + body: file, + headers: { "Content-Type": file.type }, + }); + if (!uploaded.ok) { + return { success: false, error: "Não foi possível enviar o arquivo." }; + } + + const confirmed = await confirmNoteAttachmentUploadAction({ + uploadToken: presign.uploadToken, + }); + if (!confirmed.success || !confirmed.data) { + return { + success: false, + error: confirmed.success + ? "Não foi possível salvar o anexo." + : confirmed.error, + }; + } + return { success: true, attachment: confirmed.data }; + } catch { + return { + success: false, + error: "Não foi possível enviar o arquivo agora.", + }; + } +} + +function formatBytes(bytes: number): string { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; +} + +function validateFile(file: File, maxSizeMb: number): string | null { + if ( + !ALLOWED_MIME_TYPES.includes( + file.type as (typeof ALLOWED_MIME_TYPES)[number], + ) + ) { + return "Tipo não suportado. Use PDF, JPEG, PNG ou WebP."; + } + if (file.size > maxSizeMb * 1024 * 1024) { + return `O arquivo deve ter no máximo ${maxSizeMb}MB.`; + } + return null; +} + +interface NoteAttachmentsFieldProps { + noteId?: string; + attachments: NoteAttachment[]; + pendingFiles: File[]; + onAttachmentsChange: (attachments: NoteAttachment[]) => void; + onPendingFilesChange: (files: File[]) => void; + onBusyChange?: (busy: boolean) => void; + maxSizeMb?: number; + disabled?: boolean; + readonly?: boolean; +} + +export function NoteAttachmentsField({ + noteId, + attachments, + pendingFiles, + onAttachmentsChange, + onPendingFilesChange, + onBusyChange, + maxSizeMb = DEFAULT_MAX_FILE_SIZE_MB, + disabled = false, + readonly = false, +}: NoteAttachmentsFieldProps) { + const inputRef = useRef(null); + const [uploading, setUploading] = useState(false); + const [removing, setRemoving] = useState(null); + const [isRemoving, setIsRemoving] = useState(false); + const [openingId, setOpeningId] = useState(null); + + async function addFiles(files: File[]) { + const valid: File[] = []; + for (const file of files) { + const error = validateFile(file, maxSizeMb); + if (error) toast.error(`${file.name}: ${error}`); + else valid.push(file); + } + if (valid.length === 0) return; + + if (!noteId) { + onPendingFilesChange([...pendingFiles, ...valid]); + return; + } + + setUploading(true); + onBusyChange?.(true); + const added: NoteAttachment[] = []; + for (const file of valid) { + const result = await uploadNoteAttachment(noteId, file); + if (result.success) added.push(result.attachment); + else toast.error(`${file.name}: ${result.error}`); + } + setUploading(false); + onBusyChange?.(false); + if (added.length > 0) { + onAttachmentsChange([...attachments, ...added]); + toast.success( + added.length === 1 + ? "Anexo enviado." + : `${added.length} anexos enviados.`, + ); + } + } + + async function downloadAttachment(attachment: NoteAttachment) { + setOpeningId(attachment.attachmentId); + try { + const response = await fetch( + `/api/attachments/${attachment.attachmentId}/presign`, + ); + if (!response.ok) throw new Error(); + const { url } = (await response.json()) as { url: string }; + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = attachment.fileName; + anchor.target = "_blank"; + anchor.rel = "noreferrer"; + anchor.click(); + } catch { + toast.error("Não foi possível baixar o anexo agora."); + } finally { + setOpeningId(null); + } + } + + async function confirmRemove() { + if (!noteId || !removing) return; + setIsRemoving(true); + onBusyChange?.(true); + const result = await removeNoteAttachmentAction({ + noteId, + attachmentId: removing.attachmentId, + }); + setIsRemoving(false); + onBusyChange?.(false); + if (result.success) { + onAttachmentsChange( + attachments.filter( + (item) => item.attachmentId !== removing.attachmentId, + ), + ); + setRemoving(null); + toast.success(result.message); + } else { + toast.error(result.error); + } + } + + return ( +
+

Anexos

+ { + void addFiles(Array.from(event.target.files ?? [])); + event.target.value = ""; + }} + /> + + {attachments.length > 0 && ( +
+ {attachments.map((attachment) => ( +
+ {attachment.mimeType === "application/pdf" ? ( + + ) : ( + + )} +
+

+ {attachment.fileName} +

+

+ {formatBytes(attachment.fileSize)} +

+
+ + {!readonly && ( + + )} +
+ ))} +
+ )} + + {pendingFiles.map((file, index) => ( +
+ +
+

{file.name}

+

+ Será enviado ao salvar +

+
+ +
+ ))} + + {!readonly && ( + + )} + + !open && setRemoving(null)} + > + + + Remover anexo? + + O arquivo {removing?.fileName} será removido desta nota. + + + + + + + + + + +
+ ); +} diff --git a/src/features/notes/components/note-card.tsx b/src/features/notes/components/note-card.tsx index a39ade2..117a234 100644 --- a/src/features/notes/components/note-card.tsx +++ b/src/features/notes/components/note-card.tsx @@ -2,6 +2,7 @@ import { RiArchiveLine, + RiAttachment2, RiCheckLine, RiDeleteBin5Line, RiFileList2Line, @@ -87,11 +88,19 @@ export function NoteCard({ )} - {isTask && ( - - {completedCount}/{totalCount} concluídas - - )} +
+ {isTask && ( + + {completedCount}/{totalCount} concluídas + + )} + {!isTask && note.attachments.length > 0 && ( + + + {note.attachments.length} + + )} +
{isTask ? ( diff --git a/src/features/notes/components/note-details-dialog.tsx b/src/features/notes/components/note-details-dialog.tsx index 526d40c..990ad33 100644 --- a/src/features/notes/components/note-details-dialog.tsx +++ b/src/features/notes/components/note-details-dialog.tsx @@ -1,6 +1,7 @@ "use client"; import { RiCheckLine, RiSubtractLine } from "@remixicon/react"; +import { NoteAttachmentsField } from "@/features/notes/components/note-attachments-field"; import { buildNoteDisplayTitle, formatNoteCreatedAtLong, @@ -85,8 +86,20 @@ export function NoteDetailsDialog({ ))} ) : ( -
- {note.description} +
+
+ {note.description} +
+ {note.attachments.length > 0 && ( + undefined} + onPendingFilesChange={() => undefined} + readonly + /> + )}
)} diff --git a/src/features/notes/components/note-dialog.tsx b/src/features/notes/components/note-dialog.tsx index a6b0a7e..e16aaff 100644 --- a/src/features/notes/components/note-dialog.tsx +++ b/src/features/notes/components/note-dialog.tsx @@ -15,6 +15,10 @@ import { } from "react"; import { toast } from "sonner"; import { createNoteAction, updateNoteAction } from "@/features/notes/actions"; +import { + NoteAttachmentsField, + uploadNoteAttachment, +} from "@/features/notes/components/note-attachments-field"; import { Button } from "@/shared/components/ui/button"; import { Checkbox } from "@/shared/components/ui/checkbox"; import { @@ -34,6 +38,7 @@ import { useFormState } from "@/shared/hooks/use-form-state"; import { cn } from "@/shared/utils/ui"; import { type Note, + type NoteAttachment, type NoteFormValues, sortTasksByStatus, type Task, @@ -46,6 +51,7 @@ interface NoteDialogProps { note?: Note; open?: boolean; onOpenChange?: (open: boolean) => void; + attachmentMaxSizeMb?: number; } const MAX_TITLE = 30; @@ -69,12 +75,16 @@ export function NoteDialog({ note, open, onOpenChange, + attachmentMaxSizeMb, }: NoteDialogProps) { const [isPending, startTransition] = useTransition(); const [errorMessage, setErrorMessage] = useState(null); const [newTaskText, setNewTaskText] = useState(""); const [editingTaskId, setEditingTaskId] = useState(null); const [editingTaskText, setEditingTaskText] = useState(""); + const [noteAttachments, setNoteAttachments] = useState([]); + const [pendingFiles, setPendingFiles] = useState([]); + const [isAttachmentPending, setIsAttachmentPending] = useState(false); const titleRef = useRef(null); const descRef = useRef(null); @@ -99,6 +109,9 @@ export function NoteDialog({ setNewTaskText(""); setEditingTaskId(null); setEditingTaskText(""); + setNoteAttachments(note?.attachments ?? []); + setPendingFiles([]); + setIsAttachmentPending(false); requestAnimationFrame(() => titleRef.current?.focus()); } }, [dialogOpen, note, resetForm]); @@ -137,12 +150,14 @@ export function NoteDialog({ const disableSubmit = isPending || + isAttachmentPending || onlySpaces || unchanged || invalidLen || Boolean(editingTaskId); const handleOpenChange = (v: boolean) => { + if (!v && (isPending || isAttachmentPending)) return; setDialogOpen(v); if (!v) setErrorMessage(null); }; @@ -252,7 +267,9 @@ export function NoteDialog({ } startTransition(async () => { - let result: { success: boolean; message?: string; error?: string }; + let result: + | Awaited> + | Awaited>; if (mode === "create") { result = await createNoteAction(payload); } else { @@ -266,7 +283,31 @@ export function NoteDialog({ } if (result.success) { - toast.success(result.message); + if (mode === "create" && pendingFiles.length > 0) { + const noteId = "data" in result ? result.data?.noteId : undefined; + if (noteId) { + let failedUploads = 0; + for (const file of pendingFiles) { + const upload = await uploadNoteAttachment(noteId, file); + if (!upload.success) failedUploads += 1; + } + if (failedUploads > 0) { + toast.warning( + failedUploads === 1 + ? "A nota foi salva, mas um anexo não pôde ser enviado." + : `A nota foi salva, mas ${failedUploads} anexos não puderam ser enviados.`, + ); + } else { + toast.success( + pendingFiles.length === 1 + ? "Anotação e anexo salvos." + : "Anotação e anexos salvos.", + ); + } + } + } else { + toast.success(result.message); + } setDialogOpen(false); return; } @@ -355,35 +396,48 @@ export function NoteDialog({
{isNote && ( -
-
- - MAX_DESC - ? "text-destructive" - : "text-muted-foreground", - )} - > - {descCount}/{MAX_DESC} - +
+
+
+ + MAX_DESC + ? "text-destructive" + : "text-muted-foreground", + )} + > + {descCount}/{MAX_DESC} + +
+