Integração com Odoo
This commit is contained in:
@@ -1,2 +1,9 @@
|
||||
# Drizzle
|
||||
DATABASE_URL=local.db
|
||||
|
||||
# Odoo API
|
||||
ODOO_URL=
|
||||
ODOO_DB=
|
||||
ODOO_KEY=
|
||||
ODOO_USER=
|
||||
ODOO_COMPANY_ID=
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`);
|
||||
@@ -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": {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1780444363958,
|
||||
"tag": "0000_light_red_wolf",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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<number> {
|
||||
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' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,46 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { enhance } from '$app/forms';
|
||||
|
||||
let { data } = $props();
|
||||
let { data, form } = $props();
|
||||
|
||||
let syncing = $state(false);
|
||||
|
||||
let sortBy = $state<'number' | 'name' | 'householdSize'>('number');
|
||||
let sortOrder = $state<'asc' | 'desc'>('asc');
|
||||
|
||||
function toggleSort(field: 'number' | 'name' | 'householdSize') {
|
||||
if (sortBy === field) {
|
||||
sortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
sortBy = field;
|
||||
sortOrder = 'asc';
|
||||
}
|
||||
}
|
||||
|
||||
const sortedBeneficiaries = $derived.by(() => {
|
||||
const list = [...(data.beneficiaries || [])];
|
||||
return list.sort((a, b) => {
|
||||
let valA = a[sortBy];
|
||||
let valB = b[sortBy];
|
||||
|
||||
if (valA === null || valA === undefined) return 1;
|
||||
if (valB === null || valB === undefined) return -1;
|
||||
|
||||
if (typeof valA === 'string' && typeof valB === 'string') {
|
||||
return sortOrder === 'asc'
|
||||
? valA.localeCompare(valB, 'pt-PT')
|
||||
: valB.localeCompare(valA, 'pt-PT');
|
||||
} else {
|
||||
return sortOrder === 'asc'
|
||||
? (valA as number) - (valB as number)
|
||||
: (valB as number) - (valA as number);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const successMessage = $derived(page.url.searchParams.get('success'));
|
||||
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -24,17 +59,58 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if form?.success}
|
||||
<div class="alert alert-success border-0 shadow-sm rounded-3 d-flex align-items-center gap-2 mb-4" role="alert">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-check-circle-fill text-success" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
|
||||
</svg>
|
||||
<div>
|
||||
Sincronização concluída com sucesso! {form.count} beneficiários atualizados/importados.
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if form?.error}
|
||||
<div class="alert alert-danger border-0 shadow-sm rounded-3 d-flex align-items-center gap-2 mb-4" role="alert">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-exclamation-triangle-fill text-danger" viewBox="0 0 16 16">
|
||||
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
|
||||
</svg>
|
||||
<div>
|
||||
{form.error}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-start align-items-md-center gap-3 mb-4">
|
||||
<div>
|
||||
<h2 class="fw-bold text-dark mb-1">Beneficiários</h2>
|
||||
<p class="text-muted mb-0">Gerir a listagem de beneficiários e agregados familiares</p>
|
||||
</div>
|
||||
<a href="/admin/beneficiarios/novo" class="btn btn-success btn-lg d-flex align-items-center gap-2 rounded-3 shadow-sm border-0" style="background-color: var(--refood-primary, #FCB515);">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-plus-lg" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M8 2a.5.5 0 0 1 .5.5v5h5a.5.5 0 0 1 0 1h-5v5a.5.5 0 0 1-1 0v-5h-5a.5.5 0 0 1 0-1h5v-5A.5.5 0 0 1 8 2"/>
|
||||
</svg>
|
||||
Novo Beneficiário
|
||||
</a>
|
||||
<div class="d-flex gap-2">
|
||||
{#if data.user?.role === 'admin'}
|
||||
<form method="POST" action="?/sync" use:enhance={() => {
|
||||
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(); }}>
|
||||
<button type="submit" class="btn btn-outline-sync btn-lg d-flex align-items-center gap-2 rounded-3 shadow-sm" disabled={syncing}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-arrow-repeat {syncing ? 'spin' : ''}" viewBox="0 0 16 16">
|
||||
<path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z"/>
|
||||
<path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"/>
|
||||
</svg>
|
||||
{syncing ? 'A sincronizar...' : 'Sincronizar Odoo'}
|
||||
</button>
|
||||
</form>
|
||||
{/if}
|
||||
<a href="/admin/beneficiarios/novo" class="btn btn-success btn-lg d-flex align-items-center gap-2 rounded-3 shadow-sm border-0" style="background-color: var(--refood-primary, #FCB515);">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-plus-lg" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M8 2a.5.5 0 0 1 .5.5v5h5a.5.5 0 0 1 0 1h-5v5a.5.5 0 0 1-1 0v-5h-5a.5.5 0 0 1 0-1h5v-5A.5.5 0 0 1 8 2"/>
|
||||
</svg>
|
||||
Novo Beneficiário
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm rounded-4 p-3 mb-4 bg-white">
|
||||
@@ -69,20 +145,44 @@
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm rounded-4 overflow-hidden bg-white">
|
||||
<div class="card-header border-0 bg-light py-3 px-4 d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<h5 class="fw-bold text-dark mb-0">Lista de Beneficiários</h5>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light text-secondary fw-semibold">
|
||||
<tr>
|
||||
<th class="px-4 py-3" style="width: 120px;">Número</th>
|
||||
<th class="py-3">Nome</th>
|
||||
<th class="px-4 py-3 sortable-header" style="width: 120px;" onclick={() => toggleSort('number')}>
|
||||
<div class="d-flex align-items-center gap-1 select-none">
|
||||
Número
|
||||
{#if sortBy === 'number'}
|
||||
<span class="sort-arrow">{sortOrder === 'asc' ? '▲' : '▼'}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</th>
|
||||
<th class="py-3 sortable-header" onclick={() => toggleSort('name')}>
|
||||
<div class="d-flex align-items-center gap-1 select-none">
|
||||
Nome
|
||||
{#if sortBy === 'name'}
|
||||
<span class="sort-arrow">{sortOrder === 'asc' ? '▲' : '▼'}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</th>
|
||||
<th class="py-3">Contacto</th>
|
||||
<th class="py-3 text-center" style="width: 120px;">Agregado</th>
|
||||
<th class="py-3 text-center sortable-header" style="width: 120px;" onclick={() => toggleSort('householdSize')}>
|
||||
<div class="d-flex align-items-center justify-content-center gap-1 select-none">
|
||||
Agregado
|
||||
{#if sortBy === 'householdSize'}
|
||||
<span class="sort-arrow">{sortOrder === 'asc' ? '▲' : '▼'}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</th>
|
||||
<th class="py-3" style="width: 120px;">Estado</th>
|
||||
<th class="px-4 py-3 text-end" style="width: 120px;">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if data.beneficiaries.length === 0}
|
||||
{#if sortedBeneficiaries.length === 0}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-5 text-muted">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="currentColor" class="bi bi-people mb-2 text-black-50" viewBox="0 0 16 16">
|
||||
@@ -92,7 +192,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
{:else}
|
||||
{#each data.beneficiaries as beneficiary}
|
||||
{#each sortedBeneficiaries as beneficiary}
|
||||
<tr>
|
||||
<td class="px-4 fw-bold text-secondary">
|
||||
#{beneficiary.number}
|
||||
@@ -129,3 +229,42 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.sortable-header {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
}
|
||||
.sortable-header:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.select-none {
|
||||
user-select: none;
|
||||
}
|
||||
.sort-arrow {
|
||||
font-size: 0.75rem;
|
||||
color: var(--bs-primary, #2c562e);
|
||||
}
|
||||
.btn-outline-sync {
|
||||
color: #2c562e;
|
||||
border-color: #2c562e;
|
||||
background-color: transparent;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
.btn-outline-sync:hover:not(:disabled) {
|
||||
color: #ffffff;
|
||||
background-color: #2c562e;
|
||||
border-color: #2c562e;
|
||||
}
|
||||
.spin {
|
||||
animation: spin-animation 1s infinite linear;
|
||||
}
|
||||
@keyframes spin-animation {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -68,7 +68,17 @@
|
||||
|
||||
<!-- Beneficiaries Grid -->
|
||||
<div class="card border-0 shadow-sm rounded-4 p-4 mb-4 bg-white">
|
||||
<h5 class="fw-bold text-secondary mb-3">Selecione o Beneficiário</h5>
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
|
||||
<h5 class="fw-bold text-secondary mb-0">Selecione o Beneficiário</h5>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="badge bg-success-subtle text-success border border-success-subtle px-2.5 py-1 rounded-pill" style="font-size: 0.75rem;">
|
||||
{deliveredSet.size} entregues
|
||||
</span>
|
||||
<span class="badge bg-warning-subtle text-warning-emphasis border border-warning-subtle px-2.5 py-1 rounded-pill" style="font-size: 0.75rem;">
|
||||
{data.beneficiaries.length - deliveredSet.size} por entregar
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{#if data.beneficiaries.length === 0}
|
||||
<div class="text-center py-5 text-muted">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" class="bi bi-person-slash mb-2 text-black-50" viewBox="0 0 16 16">
|
||||
@@ -87,12 +97,12 @@
|
||||
disabled={isDelivered}
|
||||
aria-label="Beneficiário número {beneficiary.number} - {beneficiary.name}"
|
||||
>
|
||||
<span class="beneficiary-number fw-bold mb-0">{beneficiary.number}</span>
|
||||
<span class="beneficiary-number fw-bold mb-0">{beneficiary.number ?? '-'}</span>
|
||||
<span class="beneficiary-first-name text-truncate w-100 px-1 {isDelivered ? 'text-success' : 'text-secondary'}" style="font-size: 0.7rem; font-weight: 500; text-align: center;">
|
||||
{beneficiary.name.split(' ')[0]}
|
||||
</span>
|
||||
{#if isDelivered}
|
||||
<span class="delivery-status-icon mt-0.5">
|
||||
<span class="delivery-status-icon mt-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-check-circle-fill text-success" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
|
||||
</svg>
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user