From b1cc6368a1f8039972f9e62cf4345595a2d3dcbf Mon Sep 17 00:00:00 2001 From: Duarte Date: Wed, 3 Jun 2026 01:13:58 +0100 Subject: [PATCH] =?UTF-8?q?Integra=C3=A7=C3=A3o=20com=20Odoo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 7 + docs/project-description.md | 5 +- drizzle/0000_light_red_wolf.sql | 61 +++ drizzle/meta/0000_snapshot.json | 404 ++++++++++++++++++ drizzle/meta/_journal.json | 13 + src/lib/server/db/index.ts | 44 ++ src/lib/server/db/schema.ts | 22 +- src/lib/server/odoo.ts | 260 +++++++++++ .../admin/beneficiarios/+page.server.ts | 25 +- src/routes/admin/beneficiarios/+page.svelte | 163 ++++++- src/routes/entregas/+page.server.ts | 11 +- src/routes/entregas/+page.svelte | 16 +- src/routes/login/+page.server.ts | 3 +- 13 files changed, 1007 insertions(+), 27 deletions(-) create mode 100644 drizzle/0000_light_red_wolf.sql create mode 100644 drizzle/meta/0000_snapshot.json create mode 100644 drizzle/meta/_journal.json create mode 100644 src/lib/server/odoo.ts diff --git a/.env.example b/.env.example index b86d245..f4646fb 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,9 @@ # Drizzle DATABASE_URL=local.db + +# Odoo API +ODOO_URL= +ODOO_DB= +ODOO_KEY= +ODOO_USER= +ODOO_COMPANY_ID= diff --git a/docs/project-description.md b/docs/project-description.md index 8efde09..1dc16a3 100644 --- a/docs/project-description.md +++ b/docs/project-description.md @@ -26,6 +26,5 @@ Main modules * Track delivery * List deliveries - - - +## Integrações Externas +* **Beneficiários**: O repositório dos beneficiários é a API json-rpc da Odoo v14. diff --git a/drizzle/0000_light_red_wolf.sql b/drizzle/0000_light_red_wolf.sql new file mode 100644 index 0000000..4f9d783 --- /dev/null +++ b/drizzle/0000_light_red_wolf.sql @@ -0,0 +1,61 @@ +CREATE TABLE `beneficiaries` ( + `id` text PRIMARY KEY NOT NULL, + `number` integer, + `name` text NOT NULL, + `contact` text NOT NULL, + `household_size` integer DEFAULT 1 NOT NULL, + `observations` text, + `status` text DEFAULT 'ativo' NOT NULL, + `odoo_id` integer, + `odoo_number` text, + `parent_odoo_id` integer, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `odoo_id_idx` ON `beneficiaries` (`odoo_id`);--> statement-breakpoint +CREATE INDEX `number_idx` ON `beneficiaries` (`number`);--> statement-breakpoint +CREATE TABLE `deliveries` ( + `id` text PRIMARY KEY NOT NULL, + `beneficiary_id` text NOT NULL, + `shift_id` text NOT NULL, + `date` text NOT NULL, + `created_at` integer NOT NULL, + FOREIGN KEY (`beneficiary_id`) REFERENCES `beneficiaries`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`shift_id`) REFERENCES `shifts`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `sessions` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `expires_at` integer NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `settings` ( + `key` text PRIMARY KEY NOT NULL, + `value` text NOT NULL, + `updated_at` integer NOT NULL +); +--> statement-breakpoint +CREATE TABLE `shifts` ( + `id` text PRIMARY KEY NOT NULL, + `code` text NOT NULL, + `start_time` text NOT NULL, + `end_time` text NOT NULL, + `days` text NOT NULL, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `shifts_code_unique` ON `shifts` (`code`);--> statement-breakpoint +CREATE TABLE `users` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `username` text NOT NULL, + `password_hash` text NOT NULL, + `role` text NOT NULL, + `created_at` integer NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `users_username_unique` ON `users` (`username`); \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..30fd64f --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,404 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "08266197-0b38-43f0-a6c0-19ac410008f8", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "beneficiaries": { + "name": "beneficiaries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contact": { + "name": "contact", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "household_size": { + "name": "household_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "observations": { + "name": "observations", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ativo'" + }, + "odoo_id": { + "name": "odoo_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "odoo_number": { + "name": "odoo_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_odoo_id": { + "name": "parent_odoo_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "odoo_id_idx": { + "name": "odoo_id_idx", + "columns": [ + "odoo_id" + ], + "isUnique": true + }, + "number_idx": { + "name": "number_idx", + "columns": [ + "number" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "deliveries": { + "name": "deliveries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "beneficiary_id": { + "name": "beneficiary_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "shift_id": { + "name": "shift_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "deliveries_beneficiary_id_beneficiaries_id_fk": { + "name": "deliveries_beneficiary_id_beneficiaries_id_fk", + "tableFrom": "deliveries", + "tableTo": "beneficiaries", + "columnsFrom": [ + "beneficiary_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deliveries_shift_id_shifts_id_fk": { + "name": "deliveries_shift_id_shifts_id_fk", + "tableFrom": "deliveries", + "tableTo": "shifts", + "columnsFrom": [ + "shift_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shifts": { + "name": "shifts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "start_time": { + "name": "start_time", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "end_time": { + "name": "end_time", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "days": { + "name": "days", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "shifts_code_unique": { + "name": "shifts_code_unique", + "columns": [ + "code" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "users_username_unique": { + "name": "users_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..25ca2a5 --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1780444363958, + "tag": "0000_light_red_wolf", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/src/lib/server/db/index.ts b/src/lib/server/db/index.ts index 3f95b3d..f3c5182 100644 --- a/src/lib/server/db/index.ts +++ b/src/lib/server/db/index.ts @@ -43,6 +43,50 @@ try { console.error("Migration failed:", err); } +// Run schema migration if 'odoo_id', 'parent_odoo_id' or 'odoo_number' column is missing in beneficiaries +try { + const tableInfo = client.prepare("PRAGMA table_info(beneficiaries)").all() as any[]; + const hasOdooId = tableInfo.some((col) => col.name === 'odoo_id'); + const hasParentOdooId = tableInfo.some((col) => col.name === 'parent_odoo_id'); + const hasOdooNumber = tableInfo.some((col) => col.name === 'odoo_number'); + + if (!hasOdooId) { + console.log("Migrating beneficiaries table: adding 'odoo_id' column..."); + client.prepare("ALTER TABLE beneficiaries ADD COLUMN odoo_id INTEGER").run(); + } + if (!hasParentOdooId) { + console.log("Migrating beneficiaries table: adding 'parent_odoo_id' column..."); + client.prepare("ALTER TABLE beneficiaries ADD COLUMN parent_odoo_id INTEGER").run(); + } + if (!hasOdooNumber) { + console.log("Migrating beneficiaries table: adding 'odoo_number' column..."); + client.prepare("ALTER TABLE beneficiaries ADD COLUMN odoo_number TEXT").run(); + } + + // Create unique index if it doesn't exist + client.prepare("CREATE UNIQUE INDEX IF NOT EXISTS odoo_id_idx ON beneficiaries(odoo_id)").run(); + + console.log("Database migration for beneficiaries table completed."); +} catch (err) { + console.error("Migration for beneficiaries failed:", err); +} + +// Run schema migration to create settings table if missing +try { + client.prepare(` + CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at INTEGER NOT NULL + ) + `).run(); + console.log("Database migration for settings table completed."); +} catch (err) { + console.error("Migration for settings failed:", err); +} + + + export const db = drizzle(client, { schema }); // Seed admin user if the users table is empty (US00) diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts index f149157..c687261 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -1,4 +1,4 @@ -import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; +import { index, integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core'; export const users = sqliteTable('users', { id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()), @@ -19,15 +19,24 @@ export const sessions = sqliteTable('sessions', { export const beneficiaries = sqliteTable('beneficiaries', { id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()), - number: integer('number').notNull().unique(), + number: integer('number'), name: text('name').notNull(), contact: text('contact').notNull(), householdSize: integer('household_size').notNull().default(1), observations: text('observations'), status: text('status').notNull().default('ativo'), // 'ativo' | 'inativo' + odooId: integer('odoo_id'), + odooNumber: text('odoo_number'), + parentOdooId: integer('parent_odoo_id'), + isParent: integer('is_parent', { mode: 'boolean' }), createdAt: integer('created_at').notNull().$defaultFn(() => Date.now()), updatedAt: integer('updated_at').notNull().$defaultFn(() => Date.now()) -}); +}, (table) => [ + uniqueIndex('odoo_id_idx').on(table.odooId), + index('number_idx').on(table.number) +]); + + export const shifts = sqliteTable('shifts', { id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()), @@ -51,3 +60,10 @@ export const deliveries = sqliteTable('deliveries', { createdAt: integer('created_at').notNull().$defaultFn(() => Date.now()) }); +export const settings = sqliteTable('settings', { + key: text('key').primaryKey(), + value: text('value').notNull(), + updatedAt: integer('updated_at').notNull().$defaultFn(() => Date.now()) +}); + + diff --git a/src/lib/server/odoo.ts b/src/lib/server/odoo.ts new file mode 100644 index 0000000..d1ec181 --- /dev/null +++ b/src/lib/server/odoo.ts @@ -0,0 +1,260 @@ +import { env } from '$env/dynamic/private'; + +/** + * Authenticates with the Odoo external API and returns the user's UID. + */ +export async function authenticateOdoo(): Promise { + const url = env.ODOO_URL || 'https://erp.onrefood.com/jsonrpc'; + const db = env.ODOO_DB; + const user = env.ODOO_USER; + const key = env.ODOO_KEY; + + if (!db || !user || !key) { + throw new Error('Faltam parâmetros de configuração do Odoo no ambiente (.env): ODOO_DB, ODOO_USER, ODOO_KEY'); + } + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'call', + params: { + service: 'common', + method: 'authenticate', + args: [db, user, key, {}] + }, + id: 1 + }) + }); + + if (!response.ok) { + throw new Error(`Erro na chamada HTTP do Odoo: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + if (data.error) { + throw new Error(`Erro retornado pela API do Odoo: ${JSON.stringify(data.error)}`); + } + + // In Odoo, if authentication fails, it returns false (not throwing jsonrpc error always) + const uid = data.result; + if (uid === false || uid === undefined || uid === null) { + throw new Error('Credenciais do Odoo incorretas (UID não retornado).'); + } + + return uid; +} + +/** + * Syncs beneficiaries from Odoo. + */ +import { db } from './db'; +import * as schema from './db/schema'; +import { eq, sql } from 'drizzle-orm'; + +export async function syncBeneficiaries(): Promise<{ success: boolean; count: number; error?: string }> { + try { + const url = env.ODOO_URL || 'https://erp.onrefood.com/jsonrpc'; + const dbName = env.ODOO_DB; + const user = env.ODOO_USER; + const key = env.ODOO_KEY; + + if (!dbName || !user || !key) { + throw new Error('Faltam parâmetros de configuração do Odoo no ambiente (.env): ODOO_DB, ODOO_USER, ODOO_KEY'); + } + + // 1. Authenticate to get UID + const uid = await authenticateOdoo(); + + // 2. Get last sync date from database settings + const lastSyncSetting = db + .select() + .from(schema.settings) + .where(eq(schema.settings.key, 'last_beneficiary_sync')) + .get(); + + const lastSyncTime = lastSyncSetting ? lastSyncSetting.value : '2026-01-01 00:00:00'; + + const companyIdRaw = env.ODOO_COMPANY_ID; + const companyId = companyIdRaw ? parseInt(companyIdRaw, 10) : 107; + + // 3. Make execute_kw call to search_read beneficiaries + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'call', + params: { + service: 'object', + method: 'execute_kw', + args: [ + dbName, + uid, + key, + 'res.beneficiary', + 'search_read', + [[ + ['write_date', '>=', lastSyncTime] + ]], + { + context: { + allowed_company_ids: [companyId], + active_test: false + } + } + ] + }, + id: 2 + }) + }); + + if (!response.ok) { + throw new Error(`Erro na chamada HTTP do Odoo search_read: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + if (data.error) { + throw new Error(`Erro retornado pela API do Odoo search_read: ${JSON.stringify(data.error)}`); + } + + const results = data.result; + if (!Array.isArray(results)) { + throw new Error('Formato inválido de resposta do Odoo (result não é um array)'); + } + + let syncCount = 0; + const now = Date.now(); + + // Helper to extract trailing digits as integer + const extractNumber = (odooNum: string | null | undefined): number | null => { + if (!odooNum) return null; + const match = odooNum.match(/\d+$/); + return match ? parseInt(match[0], 10) : null; + }; + + // 4. Process and save/update each beneficiary + for (const result of results) { + const odooId = result.id; + const name = result.name || ''; + const contact = result.phone || result.mobile || ''; + const odooNumber = result.number || ''; + const parsedNumber = extractNumber(odooNumber); + const parentOdooId = Array.isArray(result.parent_id) ? result.parent_id[0] : null; + const isParent = parentOdooId === null; + const activeStatus = result.active !== false ? 'ativo' : 'inativo'; + + // Check if beneficiary with odooId already exists + const existingById = db + .select() + .from(schema.beneficiaries) + .where(eq(schema.beneficiaries.odooId, odooId)) + .get(); + + if (existingById) { + // Update existing + db.update(schema.beneficiaries) + .set({ + name, + contact, + number: parsedNumber !== null ? parsedNumber : existingById.number, + odooNumber, + parentOdooId, + isParent, + status: activeStatus, + updatedAt: now + }) + .where(eq(schema.beneficiaries.id, existingById.id)) + .run(); + } else { + // Insert new + db.insert(schema.beneficiaries) + .values({ + number: parsedNumber, + name, + contact, + odooId, + odooNumber, + parentOdooId, + isParent, + status: activeStatus, + createdAt: now, + updatedAt: now + }) + .run(); + } + syncCount++; + } + + // Update household size for all main beneficiaries (parentOdooId is null) + // householdSize = 1 + count of dependents (other beneficiaries with parentOdooId pointing to this odooId) + db.run( + sql` + UPDATE beneficiaries + SET household_size = 1 + ( + SELECT COUNT(*) + FROM beneficiaries AS dependents + WHERE dependents.parent_odoo_id = beneficiaries.odoo_id + ) + WHERE parent_odoo_id IS NULL AND odoo_id IS NOT NULL + ` + ); + + // Set household size of dependents (parentOdooId is not null) to 1 + db.run( + sql` + UPDATE beneficiaries + SET household_size = 1 + WHERE parent_odoo_id IS NOT NULL AND odoo_id IS NOT NULL + ` + ); + + // 5. Update last sync date to current time in UTC format (YYYY-MM-DD HH:MM:SS) + const formatUTC = (date: Date): string => { + const pad = (n: number) => n.toString().padStart(2, '0'); + return ( + date.getUTCFullYear() + + '-' + + pad(date.getUTCMonth() + 1) + + '-' + + pad(date.getUTCDate()) + + ' ' + + pad(date.getUTCHours()) + + ':' + + pad(date.getUTCMinutes()) + + ':' + + pad(date.getUTCSeconds()) + ); + }; + const newSyncTime = formatUTC(new Date()); + + // Upsert settings table + const hasSetting = db + .select() + .from(schema.settings) + .where(eq(schema.settings.key, 'last_beneficiary_sync')) + .get(); + + if (hasSetting) { + db.update(schema.settings) + .set({ value: newSyncTime, updatedAt: now }) + .where(eq(schema.settings.key, 'last_beneficiary_sync')) + .run(); + } else { + db.insert(schema.settings) + .values({ key: 'last_beneficiary_sync', value: newSyncTime, updatedAt: now }) + .run(); + } + + return { success: true, count: syncCount }; + } catch (error: any) { + console.error('Erro na sincronização de beneficiários:', error); + return { success: false, count: 0, error: error.message || 'Erro desconhecido' }; + } +} + diff --git a/src/routes/admin/beneficiarios/+page.server.ts b/src/routes/admin/beneficiarios/+page.server.ts index 600678d..f1adbdf 100644 --- a/src/routes/admin/beneficiarios/+page.server.ts +++ b/src/routes/admin/beneficiarios/+page.server.ts @@ -8,7 +8,10 @@ export const load: PageServerLoad = async ({ url }) => { const status = url.searchParams.get('status') || ''; try { - let query = db.select().from(schema.beneficiaries); + let query = db + .select() + .from(schema.beneficiaries) + .where(eq(schema.beneficiaries.isParent, true)); // We can fetch all and filter in memory since table size is guaranteed to be small (<20 tables, low concurrency). // This keeps SQLite query logic extremely simple and readable. @@ -19,7 +22,7 @@ export const load: PageServerLoad = async ({ url }) => { list = list.filter( (b) => b.name.toLowerCase().includes(searchLower) || - b.number.toString().includes(searchLower) || + (b.number !== null && b.number.toString().includes(searchLower)) || (b.contact && b.contact.includes(searchLower)) ); } @@ -43,3 +46,21 @@ export const load: PageServerLoad = async ({ url }) => { }; } }; + +import { syncBeneficiaries } from '$lib/server/odoo'; +import { fail } from '@sveltejs/kit'; +import type { Actions } from './$types'; + +export const actions: Actions = { + sync: async ({ locals }) => { + if (locals.user?.role !== 'admin') { + return fail(403, { error: 'Acesso negado. Apenas administradores podem sincronizar com o Odoo.' }); + } + const result = await syncBeneficiaries(); + if (!result.success) { + return fail(500, { error: result.error || 'Erro ao sincronizar com o Odoo.' }); + } + return { success: true, count: result.count }; + } +}; + diff --git a/src/routes/admin/beneficiarios/+page.svelte b/src/routes/admin/beneficiarios/+page.svelte index 2c86ed0..bc50852 100644 --- a/src/routes/admin/beneficiarios/+page.svelte +++ b/src/routes/admin/beneficiarios/+page.svelte @@ -1,11 +1,46 @@ @@ -24,17 +59,58 @@ {/if} + {#if form?.success} + + {/if} + + {#if form?.error} + + {/if} +

