- Add updateUser and resetUserPassword admin endpoints - Change company status from boolean to enum (registered, lead, customer, inactive) - Add 'important' event type to calendar validators and email templates - Add 1-hour-before event notifications cron job - Add 18:00 evening notifications for next-day events - Add contact description field support - Fix count() function usage in admin service - Add SQL migrations for schema changes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
286 lines
15 KiB
JavaScript
286 lines
15 KiB
JavaScript
import { pgTable, text, timestamp, boolean, uuid, pgEnum, unique, integer } from 'drizzle-orm/pg-core';
|
|
|
|
// Enums
|
|
export const roleEnum = pgEnum('role', ['admin', 'member']);
|
|
export const projectStatusEnum = pgEnum('project_status', ['active', 'completed', 'on_hold', 'cancelled']);
|
|
export const todoStatusEnum = pgEnum('todo_status', ['pending', 'in_progress', 'completed', 'cancelled']);
|
|
export const todoPriorityEnum = pgEnum('todo_priority', ['low', 'medium', 'high', 'urgent']);
|
|
export const companyStatusEnum = pgEnum('company_status', ['registered', 'lead', 'customer', 'inactive']);
|
|
|
|
// Users table - používatelia systému
|
|
export const users = pgTable('users', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
username: text('username').notNull().unique(),
|
|
firstName: text('first_name'),
|
|
lastName: text('last_name'),
|
|
password: text('password'), // bcrypt hash (null ak ešte nenastavené)
|
|
tempPassword: text('temp_password'), // dočasné heslo (bcrypt hash)
|
|
changedPassword: boolean('changed_password').default(false),
|
|
role: roleEnum('role').default('member').notNull(),
|
|
lastLogin: timestamp('last_login'),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
});
|
|
|
|
// Email Accounts table - emailové účty (môžu byť zdieľané medzi viacerými používateľmi)
|
|
export const emailAccounts = pgTable('email_accounts', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
email: text('email').notNull().unique(), // Email adresa
|
|
emailPassword: text('email_password').notNull(), // Heslo k emailovému účtu (encrypted)
|
|
jmapAccountId: text('jmap_account_id').notNull(), // JMAP account ID z truemail
|
|
isActive: boolean('is_active').default(true).notNull(), // či je účet aktívny
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
});
|
|
|
|
// User Email Accounts - many-to-many medzi users a emailAccounts
|
|
// Umožňuje zdieľať email účty medzi viacerými používateľmi
|
|
export const userEmailAccounts = pgTable('user_email_accounts', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
|
emailAccountId: uuid('email_account_id').references(() => emailAccounts.id, { onDelete: 'cascade' }).notNull(),
|
|
isPrimary: boolean('is_primary').default(false).notNull(), // primárny email účet pre daného usera
|
|
addedAt: timestamp('added_at').defaultNow().notNull(),
|
|
}, (table) => ({
|
|
// Jeden user môže mať email account len raz
|
|
userEmailUnique: unique('user_email_unique').on(table.userId, table.emailAccountId),
|
|
}));
|
|
|
|
// Audit logs - kompletný audit trail všetkých akcií
|
|
export const auditLogs = pgTable('audit_logs', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
userId: uuid('user_id').references(() => users.id, { onDelete: 'set null' }),
|
|
action: text('action').notNull(), // 'login', 'password_change', 'email_linked', 'role_change', atď.
|
|
resource: text('resource').notNull(), // 'user', 'auth', atď.
|
|
resourceId: text('resource_id'), // ID ovplyvneného zdroja
|
|
oldValue: text('old_value'), // JSON string starých hodnôt
|
|
newValue: text('new_value'), // JSON string nových hodnôt
|
|
ipAddress: text('ip_address'),
|
|
userAgent: text('user_agent'),
|
|
success: boolean('success').default(true).notNull(),
|
|
errorMessage: text('error_message'),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
});
|
|
|
|
// Contacts table - kontakty patriace k emailovému účtu
|
|
// Kontakty sú zdieľané medzi všetkými používateľmi, ktorí majú prístup k danému email accountu
|
|
export const contacts = pgTable('contacts', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
emailAccountId: uuid('email_account_id').references(() => emailAccounts.id, { onDelete: 'cascade' }).notNull(),
|
|
companyId: uuid('company_id').references(() => companies.id, { onDelete: 'set null' }), // kontakt môže byť linknutý k firme
|
|
email: text('email').notNull(),
|
|
name: text('name'),
|
|
notes: text('notes'),
|
|
addedBy: uuid('added_by').references(() => users.id, { onDelete: 'set null' }), // kto pridal kontakt
|
|
addedAt: timestamp('added_at').defaultNow().notNull(),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
}, (table) => ({
|
|
// Unique constraint: jeden email môže byť len raz v rámci email accountu
|
|
accountEmailUnique: unique('account_email_unique').on(table.emailAccountId, table.email),
|
|
}));
|
|
|
|
// Personal contacts - osobné kontakty používateľa (s voliteľnou väzbou na firmu)
|
|
export const personalContacts = pgTable('personal_contacts', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
|
companyId: uuid('company_id').references(() => companies.id, { onDelete: 'set null' }), // voliteľná väzba na firmu
|
|
firstName: text('first_name').notNull(),
|
|
lastName: text('last_name'),
|
|
phone: text('phone').notNull(),
|
|
email: text('email').notNull(),
|
|
secondaryEmail: text('secondary_email'),
|
|
description: text('description'), // popis kontaktu
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
}, (table) => ({
|
|
personalContactUniqueEmail: unique('personal_contact_user_email').on(table.userId, table.email),
|
|
}));
|
|
|
|
// Emails table - uložené emaily z JMAP
|
|
// Emaily patria k email accountu a sú zdieľané medzi všetkými používateľmi s prístupom
|
|
export const emails = pgTable('emails', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
emailAccountId: uuid('email_account_id').references(() => emailAccounts.id, { onDelete: 'cascade' }).notNull(),
|
|
contactId: uuid('contact_id').references(() => contacts.id, { onDelete: 'cascade' }),
|
|
companyId: uuid('company_id').references(() => companies.id, { onDelete: 'set null' }),
|
|
jmapId: text('jmap_id').unique(),
|
|
messageId: text('message_id').unique(),
|
|
threadId: text('thread_id'),
|
|
inReplyTo: text('in_reply_to'),
|
|
from: text('from'),
|
|
to: text('to'),
|
|
subject: text('subject'),
|
|
body: text('body'),
|
|
isRead: boolean('is_read').default(false).notNull(),
|
|
sentByUserId: uuid('sent_by_user_id').references(() => users.id, { onDelete: 'set null' }), // kto poslal odpoveď (null ak prijatý email)
|
|
date: timestamp('date'),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
});
|
|
|
|
// Companies table - firmy/spoločnosti
|
|
export const companies = pgTable('companies', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
name: text('name').notNull(),
|
|
description: text('description'),
|
|
address: text('address'),
|
|
city: text('city'),
|
|
country: text('country'),
|
|
phone: text('phone'),
|
|
email: text('email'),
|
|
website: text('website'),
|
|
status: companyStatusEnum('status').default('registered').notNull(), // stav firmy
|
|
createdBy: uuid('created_by').references(() => users.id, { onDelete: 'set null' }),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
});
|
|
|
|
// Projects table - projekty
|
|
export const projects = pgTable('projects', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
name: text('name').notNull(),
|
|
description: text('description'),
|
|
companyId: uuid('company_id').references(() => companies.id, { onDelete: 'cascade' }), // projekt môže patriť firme
|
|
status: projectStatusEnum('status').default('active').notNull(),
|
|
startDate: timestamp('start_date'),
|
|
endDate: timestamp('end_date'),
|
|
createdBy: uuid('created_by').references(() => users.id, { onDelete: 'set null' }),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
});
|
|
|
|
// Company reminders table - pripomienky naviazané na firmu s dátumom
|
|
export const companyReminders = pgTable('company_remind', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
companyId: uuid('company_id').references(() => companies.id, { onDelete: 'cascade' }).notNull(),
|
|
description: text('description').notNull(),
|
|
dueDate: timestamp('due_date'), // kedy má byť splnená
|
|
isChecked: boolean('is_checked').default(false).notNull(),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
});
|
|
|
|
// Company Users - many-to-many medzi companies a users (tím firmy)
|
|
export const companyUsers = pgTable('company_users', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
companyId: uuid('company_id').references(() => companies.id, { onDelete: 'cascade' }).notNull(),
|
|
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
|
role: text('role'), // napr. 'lead', 'member', 'viewer' - voliteľné
|
|
addedBy: uuid('added_by').references(() => users.id, { onDelete: 'set null' }), // kto pridal používateľa do firmy
|
|
addedAt: timestamp('added_at').defaultNow().notNull(),
|
|
}, (table) => ({
|
|
companyUserUnique: unique('company_user_unique').on(table.companyId, table.userId),
|
|
}));
|
|
|
|
// Project Users - many-to-many medzi projects a users (tím projektu)
|
|
export const projectUsers = pgTable('project_users', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
projectId: uuid('project_id').references(() => projects.id, { onDelete: 'cascade' }).notNull(),
|
|
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
|
role: text('role'), // napr. 'lead', 'member', 'viewer' - voliteľné
|
|
addedBy: uuid('added_by').references(() => users.id, { onDelete: 'set null' }), // kto pridal používateľa do projektu
|
|
addedAt: timestamp('added_at').defaultNow().notNull(),
|
|
}, (table) => ({
|
|
projectUserUnique: unique('project_user_unique').on(table.projectId, table.userId),
|
|
}));
|
|
|
|
// Todos table - úlohy/tasky
|
|
export const todos = pgTable('todos', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
title: text('title').notNull(),
|
|
description: text('description'),
|
|
projectId: uuid('project_id').references(() => projects.id, { onDelete: 'cascade' }), // todo môže patriť projektu
|
|
companyId: uuid('company_id').references(() => companies.id, { onDelete: 'cascade' }), // alebo firme
|
|
status: todoStatusEnum('status').default('pending').notNull(),
|
|
priority: todoPriorityEnum('priority').default('medium').notNull(),
|
|
dueDate: timestamp('due_date'),
|
|
completedAt: timestamp('completed_at'),
|
|
createdBy: uuid('created_by').references(() => users.id, { onDelete: 'set null' }),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
});
|
|
|
|
// Todo Users - many-to-many medzi todos a users (priradení používatelia)
|
|
export const todoUsers = pgTable('todo_users', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
todoId: uuid('todo_id').references(() => todos.id, { onDelete: 'cascade' }).notNull(),
|
|
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
|
assignedBy: uuid('assigned_by').references(() => users.id, { onDelete: 'set null' }), // kto pridal používateľa k todo
|
|
assignedAt: timestamp('assigned_at').defaultNow().notNull(),
|
|
}, (table) => ({
|
|
todoUserUnique: unique('todo_user_unique').on(table.todoId, table.userId),
|
|
}));
|
|
|
|
// Notes table - poznámky s voliteľným dátumom a časom splnenia
|
|
export const notes = pgTable('notes', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
title: text('title'),
|
|
content: text('content').notNull(),
|
|
companyId: uuid('company_id').references(() => companies.id, { onDelete: 'cascade' }), // poznámka k firme
|
|
projectId: uuid('project_id').references(() => projects.id, { onDelete: 'cascade' }), // alebo projektu
|
|
todoId: uuid('todo_id').references(() => todos.id, { onDelete: 'cascade' }), // alebo todo
|
|
contactId: uuid('contact_id').references(() => contacts.id, { onDelete: 'cascade' }), // alebo kontaktu
|
|
dueDate: timestamp('due_date'), // voliteľný dátum a čas splnenia (24h formát)
|
|
createdBy: uuid('created_by').references(() => users.id, { onDelete: 'set null' }),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
});
|
|
|
|
// Timesheets table - nahrané timesheets od používateľov
|
|
export const timesheets = pgTable('timesheets', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(), // kto nahral timesheet
|
|
projectId: uuid('project_id').references(() => projects.id, { onDelete: 'set null' }), // projekt ku ktorému patrí timesheet
|
|
fileName: text('file_name').notNull(), // originálny názov súboru
|
|
filePath: text('file_path').notNull(), // cesta k súboru na serveri
|
|
fileType: text('file_type').notNull(), // 'pdf' alebo 'xlsx'
|
|
fileSize: integer('file_size').notNull(), // veľkosť súboru v bytoch
|
|
year: integer('year').notNull(), // rok (napr. 2024)
|
|
month: integer('month').notNull(), // mesiac (1-12)
|
|
isGenerated: boolean('is_generated').default(false).notNull(), // či bol súbor vygenerovaný systémom
|
|
uploadedAt: timestamp('uploaded_at').defaultNow().notNull(),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
});
|
|
|
|
// Events table - udalosti v kalendári (meeting/event)
|
|
export const events = pgTable('events', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
title: text('title').notNull(),
|
|
description: text('description'),
|
|
type: text('type').notNull().default('meeting'), // 'meeting' | 'event'
|
|
start: timestamp('start', { withTimezone: true }).notNull(),
|
|
end: timestamp('end', { withTimezone: true }).notNull(),
|
|
createdBy: uuid('created_by').references(() => users.id, { onDelete: 'set null' }),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
});
|
|
|
|
// Event Users - many-to-many medzi events a users (kto vidí udalosť)
|
|
export const eventUsers = pgTable('event_users', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
eventId: uuid('event_id').references(() => events.id, { onDelete: 'cascade' }).notNull(),
|
|
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
|
assignedAt: timestamp('assigned_at').defaultNow().notNull(),
|
|
}, (table) => ({
|
|
eventUserUnique: unique('event_user_unique').on(table.eventId, table.userId),
|
|
}));
|
|
|
|
// Time Entries table - sledovanie odpracovaného času používateľov
|
|
export const timeEntries = pgTable('time_entries', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(), // kto trackuje čas
|
|
projectId: uuid('project_id').references(() => projects.id, { onDelete: 'set null' }), // na akom projekte (voliteľné)
|
|
todoId: uuid('todo_id').references(() => todos.id, { onDelete: 'set null' }), // na akom todo (voliteľné)
|
|
companyId: uuid('company_id').references(() => companies.id, { onDelete: 'set null' }), // pre akú firmu (voliteľné)
|
|
startTime: timestamp('start_time').notNull(), // kedy začal trackovať
|
|
endTime: timestamp('end_time'), // kedy skončil (null ak ešte beží)
|
|
duration: integer('duration'), // trvanie v minútach
|
|
description: text('description'), // popis práce
|
|
isRunning: boolean('is_running').default(false).notNull(), // či práve beží
|
|
isEdited: boolean('is_edited').default(false).notNull(), // či bol editovaný
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
});
|