feat: AI Kurzy module, project/service documents, services SQL import
- Add AI Kurzy module with courses, participants, and registrations management - Add project documents and service documents features - Add service folders for document organization - Add SQL import queries for services from firmy.slovensko.ai - Update todo notifications and group messaging - Various API improvements and bug fixes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
193
src/db/migrations/0001_living_natasha_romanoff.sql
Normal file
193
src/db/migrations/0001_living_natasha_romanoff.sql
Normal file
@@ -0,0 +1,193 @@
|
||||
CREATE TYPE "public"."forma_kurzu_enum" AS ENUM('prezencne', 'online', 'hybridne');--> statement-breakpoint
|
||||
CREATE TYPE "public"."stav_registracie_enum" AS ENUM('potencialny', 'registrovany', 'potvrdeny', 'absolvoval', 'zruseny');--> statement-breakpoint
|
||||
CREATE TYPE "public"."typ_prilohy_enum" AS ENUM('certifikat', 'faktura', 'prihlaska', 'doklad_o_platbe', 'ine');--> statement-breakpoint
|
||||
CREATE TABLE "chat_group_members" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"group_id" uuid NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"joined_at" timestamp DEFAULT now() NOT NULL,
|
||||
"last_read_at" timestamp DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "chat_group_member_unique" UNIQUE("group_id","user_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "chat_groups" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"created_by_id" uuid,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "company_documents" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"company_id" uuid NOT NULL,
|
||||
"file_name" text NOT NULL,
|
||||
"original_name" text NOT NULL,
|
||||
"file_path" text NOT NULL,
|
||||
"file_type" text NOT NULL,
|
||||
"file_size" integer NOT NULL,
|
||||
"description" text,
|
||||
"uploaded_by" uuid,
|
||||
"uploaded_at" timestamp DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "email_signatures" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"full_name" text,
|
||||
"position" text,
|
||||
"phone" text,
|
||||
"email" text,
|
||||
"company_name" text,
|
||||
"website" text,
|
||||
"is_enabled" boolean DEFAULT true NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "email_signatures_user_id_unique" UNIQUE("user_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "group_messages" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"group_id" uuid NOT NULL,
|
||||
"sender_id" uuid,
|
||||
"content" text NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "kurzy" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"nazov" varchar(255) NOT NULL,
|
||||
"typ_kurzu" varchar(100) NOT NULL,
|
||||
"popis" text,
|
||||
"cena" numeric(10, 2) NOT NULL,
|
||||
"max_kapacita" integer,
|
||||
"aktivny" boolean DEFAULT true NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "prilohy" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"registracia_id" integer NOT NULL,
|
||||
"nazov_suboru" varchar(255) NOT NULL,
|
||||
"typ_prilohy" "typ_prilohy_enum" DEFAULT 'ine' NOT NULL,
|
||||
"cesta_k_suboru" varchar(500) NOT NULL,
|
||||
"mime_type" varchar(100),
|
||||
"velkost_suboru" bigint,
|
||||
"popis" text,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "project_documents" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"project_id" uuid NOT NULL,
|
||||
"file_name" text NOT NULL,
|
||||
"original_name" text NOT NULL,
|
||||
"file_path" text NOT NULL,
|
||||
"file_type" text NOT NULL,
|
||||
"file_size" integer NOT NULL,
|
||||
"description" text,
|
||||
"uploaded_by" uuid,
|
||||
"uploaded_at" timestamp DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "push_subscriptions" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"endpoint" text NOT NULL,
|
||||
"p256dh" text NOT NULL,
|
||||
"auth" text NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "push_subscription_endpoint_unique" UNIQUE("user_id","endpoint")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "registracie" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"kurz_id" integer NOT NULL,
|
||||
"ucastnik_id" integer NOT NULL,
|
||||
"datum_od" date,
|
||||
"datum_do" date,
|
||||
"forma_kurzu" "forma_kurzu_enum" DEFAULT 'prezencne' NOT NULL,
|
||||
"pocet_ucastnikov" integer DEFAULT 1 NOT NULL,
|
||||
"faktura_cislo" varchar(100),
|
||||
"faktura_vystavena" boolean DEFAULT false NOT NULL,
|
||||
"zaplatene" boolean DEFAULT false NOT NULL,
|
||||
"stav" "stav_registracie_enum" DEFAULT 'registrovany' NOT NULL,
|
||||
"poznamka" text,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "service_documents" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"folder_id" uuid NOT NULL,
|
||||
"file_name" text NOT NULL,
|
||||
"original_name" text NOT NULL,
|
||||
"file_path" text NOT NULL,
|
||||
"file_type" text NOT NULL,
|
||||
"file_size" integer NOT NULL,
|
||||
"description" text,
|
||||
"uploaded_by" uuid,
|
||||
"uploaded_at" timestamp DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "service_folders" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"created_by" uuid,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "services" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"price" text NOT NULL,
|
||||
"description" text,
|
||||
"created_by" uuid,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "ucastnici" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"titul" varchar(50),
|
||||
"meno" varchar(100) NOT NULL,
|
||||
"priezvisko" varchar(100) NOT NULL,
|
||||
"email" varchar(255) NOT NULL,
|
||||
"telefon" varchar(50),
|
||||
"firma" varchar(255),
|
||||
"mesto" varchar(100),
|
||||
"ulica" varchar(255),
|
||||
"psc" varchar(10),
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "ucastnici_email_unique" UNIQUE("email")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "personal_contacts" ALTER COLUMN "phone" DROP NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "todos" ADD COLUMN "completed_notified_at" timestamp;--> statement-breakpoint
|
||||
ALTER TABLE "chat_group_members" ADD CONSTRAINT "chat_group_members_group_id_chat_groups_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."chat_groups"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "chat_group_members" ADD CONSTRAINT "chat_group_members_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "chat_groups" ADD CONSTRAINT "chat_groups_created_by_id_users_id_fk" FOREIGN KEY ("created_by_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "company_documents" ADD CONSTRAINT "company_documents_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "company_documents" ADD CONSTRAINT "company_documents_uploaded_by_users_id_fk" FOREIGN KEY ("uploaded_by") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "email_signatures" ADD CONSTRAINT "email_signatures_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "group_messages" ADD CONSTRAINT "group_messages_group_id_chat_groups_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."chat_groups"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "group_messages" ADD CONSTRAINT "group_messages_sender_id_users_id_fk" FOREIGN KEY ("sender_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "prilohy" ADD CONSTRAINT "prilohy_registracia_id_registracie_id_fk" FOREIGN KEY ("registracia_id") REFERENCES "public"."registracie"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "project_documents" ADD CONSTRAINT "project_documents_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "project_documents" ADD CONSTRAINT "project_documents_uploaded_by_users_id_fk" FOREIGN KEY ("uploaded_by") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "push_subscriptions" ADD CONSTRAINT "push_subscriptions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "registracie" ADD CONSTRAINT "registracie_kurz_id_kurzy_id_fk" FOREIGN KEY ("kurz_id") REFERENCES "public"."kurzy"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "registracie" ADD CONSTRAINT "registracie_ucastnik_id_ucastnici_id_fk" FOREIGN KEY ("ucastnik_id") REFERENCES "public"."ucastnici"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "service_documents" ADD CONSTRAINT "service_documents_folder_id_service_folders_id_fk" FOREIGN KEY ("folder_id") REFERENCES "public"."service_folders"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "service_documents" ADD CONSTRAINT "service_documents_uploaded_by_users_id_fk" FOREIGN KEY ("uploaded_by") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "service_folders" ADD CONSTRAINT "service_folders_created_by_users_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "services" ADD CONSTRAINT "services_created_by_users_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "registracie_kurz_ucastnik_idx" ON "registracie" USING btree ("kurz_id","ucastnik_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "ucastnici_email_idx" ON "ucastnici" USING btree ("email");
|
||||
8
src/db/migrations/0003_add_group_last_read.sql
Normal file
8
src/db/migrations/0003_add_group_last_read.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
-- Add lastReadAt column to chat_group_members for tracking unread group messages
|
||||
ALTER TABLE chat_group_members
|
||||
ADD COLUMN IF NOT EXISTS last_read_at TIMESTAMP DEFAULT NOW() NOT NULL;
|
||||
|
||||
-- Update existing records to have current timestamp as lastReadAt
|
||||
UPDATE chat_group_members
|
||||
SET last_read_at = NOW()
|
||||
WHERE last_read_at IS NULL;
|
||||
17
src/db/migrations/0004_add_project_documents.sql
Normal file
17
src/db/migrations/0004_add_project_documents.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
-- Create project_documents table
|
||||
CREATE TABLE IF NOT EXISTS project_documents (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
file_name TEXT NOT NULL,
|
||||
original_name TEXT NOT NULL,
|
||||
file_path TEXT NOT NULL,
|
||||
file_type TEXT NOT NULL,
|
||||
file_size INTEGER NOT NULL,
|
||||
description TEXT,
|
||||
uploaded_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
uploaded_at TIMESTAMP DEFAULT NOW() NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT NOW() NOT NULL
|
||||
);
|
||||
|
||||
-- Create index for faster lookups
|
||||
CREATE INDEX IF NOT EXISTS idx_project_documents_project_id ON project_documents(project_id);
|
||||
26
src/db/migrations/0005_add_service_folders_documents.sql
Normal file
26
src/db/migrations/0005_add_service_folders_documents.sql
Normal file
@@ -0,0 +1,26 @@
|
||||
-- Create service_folders table
|
||||
CREATE TABLE IF NOT EXISTS service_folders (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL,
|
||||
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMP DEFAULT NOW() NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT NOW() NOT NULL
|
||||
);
|
||||
|
||||
-- Create service_documents table
|
||||
CREATE TABLE IF NOT EXISTS service_documents (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
folder_id UUID NOT NULL REFERENCES service_folders(id) ON DELETE CASCADE,
|
||||
file_name TEXT NOT NULL,
|
||||
original_name TEXT NOT NULL,
|
||||
file_path TEXT NOT NULL,
|
||||
file_type TEXT NOT NULL,
|
||||
file_size INTEGER NOT NULL,
|
||||
description TEXT,
|
||||
uploaded_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
uploaded_at TIMESTAMP DEFAULT NOW() NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT NOW() NOT NULL
|
||||
);
|
||||
|
||||
-- Create indexes for faster lookups
|
||||
CREATE INDEX IF NOT EXISTS idx_service_documents_folder_id ON service_documents(folder_id);
|
||||
88
src/db/migrations/0006_add_ai_kurzy.sql
Normal file
88
src/db/migrations/0006_add_ai_kurzy.sql
Normal file
@@ -0,0 +1,88 @@
|
||||
-- Create enums for AI Courses
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE forma_kurzu_enum AS ENUM ('prezencne', 'online', 'hybridne');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE stav_registracie_enum AS ENUM ('potencialny', 'registrovany', 'potvrdeny', 'absolvoval', 'zruseny');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE typ_prilohy_enum AS ENUM ('certifikat', 'faktura', 'prihlaska', 'doklad_o_platbe', 'ine');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
-- Create kurzy table (AI courses)
|
||||
CREATE TABLE IF NOT EXISTS kurzy (
|
||||
id SERIAL PRIMARY KEY,
|
||||
nazov VARCHAR(255) NOT NULL,
|
||||
typ_kurzu VARCHAR(100) NOT NULL,
|
||||
popis TEXT,
|
||||
cena NUMERIC(10, 2) NOT NULL,
|
||||
datum_od DATE NOT NULL,
|
||||
datum_do DATE NOT NULL,
|
||||
max_kapacita INTEGER,
|
||||
aktivny BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
|
||||
);
|
||||
|
||||
-- Create ucastnici table (participants)
|
||||
CREATE TABLE IF NOT EXISTS ucastnici (
|
||||
id SERIAL PRIMARY KEY,
|
||||
titul VARCHAR(50),
|
||||
meno VARCHAR(100) NOT NULL,
|
||||
priezvisko VARCHAR(100) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
telefon VARCHAR(50),
|
||||
firma VARCHAR(255),
|
||||
mesto VARCHAR(100),
|
||||
ulica VARCHAR(255),
|
||||
psc VARCHAR(10),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ucastnici_email_idx ON ucastnici(email);
|
||||
|
||||
-- Create registracie table (registrations)
|
||||
CREATE TABLE IF NOT EXISTS registracie (
|
||||
id SERIAL PRIMARY KEY,
|
||||
kurz_id INTEGER NOT NULL REFERENCES kurzy(id) ON DELETE CASCADE,
|
||||
ucastnik_id INTEGER NOT NULL REFERENCES ucastnici(id) ON DELETE CASCADE,
|
||||
forma_kurzu forma_kurzu_enum DEFAULT 'prezencne' NOT NULL,
|
||||
pocet_ucastnikov INTEGER DEFAULT 1 NOT NULL,
|
||||
faktura_cislo VARCHAR(100),
|
||||
faktura_vystavena BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
zaplatene BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
stav stav_registracie_enum DEFAULT 'registrovany' NOT NULL,
|
||||
poznamka TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS registracie_kurz_ucastnik_idx ON registracie(kurz_id, ucastnik_id);
|
||||
|
||||
-- Create prilohy table (attachments)
|
||||
CREATE TABLE IF NOT EXISTS prilohy (
|
||||
id SERIAL PRIMARY KEY,
|
||||
registracia_id INTEGER NOT NULL REFERENCES registracie(id) ON DELETE CASCADE,
|
||||
nazov_suboru VARCHAR(255) NOT NULL,
|
||||
typ_prilohy typ_prilohy_enum DEFAULT 'ine' NOT NULL,
|
||||
cesta_k_suboru VARCHAR(500) NOT NULL,
|
||||
mime_type VARCHAR(100),
|
||||
velkost_suboru BIGINT,
|
||||
popis TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
|
||||
);
|
||||
|
||||
-- Create indexes for faster lookups
|
||||
CREATE INDEX IF NOT EXISTS idx_registracie_kurz_id ON registracie(kurz_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_registracie_ucastnik_id ON registracie(ucastnik_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_prilohy_registracia_id ON prilohy(registracia_id);
|
||||
3
src/db/migrations/0007_add_completed_notified_at.sql
Normal file
3
src/db/migrations/0007_add_completed_notified_at.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
-- Add completed_notified_at column to todos table
|
||||
-- This tracks when the creator was notified about a todo completion
|
||||
ALTER TABLE todos ADD COLUMN IF NOT EXISTS completed_notified_at TIMESTAMP;
|
||||
3553
src/db/migrations/meta/0001_snapshot.json
Normal file
3553
src/db/migrations/meta/0001_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,13 @@
|
||||
"when": 1768469306890,
|
||||
"tag": "0000_fat_unus",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1768990516243,
|
||||
"tag": "0001_living_natasha_romanoff",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
115
src/db/schema.js
115
src/db/schema.js
@@ -1,4 +1,4 @@
|
||||
import { pgTable, text, timestamp, boolean, uuid, pgEnum, unique, integer } from 'drizzle-orm/pg-core';
|
||||
import { pgTable, text, timestamp, boolean, uuid, pgEnum, unique, integer, serial, varchar, numeric, date, bigint, uniqueIndex } from 'drizzle-orm/pg-core';
|
||||
|
||||
// Enums
|
||||
export const roleEnum = pgEnum('role', ['admin', 'member']);
|
||||
@@ -196,6 +196,7 @@ export const todos = pgTable('todos', {
|
||||
priority: todoPriorityEnum('priority').default('medium').notNull(),
|
||||
dueDate: timestamp('due_date'),
|
||||
completedAt: timestamp('completed_at'),
|
||||
completedNotifiedAt: timestamp('completed_notified_at'), // when creator was notified about completion
|
||||
createdBy: uuid('created_by').references(() => users.id, { onDelete: 'set null' }),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
@@ -299,6 +300,21 @@ export const companyDocuments = pgTable('company_documents', {
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// Project Documents table - dokumenty nahrané k projektu
|
||||
export const projectDocuments = pgTable('project_documents', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
projectId: uuid('project_id').references(() => projects.id, { onDelete: 'cascade' }).notNull(),
|
||||
fileName: text('file_name').notNull(), // unikátny názov súboru na disku
|
||||
originalName: text('original_name').notNull(), // pôvodný názov súboru
|
||||
filePath: text('file_path').notNull(), // cesta k súboru
|
||||
fileType: text('file_type').notNull(), // MIME typ
|
||||
fileSize: integer('file_size').notNull(), // veľkosť v bytoch
|
||||
description: text('description'),
|
||||
uploadedBy: uuid('uploaded_by').references(() => users.id, { onDelete: 'set null' }),
|
||||
uploadedAt: timestamp('uploaded_at').defaultNow().notNull(),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// Services table - služby ponúkané firmou
|
||||
export const services = pgTable('services', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
@@ -353,6 +369,7 @@ export const chatGroupMembers = pgTable('chat_group_members', {
|
||||
groupId: uuid('group_id').references(() => chatGroups.id, { onDelete: 'cascade' }).notNull(),
|
||||
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
||||
joinedAt: timestamp('joined_at').defaultNow().notNull(),
|
||||
lastReadAt: timestamp('last_read_at').defaultNow().notNull(), // Track when user last read group messages
|
||||
}, (table) => ({
|
||||
uniqueMember: unique('chat_group_member_unique').on(table.groupId, table.userId),
|
||||
}));
|
||||
@@ -377,3 +394,99 @@ export const pushSubscriptions = pgTable('push_subscriptions', {
|
||||
}, (table) => ({
|
||||
uniqueEndpoint: unique('push_subscription_endpoint_unique').on(table.userId, table.endpoint),
|
||||
}));
|
||||
|
||||
// Service Folders table - priečinky pre dokumenty služieb
|
||||
export const serviceFolders = pgTable('service_folders', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
name: text('name').notNull(),
|
||||
createdBy: uuid('created_by').references(() => users.id, { onDelete: 'set null' }),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// Service Documents table - dokumenty v priečinkoch služieb
|
||||
export const serviceDocuments = pgTable('service_documents', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
folderId: uuid('folder_id').references(() => serviceFolders.id, { onDelete: 'cascade' }).notNull(),
|
||||
fileName: text('file_name').notNull(),
|
||||
originalName: text('original_name').notNull(),
|
||||
filePath: text('file_path').notNull(),
|
||||
fileType: text('file_type').notNull(),
|
||||
fileSize: integer('file_size').notNull(),
|
||||
description: text('description'),
|
||||
uploadedBy: uuid('uploaded_by').references(() => users.id, { onDelete: 'set null' }),
|
||||
uploadedAt: timestamp('uploaded_at').defaultNow().notNull(),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// ==================== AI KURZY ====================
|
||||
|
||||
// Enums for AI Courses
|
||||
export const formaKurzuEnum = pgEnum('forma_kurzu_enum', ['prezencne', 'online', 'hybridne']);
|
||||
export const stavRegistracieEnum = pgEnum('stav_registracie_enum', ['potencialny', 'registrovany', 'potvrdeny', 'absolvoval', 'zruseny']);
|
||||
export const typPrilohyEnum = pgEnum('typ_prilohy_enum', ['certifikat', 'faktura', 'prihlaska', 'doklad_o_platbe', 'ine']);
|
||||
|
||||
// Kurzy table - AI courses definitions (templates)
|
||||
export const kurzy = pgTable('kurzy', {
|
||||
id: serial('id').primaryKey(),
|
||||
nazov: varchar('nazov', { length: 255 }).notNull(),
|
||||
typKurzu: varchar('typ_kurzu', { length: 100 }).notNull(),
|
||||
popis: text('popis'),
|
||||
cena: numeric('cena', { precision: 10, scale: 2 }).notNull(),
|
||||
maxKapacita: integer('max_kapacita'),
|
||||
aktivny: boolean('aktivny').default(true).notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// Ucastnici table - course participants
|
||||
export const ucastnici = pgTable('ucastnici', {
|
||||
id: serial('id').primaryKey(),
|
||||
titul: varchar('titul', { length: 50 }),
|
||||
meno: varchar('meno', { length: 100 }).notNull(),
|
||||
priezvisko: varchar('priezvisko', { length: 100 }).notNull(),
|
||||
email: varchar('email', { length: 255 }).notNull().unique(),
|
||||
telefon: varchar('telefon', { length: 50 }),
|
||||
firma: varchar('firma', { length: 255 }),
|
||||
mesto: varchar('mesto', { length: 100 }),
|
||||
ulica: varchar('ulica', { length: 255 }),
|
||||
psc: varchar('psc', { length: 10 }),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
}, (table) => ({
|
||||
emailIdx: uniqueIndex('ucastnici_email_idx').on(table.email),
|
||||
}));
|
||||
|
||||
// Registracie table - course registrations (many-to-many)
|
||||
export const registracie = pgTable('registracie', {
|
||||
id: serial('id').primaryKey(),
|
||||
kurzId: integer('kurz_id').notNull().references(() => kurzy.id, { onDelete: 'cascade' }),
|
||||
ucastnikId: integer('ucastnik_id').notNull().references(() => ucastnici.id, { onDelete: 'cascade' }),
|
||||
datumOd: date('datum_od', { mode: 'date' }), // dátum začiatku pre túto registráciu
|
||||
datumDo: date('datum_do', { mode: 'date' }), // dátum konca pre túto registráciu
|
||||
formaKurzu: formaKurzuEnum('forma_kurzu').default('prezencne').notNull(),
|
||||
pocetUcastnikov: integer('pocet_ucastnikov').default(1).notNull(),
|
||||
fakturaCislo: varchar('faktura_cislo', { length: 100 }),
|
||||
fakturaVystavena: boolean('faktura_vystavena').default(false).notNull(),
|
||||
zaplatene: boolean('zaplatene').default(false).notNull(),
|
||||
stav: stavRegistracieEnum('stav').default('registrovany').notNull(),
|
||||
poznamka: text('poznamka'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
}, (table) => ({
|
||||
uniqRegistracia: uniqueIndex('registracie_kurz_ucastnik_idx').on(table.kurzId, table.ucastnikId),
|
||||
}));
|
||||
|
||||
// Prilohy table - attachments for registrations
|
||||
export const prilohy = pgTable('prilohy', {
|
||||
id: serial('id').primaryKey(),
|
||||
registraciaId: integer('registracia_id').notNull().references(() => registracie.id, { onDelete: 'cascade' }),
|
||||
nazovSuboru: varchar('nazov_suboru', { length: 255 }).notNull(),
|
||||
typPrilohy: typPrilohyEnum('typ_prilohy').default('ine').notNull(),
|
||||
cestaKSuboru: varchar('cesta_k_suboru', { length: 500 }).notNull(),
|
||||
mimeType: varchar('mime_type', { length: 100 }),
|
||||
velkostSuboru: bigint('velkost_suboru', { mode: 'number' }),
|
||||
popis: text('popis'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
303
src/db/seeds/ai-kurzy-csv-import.seed.js
Normal file
303
src/db/seeds/ai-kurzy-csv-import.seed.js
Normal file
@@ -0,0 +1,303 @@
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
import { eq, sql } from 'drizzle-orm';
|
||||
|
||||
const { db } = await import('../../config/database.js');
|
||||
const { kurzy, ucastnici, registracie } = await import('../schema.js');
|
||||
|
||||
// Clear existing data
|
||||
async function clearData() {
|
||||
console.log('Clearing existing data...');
|
||||
await db.delete(registracie);
|
||||
await db.delete(ucastnici);
|
||||
await db.delete(kurzy);
|
||||
// Reset sequences
|
||||
await db.execute(sql`ALTER SEQUENCE kurzy_id_seq RESTART WITH 1`);
|
||||
await db.execute(sql`ALTER SEQUENCE ucastnici_id_seq RESTART WITH 1`);
|
||||
await db.execute(sql`ALTER SEQUENCE registracie_id_seq RESTART WITH 1`);
|
||||
console.log('Data cleared.');
|
||||
}
|
||||
|
||||
// Course data - now without dates (dates are per-registration)
|
||||
const coursesData = [
|
||||
{
|
||||
nazov: 'AI 1+2 (2 dni) - 290€',
|
||||
typKurzu: 'AI',
|
||||
cena: '290',
|
||||
},
|
||||
{
|
||||
nazov: 'AI 1 (1 deň) - 150€',
|
||||
typKurzu: 'AI',
|
||||
cena: '150',
|
||||
},
|
||||
{
|
||||
nazov: 'AI 2 (1 deň) - 150€',
|
||||
typKurzu: 'AI',
|
||||
cena: '150',
|
||||
},
|
||||
{
|
||||
nazov: 'AI v SEO (1 deň) - 150€',
|
||||
typKurzu: 'SEO',
|
||||
cena: '150',
|
||||
},
|
||||
{
|
||||
nazov: 'AI I+II Marec 2026',
|
||||
typKurzu: 'AI',
|
||||
cena: '290',
|
||||
},
|
||||
{
|
||||
nazov: 'AI I+II Apríl 2026',
|
||||
typKurzu: 'AI',
|
||||
cena: '290',
|
||||
},
|
||||
];
|
||||
|
||||
// Participants data from CSV - dates are now on registration level
|
||||
const participantsData = [
|
||||
// Umelá Inteligencia I+II 2. - 3. Február 2026
|
||||
{
|
||||
meno: 'Martin',
|
||||
priezvisko: 'Sovák',
|
||||
telefon: '0918986172',
|
||||
email: 'info@energium.sk',
|
||||
firma: 'energium sro',
|
||||
formaKurzu: 'prezencne',
|
||||
kurz: 'AI 1+2 (2 dni) - 290€',
|
||||
datumOd: new Date('2026-02-02'),
|
||||
datumDo: new Date('2026-02-03'),
|
||||
pocetUcastnikov: 1,
|
||||
mesto: 'Bratislava',
|
||||
ulica: 'Topolcianska 5',
|
||||
psc: '85105',
|
||||
fakturaVystavena: true,
|
||||
zaplatene: false,
|
||||
poznamka: 'FA 2026020',
|
||||
stav: 'registrovany',
|
||||
},
|
||||
{
|
||||
meno: 'Michal',
|
||||
priezvisko: 'Farkaš',
|
||||
telefon: '0911209122',
|
||||
email: 'michal.farkas83@gmail.com',
|
||||
firma: 'SLOVWELD',
|
||||
formaKurzu: 'online',
|
||||
kurz: 'AI 1 (1 deň) - 150€',
|
||||
datumOd: new Date('2026-02-02'),
|
||||
datumDo: new Date('2026-02-02'),
|
||||
pocetUcastnikov: 1,
|
||||
mesto: 'Dunajska Lužná',
|
||||
ulica: 'Mandlova 30',
|
||||
psc: '90042',
|
||||
fakturaVystavena: true,
|
||||
zaplatene: true,
|
||||
poznamka: 'Fa 2025 338, Súhlasil so zmeneným termínom',
|
||||
stav: 'registrovany',
|
||||
},
|
||||
{
|
||||
meno: 'Alena',
|
||||
priezvisko: 'Šranková',
|
||||
telefon: '0917352580',
|
||||
email: 'alena.srankova@gmail.com',
|
||||
formaKurzu: 'online',
|
||||
kurz: 'AI 1+2 (2 dni) - 290€',
|
||||
datumOd: new Date('2026-02-02'),
|
||||
datumDo: new Date('2026-02-03'),
|
||||
pocetUcastnikov: 1,
|
||||
mesto: 'Bratislava',
|
||||
ulica: 'Šándorova 1',
|
||||
psc: '82103',
|
||||
fakturaVystavena: true,
|
||||
zaplatene: true,
|
||||
stav: 'registrovany',
|
||||
},
|
||||
{
|
||||
meno: 'Katarina',
|
||||
priezvisko: 'Tomaníková',
|
||||
telefon: '0948 070 611',
|
||||
email: 'k.tomanikova@riseday.net',
|
||||
firma: 'Classica Shipping Limited',
|
||||
formaKurzu: 'prezencne',
|
||||
kurz: 'AI 1+2 (2 dni) - 290€',
|
||||
datumOd: new Date('2026-02-02'),
|
||||
datumDo: new Date('2026-02-03'),
|
||||
pocetUcastnikov: 1,
|
||||
mesto: 'Bratislava',
|
||||
ulica: 'Keltska 104',
|
||||
psc: '85110',
|
||||
fakturaVystavena: true,
|
||||
zaplatene: true,
|
||||
poznamka: 'presunuta z oktobra, chce až január',
|
||||
stav: 'registrovany',
|
||||
},
|
||||
{
|
||||
meno: 'Róbert',
|
||||
priezvisko: 'Brišák',
|
||||
telefon: '0910583883',
|
||||
email: 'robert.brisak@ss-nizna.sk',
|
||||
firma: 'Spojená škola, Hattalova 471, 02743 Nižná',
|
||||
formaKurzu: 'prezencne',
|
||||
kurz: 'AI 1+2 (2 dni) - 290€',
|
||||
datumOd: new Date('2026-02-02'),
|
||||
datumDo: new Date('2026-02-03'),
|
||||
pocetUcastnikov: 1,
|
||||
mesto: 'Nižná',
|
||||
ulica: 'Hattalova 471',
|
||||
psc: '02743',
|
||||
fakturaVystavena: true,
|
||||
zaplatene: false,
|
||||
poznamka: 'FA 2026019',
|
||||
stav: 'registrovany',
|
||||
},
|
||||
{
|
||||
meno: 'Marián',
|
||||
priezvisko: 'Bača',
|
||||
telefon: '0907994126',
|
||||
email: 'baca.marian@gmail.com',
|
||||
formaKurzu: 'prezencne',
|
||||
kurz: 'AI 2 (1 deň) - 150€',
|
||||
datumOd: new Date('2026-02-03'),
|
||||
datumDo: new Date('2026-02-03'),
|
||||
pocetUcastnikov: 1,
|
||||
mesto: 'Petrovany',
|
||||
ulica: '8',
|
||||
psc: '08253',
|
||||
fakturaVystavena: true,
|
||||
zaplatene: false,
|
||||
poznamka: 'Fa Gablasova',
|
||||
stav: 'registrovany',
|
||||
},
|
||||
{
|
||||
titul: 'Mgr. MBA',
|
||||
meno: 'Nikola',
|
||||
priezvisko: 'Horáčková',
|
||||
telefon: '0918482184',
|
||||
email: 'nikolahorackova11@gmail.com',
|
||||
kurz: 'AI 1+2 (2 dni) - 290€',
|
||||
datumOd: new Date('2026-02-02'),
|
||||
datumDo: new Date('2026-02-03'),
|
||||
pocetUcastnikov: 1,
|
||||
mesto: 'Zákopčie',
|
||||
ulica: 'Zákopčie stred 12',
|
||||
psc: '023 11',
|
||||
fakturaVystavena: false,
|
||||
zaplatene: false,
|
||||
poznamka: 'vzdelávací poukaz',
|
||||
stav: 'potencialny',
|
||||
},
|
||||
// AI v SEO 13.2.2026
|
||||
{
|
||||
meno: 'Tomáš',
|
||||
priezvisko: 'Kupec',
|
||||
telefon: '0911030190',
|
||||
email: 'kupec.tom@gmail.com',
|
||||
firma: 'Jamajka',
|
||||
formaKurzu: 'prezencne',
|
||||
kurz: 'AI v SEO (1 deň) - 150€',
|
||||
datumOd: new Date('2026-02-13'),
|
||||
datumDo: new Date('2026-02-13'),
|
||||
pocetUcastnikov: 1,
|
||||
mesto: 'Liptovská Sielnica',
|
||||
psc: '032 23',
|
||||
fakturaVystavena: true,
|
||||
zaplatene: false,
|
||||
poznamka: 'FA 2026021',
|
||||
stav: 'registrovany',
|
||||
},
|
||||
{
|
||||
meno: 'Anton',
|
||||
priezvisko: 'Považský',
|
||||
email: 'anton.povazsky@example.com', // No email in CSV, using placeholder
|
||||
formaKurzu: 'prezencne',
|
||||
kurz: 'AI v SEO (1 deň) - 150€',
|
||||
datumOd: new Date('2026-02-13'),
|
||||
datumDo: new Date('2026-02-13'),
|
||||
pocetUcastnikov: 1,
|
||||
fakturaVystavena: true,
|
||||
zaplatene: false,
|
||||
stav: 'registrovany',
|
||||
},
|
||||
];
|
||||
|
||||
async function importData() {
|
||||
console.log('Starting import...');
|
||||
|
||||
// Create courses (now without dates)
|
||||
console.log('\nCreating courses...');
|
||||
const createdKurzy = {};
|
||||
for (const course of coursesData) {
|
||||
const [created] = await db.insert(kurzy).values({
|
||||
nazov: course.nazov,
|
||||
typKurzu: course.typKurzu,
|
||||
cena: course.cena,
|
||||
aktivny: true,
|
||||
}).returning();
|
||||
createdKurzy[course.nazov] = created.id;
|
||||
console.log(` Created course: ${course.nazov} (ID: ${created.id})`);
|
||||
}
|
||||
|
||||
// Create participants and registrations (with dates)
|
||||
console.log('\nCreating participants and registrations...');
|
||||
for (const p of participantsData) {
|
||||
// Check if participant already exists by email
|
||||
let [existingUcastnik] = await db.select().from(ucastnici).where(eq(ucastnici.email, p.email)).limit(1);
|
||||
|
||||
let ucastnikId;
|
||||
if (existingUcastnik) {
|
||||
ucastnikId = existingUcastnik.id;
|
||||
console.log(` Using existing participant: ${p.email}`);
|
||||
} else {
|
||||
const [created] = await db.insert(ucastnici).values({
|
||||
titul: p.titul || null,
|
||||
meno: p.meno,
|
||||
priezvisko: p.priezvisko,
|
||||
email: p.email,
|
||||
telefon: p.telefon || null,
|
||||
firma: p.firma || null,
|
||||
mesto: p.mesto || null,
|
||||
ulica: p.ulica || null,
|
||||
psc: p.psc || null,
|
||||
}).returning();
|
||||
ucastnikId = created.id;
|
||||
console.log(` Created participant: ${p.meno} ${p.priezvisko} (${p.email})`);
|
||||
}
|
||||
|
||||
// Get kurz ID
|
||||
const kurzId = createdKurzy[p.kurz];
|
||||
if (!kurzId) {
|
||||
console.error(` ERROR: Course not found: ${p.kurz}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create registration with dates
|
||||
await db.insert(registracie).values({
|
||||
kurzId: kurzId,
|
||||
ucastnikId: ucastnikId,
|
||||
datumOd: p.datumOd || null,
|
||||
datumDo: p.datumDo || null,
|
||||
formaKurzu: p.formaKurzu || 'prezencne',
|
||||
pocetUcastnikov: p.pocetUcastnikov || 1,
|
||||
fakturaVystavena: p.fakturaVystavena || false,
|
||||
zaplatene: p.zaplatene || false,
|
||||
stav: p.stav || 'registrovany',
|
||||
poznamka: p.poznamka || null,
|
||||
});
|
||||
console.log(` Created registration for ${p.email} -> ${p.kurz} (${p.datumOd?.toLocaleDateString('sk-SK')} - ${p.datumDo?.toLocaleDateString('sk-SK')})`);
|
||||
}
|
||||
|
||||
console.log('\n=== Import completed ===');
|
||||
console.log(`Courses: ${coursesData.length}`);
|
||||
console.log(`Participants: ${participantsData.length}`);
|
||||
}
|
||||
|
||||
// Run
|
||||
clearData()
|
||||
.then(() => importData())
|
||||
.then(() => {
|
||||
console.log('Done!');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Import failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
292
src/db/seeds/ai-kurzy-import.seed.js
Normal file
292
src/db/seeds/ai-kurzy-import.seed.js
Normal file
@@ -0,0 +1,292 @@
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
import ExcelJS from 'exceljs';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import path from 'path';
|
||||
|
||||
// Dynamic imports to ensure env is loaded first
|
||||
const { db } = await import('../../config/database.js');
|
||||
const { kurzy, ucastnici, registracie } = await import('../schema.js');
|
||||
|
||||
const EXCEL_FILE = '/home/richardtekula/Downloads/Copy of AI školenie študenti.xlsx';
|
||||
|
||||
// Helper to parse dates from various formats
|
||||
const parseDate = (value) => {
|
||||
if (!value) return null;
|
||||
if (value instanceof Date) return value;
|
||||
if (typeof value === 'number') {
|
||||
// Excel serial date number
|
||||
const date = new Date((value - 25569) * 86400 * 1000);
|
||||
return date;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const parsed = new Date(value);
|
||||
return isNaN(parsed.getTime()) ? null : parsed;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Helper to clean string values
|
||||
const cleanString = (value) => {
|
||||
if (value === null || value === undefined) return null;
|
||||
const str = String(value).trim();
|
||||
return str === '' ? null : str;
|
||||
};
|
||||
|
||||
// Helper to parse numeric value
|
||||
const parseNumber = (value) => {
|
||||
if (value === null || value === undefined) return null;
|
||||
const num = parseFloat(value);
|
||||
return isNaN(num) ? null : num;
|
||||
};
|
||||
|
||||
// Map stav from Excel to our enum values
|
||||
const mapStav = (value) => {
|
||||
if (!value) return 'registrovany';
|
||||
const v = String(value).toLowerCase().trim();
|
||||
if (v.includes('absolvoval')) return 'absolvoval';
|
||||
if (v.includes('potvrden')) return 'potvrdeny';
|
||||
if (v.includes('zrusen')) return 'zruseny';
|
||||
if (v.includes('potencial')) return 'potencialny';
|
||||
return 'registrovany';
|
||||
};
|
||||
|
||||
// Map forma kurzu
|
||||
const mapForma = (value) => {
|
||||
if (!value) return 'prezencne';
|
||||
const v = String(value).toLowerCase().trim();
|
||||
if (v.includes('online')) return 'online';
|
||||
if (v.includes('hybrid')) return 'hybridne';
|
||||
return 'prezencne';
|
||||
};
|
||||
|
||||
async function importAiKurzy() {
|
||||
console.log('Reading Excel file:', EXCEL_FILE);
|
||||
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
await workbook.xlsx.readFile(EXCEL_FILE);
|
||||
|
||||
console.log('Sheets in workbook:', workbook.worksheets.map(ws => ws.name));
|
||||
|
||||
// Process each sheet
|
||||
for (const worksheet of workbook.worksheets) {
|
||||
console.log(`\n=== Processing sheet: ${worksheet.name} ===`);
|
||||
console.log(`Rows: ${worksheet.rowCount}, Columns: ${worksheet.columnCount}`);
|
||||
|
||||
// Get headers from first row
|
||||
const headerRow = worksheet.getRow(1);
|
||||
const headers = [];
|
||||
headerRow.eachCell((cell, colNum) => {
|
||||
headers[colNum] = cleanString(cell.value);
|
||||
});
|
||||
console.log('Headers:', headers.filter(Boolean));
|
||||
|
||||
// Collect data rows
|
||||
const dataRows = [];
|
||||
worksheet.eachRow((row, rowNum) => {
|
||||
if (rowNum === 1) return; // Skip header
|
||||
|
||||
const rowData = {};
|
||||
row.eachCell((cell, colNum) => {
|
||||
const header = headers[colNum];
|
||||
if (header) {
|
||||
rowData[header] = cell.value;
|
||||
}
|
||||
});
|
||||
|
||||
// Only add if row has some data
|
||||
if (Object.values(rowData).some(v => v !== null && v !== undefined && v !== '')) {
|
||||
dataRows.push(rowData);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Found ${dataRows.length} data rows`);
|
||||
|
||||
// Log first few rows to understand structure
|
||||
if (dataRows.length > 0) {
|
||||
console.log('Sample row:', JSON.stringify(dataRows[0], null, 2));
|
||||
}
|
||||
|
||||
// Try to import data based on headers
|
||||
await importSheetData(worksheet.name, headers, dataRows);
|
||||
}
|
||||
|
||||
console.log('\n=== Import completed ===');
|
||||
}
|
||||
|
||||
async function importSheetData(sheetName, headers, rows) {
|
||||
// Detect what kind of data this is based on headers
|
||||
const headerLower = headers.map(h => h?.toLowerCase() || '');
|
||||
|
||||
const hasKurzFields = headerLower.some(h => h.includes('kurz') || h.includes('datum') || h.includes('cena'));
|
||||
const hasUcastnikFields = headerLower.some(h => h.includes('meno') || h.includes('email') || h.includes('priezvisko'));
|
||||
|
||||
if (rows.length === 0) {
|
||||
console.log('No data to import');
|
||||
return;
|
||||
}
|
||||
|
||||
// Import participants and registrations
|
||||
if (hasUcastnikFields) {
|
||||
await importParticipantsAndRegistrations(sheetName, headers, rows);
|
||||
}
|
||||
}
|
||||
|
||||
async function importParticipantsAndRegistrations(sheetName, headers, rows) {
|
||||
console.log(`\nImporting participants from sheet: ${sheetName}`);
|
||||
|
||||
// First, ensure we have a course for this sheet
|
||||
const courseName = sheetName;
|
||||
let course = await db.select().from(kurzy).where(eq(kurzy.nazov, courseName)).limit(1);
|
||||
|
||||
if (course.length === 0) {
|
||||
// Create course from sheet name
|
||||
const [newCourse] = await db.insert(kurzy).values({
|
||||
nazov: courseName,
|
||||
typKurzu: extractCourseType(sheetName),
|
||||
cena: '0', // Will need to update manually
|
||||
datumOd: new Date(),
|
||||
datumDo: new Date(),
|
||||
aktivny: true,
|
||||
}).returning();
|
||||
course = [newCourse];
|
||||
console.log(`Created course: ${courseName} (ID: ${newCourse.id})`);
|
||||
} else {
|
||||
console.log(`Using existing course: ${courseName} (ID: ${course[0].id})`);
|
||||
}
|
||||
|
||||
const kurzId = course[0].id;
|
||||
|
||||
// Map headers to our fields
|
||||
const headerMap = {};
|
||||
headers.forEach((header, idx) => {
|
||||
if (!header) return;
|
||||
const h = header.toLowerCase();
|
||||
|
||||
if (h.includes('titul') || h === 'titul') headerMap.titul = idx;
|
||||
if (h.includes('meno') && !h.includes('priezvisko')) headerMap.meno = idx;
|
||||
if (h.includes('priezvisko') || h === 'surname' || h === 'priezvisko') headerMap.priezvisko = idx;
|
||||
if (h.includes('email') || h.includes('e-mail')) headerMap.email = idx;
|
||||
if (h.includes('telefon') || h.includes('phone') || h.includes('tel')) headerMap.telefon = idx;
|
||||
if (h.includes('firma') || h.includes('company') || h.includes('spolocnost')) headerMap.firma = idx;
|
||||
if (h.includes('mesto') || h.includes('city')) headerMap.mesto = idx;
|
||||
if (h.includes('ulica') || h.includes('street') || h.includes('adresa')) headerMap.ulica = idx;
|
||||
if (h.includes('psc') || h.includes('zip') || h.includes('postal')) headerMap.psc = idx;
|
||||
if (h.includes('stav') || h.includes('status')) headerMap.stav = idx;
|
||||
if (h.includes('forma') || h.includes('form')) headerMap.forma = idx;
|
||||
if (h.includes('faktur') && h.includes('cislo')) headerMap.fakturaCislo = idx;
|
||||
if (h.includes('faktur') && h.includes('vystaven')) headerMap.fakturaVystavena = idx;
|
||||
if (h.includes('zaplaten') || h.includes('paid')) headerMap.zaplatene = idx;
|
||||
if (h.includes('poznam') || h.includes('note')) headerMap.poznamka = idx;
|
||||
if (h.includes('pocet') || h.includes('count')) headerMap.pocetUcastnikov = idx;
|
||||
});
|
||||
|
||||
console.log('Field mapping:', headerMap);
|
||||
|
||||
let importedCount = 0;
|
||||
let skippedCount = 0;
|
||||
|
||||
for (const row of rows) {
|
||||
try {
|
||||
// Get email - required field
|
||||
const email = cleanString(row[headers[headerMap.email]] || Object.values(row).find(v => String(v).includes('@')));
|
||||
if (!email || !email.includes('@')) {
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if participant exists
|
||||
let participant = await db.select().from(ucastnici).where(eq(ucastnici.email, email)).limit(1);
|
||||
|
||||
if (participant.length === 0) {
|
||||
// Try to find name fields
|
||||
let meno = cleanString(row[headers[headerMap.meno]]);
|
||||
let priezvisko = cleanString(row[headers[headerMap.priezvisko]]);
|
||||
|
||||
// If no separate fields, try to split full name
|
||||
if (!meno && !priezvisko) {
|
||||
// Look for a name-like field
|
||||
for (const [key, value] of Object.entries(row)) {
|
||||
const val = cleanString(value);
|
||||
if (val && !val.includes('@') && !val.includes('http') && val.length < 50) {
|
||||
const parts = val.split(/\s+/);
|
||||
if (parts.length >= 2) {
|
||||
meno = parts[0];
|
||||
priezvisko = parts.slice(1).join(' ');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create participant
|
||||
const [newParticipant] = await db.insert(ucastnici).values({
|
||||
titul: cleanString(row[headers[headerMap.titul]]),
|
||||
meno: meno || 'N/A',
|
||||
priezvisko: priezvisko || 'N/A',
|
||||
email: email,
|
||||
telefon: cleanString(row[headers[headerMap.telefon]]),
|
||||
firma: cleanString(row[headers[headerMap.firma]]),
|
||||
mesto: cleanString(row[headers[headerMap.mesto]]),
|
||||
ulica: cleanString(row[headers[headerMap.ulica]]),
|
||||
psc: cleanString(row[headers[headerMap.psc]]),
|
||||
}).returning();
|
||||
participant = [newParticipant];
|
||||
console.log(`Created participant: ${email}`);
|
||||
}
|
||||
|
||||
const ucastnikId = participant[0].id;
|
||||
|
||||
// Check if registration exists
|
||||
const existingReg = await db.select()
|
||||
.from(registracie)
|
||||
.where(and(eq(registracie.kurzId, kurzId), eq(registracie.ucastnikId, ucastnikId)))
|
||||
.limit(1);
|
||||
|
||||
if (existingReg.length === 0) {
|
||||
// Create registration
|
||||
await db.insert(registracie).values({
|
||||
kurzId: kurzId,
|
||||
ucastnikId: ucastnikId,
|
||||
formaKurzu: mapForma(row[headers[headerMap.forma]]),
|
||||
pocetUcastnikov: parseInt(row[headers[headerMap.pocetUcastnikov]]) || 1,
|
||||
fakturaCislo: cleanString(row[headers[headerMap.fakturaCislo]]),
|
||||
fakturaVystavena: Boolean(row[headers[headerMap.fakturaVystavena]]),
|
||||
zaplatene: Boolean(row[headers[headerMap.zaplatene]]),
|
||||
stav: mapStav(row[headers[headerMap.stav]]),
|
||||
poznamka: cleanString(row[headers[headerMap.poznamka]]),
|
||||
});
|
||||
importedCount++;
|
||||
} else {
|
||||
console.log(`Registration already exists for ${email} in ${sheetName}`);
|
||||
skippedCount++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error processing row:`, error.message);
|
||||
skippedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Imported ${importedCount} registrations, skipped ${skippedCount}`);
|
||||
}
|
||||
|
||||
function extractCourseType(sheetName) {
|
||||
const name = sheetName.toLowerCase();
|
||||
if (name.includes('ai 1') || name.includes('ai1')) return 'AI 1';
|
||||
if (name.includes('ai 2') || name.includes('ai2')) return 'AI 2';
|
||||
if (name.includes('seo')) return 'SEO';
|
||||
if (name.includes('marketing')) return 'Marketing';
|
||||
return 'AI';
|
||||
}
|
||||
|
||||
// Run the import
|
||||
importAiKurzy()
|
||||
.then(() => {
|
||||
console.log('Import finished successfully');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Import failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user