Beneficiários

Gerir a listagem de beneficiários e agregados familiares

- - - - - Novo Beneficiário - +
+ {#if data.user?.role === 'admin'} +
{ + syncing = true; + return async ({ update }) => { + await update(); + syncing = false; + }; + }} onsubmit={(e) => { if (!confirm('Tem a certeza que deseja sincronizar os beneficiários com o Odoo?')) e.preventDefault(); }}> + +
+ {/if} + + + + + Novo Beneficiário + +
@@ -69,20 +145,44 @@
+
+
Lista de Beneficiários
+
- - + + - + - {#if data.beneficiaries.length === 0} + {#if sortedBeneficiaries.length === 0} {:else} - {#each data.beneficiaries as beneficiary} + {#each sortedBeneficiaries as beneficiary}
NúmeroNome toggleSort('number')}> +
+ Número + {#if sortBy === 'number'} + {sortOrder === 'asc' ? '▲' : '▼'} + {/if} +
+
toggleSort('name')}> +
+ Nome + {#if sortBy === 'name'} + {sortOrder === 'asc' ? '▲' : '▼'} + {/if} +
+
ContactoAgregado toggleSort('householdSize')}> +
+ Agregado + {#if sortBy === 'householdSize'} + {sortOrder === 'asc' ? '▲' : '▼'} + {/if} +
+
Estado Ações
@@ -92,7 +192,7 @@
#{beneficiary.number} @@ -129,3 +229,42 @@ + + diff --git a/src/routes/entregas/+page.server.ts b/src/routes/entregas/+page.server.ts index 9a8865f..9494f96 100644 --- a/src/routes/entregas/+page.server.ts +++ b/src/routes/entregas/+page.server.ts @@ -1,6 +1,6 @@ import { db } from '$lib/server/db'; import * as schema from '$lib/server/db/schema'; -import { eq, and, asc } from 'drizzle-orm'; +import { eq, and, asc, isNull } from 'drizzle-orm'; import { fail, redirect } from '@sveltejs/kit'; import type { Actions, PageServerLoad } from './$types'; @@ -12,11 +12,16 @@ export const load: PageServerLoad = async ({ locals }) => { const todayStr = new Date().toLocaleDateString('sv-SE', { timeZone: 'Europe/Lisbon' }); // 'YYYY-MM-DD' try { - // Fetch active beneficiaries sorted by their unique number + // Fetch active beneficiaries sorted by their unique number, who are main beneficiaries (no parentOdooId) const activeBeneficiaries = db .select() .from(schema.beneficiaries) - .where(eq(schema.beneficiaries.status, 'ativo')) + .where( + and( + eq(schema.beneficiaries.status, 'ativo'), + isNull(schema.beneficiaries.parentOdooId) + ) + ) .orderBy(asc(schema.beneficiaries.number)) .all(); diff --git a/src/routes/entregas/+page.svelte b/src/routes/entregas/+page.svelte index 7602ef6..40d61b1 100644 --- a/src/routes/entregas/+page.svelte +++ b/src/routes/entregas/+page.svelte @@ -68,7 +68,17 @@
-
Selecione o Beneficiário
+
+
Selecione o Beneficiário
+
+ + {deliveredSet.size} entregues + + + {data.beneficiaries.length - deliveredSet.size} por entregar + +
+
{#if data.beneficiaries.length === 0}
@@ -87,12 +97,12 @@ disabled={isDelivered} aria-label="Beneficiário número {beneficiary.number} - {beneficiary.name}" > - {beneficiary.number} + {beneficiary.number ?? '-'} {beneficiary.name.split(' ')[0]} {#if isDelivered} - + diff --git a/src/routes/login/+page.server.ts b/src/routes/login/+page.server.ts index c47df03..6139fb7 100644 --- a/src/routes/login/+page.server.ts +++ b/src/routes/login/+page.server.ts @@ -4,6 +4,7 @@ import { eq } from 'drizzle-orm'; import { fail, redirect } from '@sveltejs/kit'; import type { Actions, PageServerLoad } from './$types'; import bcrypt from 'bcrypt'; +import { dev } from '$app/environment'; export const load: PageServerLoad = async ({ locals }) => { // If already logged in, redirect them @@ -67,7 +68,7 @@ export const actions: Actions = { path: '/', httpOnly: true, sameSite: 'lax', - secure: true, + secure: !dev, maxAge: 60 * 60 * 24 * 7 // 7 days in seconds });