Initial commit: Helpdesk application setup

- Backend: Node.js/TypeScript with Prisma ORM
- Frontend: Vite + TypeScript
- Project configuration and documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-03 08:53:22 +01:00
commit e4f63a135e
103 changed files with 19913 additions and 0 deletions

View File

@@ -0,0 +1,709 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// ==================== USER ROLES (dynamicke) ====================
model UserRole {
id String @id @default(cuid())
code String @unique
name String
description String?
permissions Json
level Int
order Int @default(0)
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
users User[]
@@index([active])
@@index([level])
}
// ==================== USERS ====================
model User {
id String @id @default(cuid())
email String @unique
password String
name String
roleId String
role UserRole @relation(fields: [roleId], references: [id])
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
ownedProjects Project[] @relation("ProjectOwner")
assignedProjects ProjectMember[]
createdTasks Task[] @relation("TaskCreator")
assignedTasks TaskAssignee[]
reminders Reminder[]
activityLogs ActivityLog[]
createdEquipment Equipment[] @relation("EquipmentCreator")
performedRevisions Revision[]
uploadedEquipmentFiles EquipmentAttachment[]
assignedRMAs RMA[] @relation("RMAAssignee")
createdRMAs RMA[] @relation("RMACreator")
approvedRMAs RMA[] @relation("RMAApprover")
rmaAttachments RMAAttachment[]
rmaStatusChanges RMAStatusHistory[]
rmaComments RMAComment[]
taskComments Comment[]
createdCustomers Customer[]
@@index([email])
@@index([roleId])
@@index([active])
}
// ==================== CONFIGURATION TABLES ====================
model EquipmentType {
id String @id @default(cuid())
code String @unique
name String
description String?
color String?
icon String?
order Int @default(0)
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
equipment Equipment[]
@@index([active])
@@index([order])
}
model RevisionType {
id String @id @default(cuid())
code String @unique
name String
intervalDays Int
reminderDays Int @default(14)
color String?
description String?
order Int @default(0)
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
revisions Revision[]
@@index([active])
@@index([order])
}
model RMAStatus {
id String @id @default(cuid())
code String @unique
name String
description String?
color String?
icon String?
order Int @default(0)
isInitial Boolean @default(false)
isFinal Boolean @default(false)
canTransitionTo Json?
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
rmas RMA[]
@@index([active])
@@index([order])
@@index([isInitial])
@@index([isFinal])
}
model RMASolution {
id String @id @default(cuid())
code String @unique
name String
description String?
color String?
order Int @default(0)
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
rmas RMA[]
@@index([active])
@@index([order])
}
model TaskStatus {
id String @id @default(cuid())
code String @unique
name String
description String?
color String?
icon String?
order Int @default(0)
swimlaneColumn String?
isInitial Boolean @default(false)
isFinal Boolean @default(false)
canTransitionTo Json?
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tasks Task[]
projects Project[]
@@index([active])
@@index([swimlaneColumn])
@@index([order])
}
model Priority {
id String @id @default(cuid())
code String @unique
name String
description String?
color String?
icon String?
level Int
order Int @default(0)
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tasks Task[]
@@index([active])
@@index([level])
@@index([order])
}
model Tag {
id String @id @default(cuid())
code String @unique
name String
description String?
color String?
entityType String
order Int @default(0)
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
projectTags ProjectTag[]
taskTags TaskTag[]
equipmentTags EquipmentTag[]
rmaTags RMATag[]
@@index([entityType])
@@index([active])
}
model SystemSetting {
id String @id @default(cuid())
key String @unique
value Json
category String
label String
description String?
dataType String
validation Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([category])
}
// ==================== CUSTOMERS ====================
model Customer {
id String @id @default(cuid())
name String
address String?
email String?
phone String?
ico String?
dic String?
icdph String?
contactPerson String?
contactEmail String?
contactPhone String?
externalId String? @unique
externalSource String?
notes String?
active Boolean @default(true)
createdById String
createdBy User @relation(fields: [createdById], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
projects Project[]
equipment Equipment[]
rmas RMA[]
@@index([name])
@@index([ico])
@@index([externalId])
@@index([active])
}
// ==================== PROJECTS ====================
model Project {
id String @id @default(cuid())
name String
description String?
customerId String?
customer Customer? @relation(fields: [customerId], references: [id])
ownerId String
owner User @relation("ProjectOwner", fields: [ownerId], references: [id])
statusId String
status TaskStatus @relation(fields: [statusId], references: [id])
softDeadline DateTime?
hardDeadline DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
completedAt DateTime?
tasks Task[]
members ProjectMember[]
tags ProjectTag[]
@@index([ownerId])
@@index([statusId])
@@index([customerId])
@@index([hardDeadline])
}
model ProjectMember {
id String @id @default(cuid())
projectId String
userId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
addedAt DateTime @default(now())
@@unique([projectId, userId])
@@index([projectId])
@@index([userId])
}
model ProjectTag {
projectId String
tagId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@id([projectId, tagId])
}
// ==================== TASKS ====================
model Task {
id String @id @default(cuid())
title String
description String?
projectId String?
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
parentId String?
parent Task? @relation("SubTasks", fields: [parentId], references: [id], onDelete: Cascade)
subTasks Task[] @relation("SubTasks")
statusId String
status TaskStatus @relation(fields: [statusId], references: [id])
priorityId String
priority Priority @relation(fields: [priorityId], references: [id])
deadline DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
completedAt DateTime?
createdById String
createdBy User @relation("TaskCreator", fields: [createdById], references: [id])
assignees TaskAssignee[]
reminders Reminder[]
comments Comment[]
tags TaskTag[]
@@index([projectId])
@@index([parentId])
@@index([statusId])
@@index([priorityId])
@@index([deadline])
@@index([createdById])
}
model TaskAssignee {
id String @id @default(cuid())
taskId String
userId String
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
assignedAt DateTime @default(now())
@@unique([taskId, userId])
@@index([taskId])
@@index([userId])
}
model TaskTag {
taskId String
tagId String
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@id([taskId, tagId])
}
model Reminder {
id String @id @default(cuid())
taskId String
userId String
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
remindAt DateTime
snoozedUntil DateTime?
dismissed Boolean @default(false)
message String?
createdAt DateTime @default(now())
@@index([userId, remindAt])
@@index([taskId])
}
model Comment {
id String @id @default(cuid())
taskId String
userId String
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id])
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([taskId])
@@index([userId])
@@index([createdAt])
}
// ==================== EQUIPMENT MANAGEMENT ====================
model Equipment {
id String @id @default(cuid())
name String
typeId String
type EquipmentType @relation(fields: [typeId], references: [id])
brand String?
model String?
customerId String?
customer Customer? @relation(fields: [customerId], references: [id])
address String
location String?
partNumber String?
serialNumber String?
installDate DateTime?
warrantyEnd DateTime?
warrantyStatus String?
description String?
notes String?
active Boolean @default(true)
createdById String
createdBy User @relation("EquipmentCreator", fields: [createdById], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
revisions Revision[]
attachments EquipmentAttachment[]
tags EquipmentTag[]
@@index([typeId])
@@index([customerId])
@@index([warrantyEnd])
@@index([active])
@@index([createdById])
}
model Revision {
id String @id @default(cuid())
equipmentId String
equipment Equipment @relation(fields: [equipmentId], references: [id], onDelete: Cascade)
typeId String
type RevisionType @relation(fields: [typeId], references: [id])
performedDate DateTime
nextDueDate DateTime?
performedById String
performedBy User @relation(fields: [performedById], references: [id])
findings String?
result String?
notes String?
reminderSent Boolean @default(false)
reminderDate DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([equipmentId])
@@index([typeId])
@@index([performedById])
@@index([nextDueDate])
@@index([reminderDate])
}
model EquipmentAttachment {
id String @id @default(cuid())
equipmentId String
equipment Equipment @relation(fields: [equipmentId], references: [id], onDelete: Cascade)
filename String
filepath String
mimetype String
size Int
uploadedById String
uploadedBy User @relation(fields: [uploadedById], references: [id])
uploadedAt DateTime @default(now())
@@index([equipmentId])
}
model EquipmentTag {
equipmentId String
tagId String
equipment Equipment @relation(fields: [equipmentId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@id([equipmentId, tagId])
}
// ==================== RMA (REKLAMACIE) ====================
model RMA {
id String @id @default(cuid())
rmaNumber String @unique
customerId String?
customer Customer? @relation(fields: [customerId], references: [id])
customerName String?
customerAddress String?
customerEmail String?
customerPhone String?
customerICO String?
submittedBy String
productName String
invoiceNumber String?
purchaseDate DateTime?
productNumber String?
serialNumber String?
accessories String?
issueDescription String
statusId String
status RMAStatus @relation(fields: [statusId], references: [id])
proposedSolutionId String?
proposedSolution RMASolution? @relation(fields: [proposedSolutionId], references: [id])
requiresApproval Boolean @default(false)
approvedById String?
approvedBy User? @relation("RMAApprover", fields: [approvedById], references: [id])
approvedAt DateTime?
receivedDate DateTime?
receivedLocation String?
internalNotes String?
resolutionDate DateTime?
resolutionNotes String?
assignedToId String?
assignedTo User? @relation("RMAAssignee", fields: [assignedToId], references: [id])
createdById String
createdBy User @relation("RMACreator", fields: [createdById], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
closedAt DateTime?
attachments RMAAttachment[]
statusHistory RMAStatusHistory[]
comments RMAComment[]
tags RMATag[]
@@index([rmaNumber])
@@index([customerId])
@@index([statusId])
@@index([proposedSolutionId])
@@index([assignedToId])
@@index([createdById])
@@index([purchaseDate])
@@index([receivedDate])
}
model RMAAttachment {
id String @id @default(cuid())
rmaId String
rma RMA @relation(fields: [rmaId], references: [id], onDelete: Cascade)
filename String
filepath String
mimetype String
size Int
uploadedById String
uploadedBy User @relation(fields: [uploadedById], references: [id])
uploadedAt DateTime @default(now())
@@index([rmaId])
}
model RMAStatusHistory {
id String @id @default(cuid())
rmaId String
rma RMA @relation(fields: [rmaId], references: [id], onDelete: Cascade)
fromStatusId String?
toStatusId String
changedById String
changedBy User @relation(fields: [changedById], references: [id])
notes String?
changedAt DateTime @default(now())
@@index([rmaId])
@@index([changedAt])
}
model RMAComment {
id String @id @default(cuid())
rmaId String
rma RMA @relation(fields: [rmaId], references: [id], onDelete: Cascade)
content String
userId String
user User @relation(fields: [userId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([rmaId])
@@index([createdAt])
}
model RMATag {
rmaId String
tagId String
rma RMA @relation(fields: [rmaId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@id([rmaId, tagId])
}
// ==================== ACTIVITY LOG ====================
model ActivityLog {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id])
action String
entity String
entityId String
changes Json?
ipAddress String?
userAgent String?
createdAt DateTime @default(now())
@@index([userId])
@@index([entity, entityId])
@@index([createdAt])
}

260
backend/prisma/seed.ts Normal file
View File

@@ -0,0 +1,260 @@
import { PrismaClient } from '@prisma/client';
import bcrypt from 'bcryptjs';
const prisma = new PrismaClient();
async function seed() {
console.log('Seeding database...');
// ===== USER ROLES =====
console.log('Creating user roles...');
const roles = await Promise.all([
prisma.userRole.upsert({
where: { code: 'ROOT' },
update: {},
create: {
code: 'ROOT',
name: 'Root Správca',
level: 1,
order: 1,
permissions: {
projects: ['*'],
tasks: ['*'],
equipment: ['*'],
rma: ['*'],
customers: ['*'],
settings: ['*'],
users: ['*'],
logs: ['*'],
},
},
}),
prisma.userRole.upsert({
where: { code: 'ADMIN' },
update: {},
create: {
code: 'ADMIN',
name: 'Administrátor',
level: 2,
order: 2,
permissions: {
projects: ['create', 'read', 'update', 'delete', 'all'],
tasks: ['create', 'read', 'update', 'delete', 'all'],
equipment: ['create', 'read', 'update', 'delete', 'all'],
rma: ['create', 'read', 'update', 'delete', 'approve'],
customers: ['create', 'read', 'update', 'delete'],
users: ['read'],
},
},
}),
prisma.userRole.upsert({
where: { code: 'USER' },
update: {},
create: {
code: 'USER',
name: 'Používateľ',
level: 3,
order: 3,
permissions: {
projects: ['read', 'update'],
tasks: ['create', 'read', 'update'],
equipment: ['read', 'update'],
rma: ['create', 'read', 'update'],
customers: ['read'],
},
},
}),
prisma.userRole.upsert({
where: { code: 'CUSTOMER' },
update: {},
create: {
code: 'CUSTOMER',
name: 'Zákazník',
level: 4,
order: 4,
permissions: {
projects: ['read'],
tasks: ['read'],
equipment: ['read'],
rma: ['create', 'read'],
},
},
}),
]);
// ===== EQUIPMENT TYPES =====
console.log('Creating equipment types...');
await prisma.equipmentType.createMany({
skipDuplicates: true,
data: [
{ code: 'EPS', name: 'Elektrická požiarna signalizácia', color: '#3B82F6', order: 1 },
{ code: 'HSP', name: 'Hasiaci systém', color: '#EF4444', order: 2 },
{ code: 'CAMERA', name: 'Kamerový systém', color: '#10B981', order: 3 },
{ code: 'ACCESS', name: 'Prístupový systém', color: '#F59E0B', order: 4 },
{ code: 'OTHER', name: 'Iné zariadenie', color: '#6B7280', order: 5 },
],
});
// ===== REVISION TYPES =====
console.log('Creating revision types...');
await prisma.revisionType.createMany({
skipDuplicates: true,
data: [
{ code: 'QUARTERLY', name: 'Štvrťročná revízia', intervalDays: 90, reminderDays: 14, color: '#FFA500', order: 1 },
{ code: 'BIANNUAL', name: 'Polročná revízia', intervalDays: 180, reminderDays: 21, color: '#FBBF24', order: 2 },
{ code: 'ANNUAL', name: 'Ročná revízia', intervalDays: 365, reminderDays: 30, color: '#DC2626', order: 3 },
{ code: 'EMERGENCY', name: 'Mimoriadna revízia', intervalDays: 0, reminderDays: 0, color: '#DC2626', order: 4 },
],
});
// ===== RMA STATUSES =====
console.log('Creating RMA statuses...');
await prisma.rMAStatus.createMany({
skipDuplicates: true,
data: [
{ code: 'NEW', name: 'Nová reklamácia', color: '#10B981', isInitial: true, canTransitionTo: ['IN_ASSESSMENT', 'REJECTED'], order: 1 },
{ code: 'IN_ASSESSMENT', name: 'V posúdzovaní', color: '#F59E0B', canTransitionTo: ['APPROVED', 'REJECTED'], order: 2 },
{ code: 'APPROVED', name: 'Schválená', color: '#3B82F6', canTransitionTo: ['IN_REPAIR', 'REPLACED', 'REFUNDED'], order: 3 },
{ code: 'REJECTED', name: 'Zamietnutá', color: '#EF4444', isFinal: true, order: 4 },
{ code: 'IN_REPAIR', name: 'V oprave', color: '#8B5CF6', canTransitionTo: ['REPAIRED', 'COMPLETED'], order: 5 },
{ code: 'REPAIRED', name: 'Opravené', color: '#059669', canTransitionTo: ['COMPLETED'], order: 6 },
{ code: 'REPLACED', name: 'Vymenené', color: '#059669', canTransitionTo: ['COMPLETED'], order: 7 },
{ code: 'REFUNDED', name: 'Vrátené peniaze', color: '#059669', canTransitionTo: ['COMPLETED'], order: 8 },
{ code: 'COMPLETED', name: 'Uzatvorená', color: '#059669', isFinal: true, order: 9 },
],
});
// ===== RMA SOLUTIONS =====
console.log('Creating RMA solutions...');
await prisma.rMASolution.createMany({
skipDuplicates: true,
data: [
{ code: 'ASSESSMENT', name: 'Posúdzovanie', color: '#F59E0B', order: 1 },
{ code: 'REPAIR', name: 'Oprava', color: '#3B82F6', order: 2 },
{ code: 'REPLACEMENT', name: 'Výmena', color: '#10B981', order: 3 },
{ code: 'REFUND', name: 'Vrátenie peňazí', color: '#8B5CF6', order: 4 },
{ code: 'REJECTED', name: 'Zamietnutie', color: '#EF4444', order: 5 },
{ code: 'OTHER', name: 'Iné riešenie', color: '#6B7280', order: 6 },
],
});
// ===== TASK STATUSES =====
console.log('Creating task statuses...');
await prisma.taskStatus.createMany({
skipDuplicates: true,
data: [
{ code: 'NEW', name: 'Nová úloha', swimlaneColumn: 'NEW', color: '#10B981', isInitial: true, order: 1 },
{ code: 'IN_PROGRESS', name: 'V riešení', swimlaneColumn: 'DOING', color: '#F59E0B', order: 2 },
{ code: 'REVIEW', name: 'Na kontrolu', swimlaneColumn: 'DOING', color: '#8B5CF6', order: 3 },
{ code: 'COMPLETED', name: 'Dokončená', swimlaneColumn: 'DONE', color: '#059669', isFinal: true, order: 4 },
],
});
// ===== PRIORITIES =====
console.log('Creating priorities...');
await prisma.priority.createMany({
skipDuplicates: true,
data: [
{ code: 'LOW', name: 'Nízka priorita', color: '#10B981', level: 1, order: 1 },
{ code: 'MEDIUM', name: 'Stredná priorita', color: '#F59E0B', level: 5, order: 2 },
{ code: 'HIGH', name: 'Vysoká priorita', color: '#EF4444', level: 8, order: 3 },
{ code: 'URGENT', name: 'Urgentná', color: '#DC2626', level: 10, order: 4 },
],
});
// ===== SYSTEM SETTINGS =====
console.log('Creating system settings...');
await prisma.systemSetting.createMany({
skipDuplicates: true,
data: [
{
key: 'REVISION_REMINDER_DAYS',
value: 14,
category: 'NOTIFICATIONS',
label: 'Pripomenúť revíziu X dní dopredu',
dataType: 'number',
validation: { min: 1, max: 365 },
},
{
key: 'RMA_NUMBER_FORMAT',
value: 'RMA-{YYYY}{MM}{DD}{XXX}',
category: 'RMA',
label: 'Formát RMA čísla',
dataType: 'string',
},
{
key: 'RMA_CUSTOMER_REQUIRES_APPROVAL',
value: true,
category: 'RMA',
label: 'Reklamácie od zákazníkov vyžadujú schválenie',
dataType: 'boolean',
},
{
key: 'ADMIN_NOTIFICATION_EMAILS',
value: ['admin@firma.sk'],
category: 'NOTIFICATIONS',
label: 'Email adresy pre admin notifikácie',
dataType: 'json',
},
{
key: 'ENABLE_WEBSOCKET',
value: false,
category: 'GENERAL',
label: 'Zapnúť real-time aktualizácie (WebSocket)',
dataType: 'boolean',
},
],
});
// ===== DEMO USERS =====
console.log('Creating demo users...');
const rootRole = roles.find(r => r.code === 'ROOT');
const adminRole = roles.find(r => r.code === 'ADMIN');
const userRole = roles.find(r => r.code === 'USER');
if (rootRole && adminRole && userRole) {
await prisma.user.upsert({
where: { email: 'root@helpdesk.sk' },
update: { password: await bcrypt.hash('root123', 10) },
create: {
email: 'root@helpdesk.sk',
password: await bcrypt.hash('root123', 10),
name: 'Root Admin',
roleId: rootRole.id,
},
});
await prisma.user.upsert({
where: { email: 'admin@helpdesk.sk' },
update: { password: await bcrypt.hash('admin123', 10) },
create: {
email: 'admin@helpdesk.sk',
password: await bcrypt.hash('admin123', 10),
name: 'Peter Admin',
roleId: adminRole.id,
},
});
await prisma.user.upsert({
where: { email: 'user@helpdesk.sk' },
update: { password: await bcrypt.hash('user123', 10) },
create: {
email: 'user@helpdesk.sk',
password: await bcrypt.hash('user123', 10),
name: 'Martin Používateľ',
roleId: userRole.id,
},
});
}
console.log('Seeding completed!');
}
seed()
.catch((error) => {
console.error('Seeding failed:', error);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});