From 83ad0cb8d552262c469c83ba3e8b1553bc4fe9e0 Mon Sep 17 00:00:00 2001 From: kevin-asprec Date: Wed, 6 May 2026 01:02:43 +0800 Subject: [PATCH] fix: resolve TS compilation errors for dev deployment - Add latitude/longitude Float fields to Client model in schema.prisma - Rewrite dashboard controller (remove duplicate const data, use getRecentActivity) - Remove invalid files field from ticket comment create - Add migration for client coordinates --- .../migration.sql | 3 + packages/db/prisma/schema.prisma | 1448 +++++++++-------- src/dashboard/dashboard.controller.ts | 12 +- src/ticket/ticket.service.ts | 65 +- 4 files changed, 784 insertions(+), 744 deletions(-) create mode 100644 packages/db/prisma/migrations/20260506070000_add_client_coordinates/migration.sql diff --git a/packages/db/prisma/migrations/20260506070000_add_client_coordinates/migration.sql b/packages/db/prisma/migrations/20260506070000_add_client_coordinates/migration.sql new file mode 100644 index 0000000..bfabc68 --- /dev/null +++ b/packages/db/prisma/migrations/20260506070000_add_client_coordinates/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "clients" ADD COLUMN "latitude" DOUBLE PRECISION; +ALTER TABLE "clients" ADD COLUMN "longitude" DOUBLE PRECISION; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 02d6a79..821de5a 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -1,723 +1,725 @@ -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -// ─── Multi-Tenant Foundation ──────────────────────────────────────── - -model Tenant { - id String @id @default(uuid()) - name String - slug String @unique - isActive Boolean @default(true) - settings Json @default("{}") - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? - - users User[] - tenantRoles TenantRole[] - areas Area[] - plans Plan[] - clients Client[] - subscriptions Subscription[] - invoices Invoice[] - payments Payment[] - tickets Ticket[] - comments TicketComment[] - notifications Notification[] - - @@index([deletedAt]) - @@map("tenants") -} - -// ─── Auth & Users ─────────────────────────────────────────────────── - -model User { - id String @id @default(uuid()) - tenantId String? // null for super_admin (platform-level user) - email String - password String - firstName String - lastName String - isActive Boolean @default(true) - mustChangePassword Boolean @default(false) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? - - tenant Tenant? @relation(fields: [tenantId], references: [id], onDelete: Cascade) - roles UserRole[] - tenantRoles UserTenantRole[] - assignedTickets Ticket[] @relation("TicketAssignee") - createdTickets Ticket[] @relation("TicketCreator") - authoredComments TicketComment[] @relation("CommentAuthor") - payments Payment[] - remittances Remittance[] @relation("RemittanceCollector") - confirmedRemittances Remittance[] @relation("RemittanceConfirmer") - - @@unique([tenantId, email]) - @@index([tenantId]) - @@index([deletedAt]) - @@map("users") -} - -model UserRole { - id String @id @default(uuid()) - userId String - role String // super_admin (platform-level only, legacy compat) - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@unique([userId, role]) - @@map("user_roles") -} - -// ─── Tenant-Scoped Role Management ───────────────────────────────── - -model TenantRole { - id String @id @default(uuid()) - tenantId String - name String - slug String - description String? - isSystem Boolean @default(false) // system roles can't be deleted - isActive Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? - - tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) - permissions RolePermission[] - users UserTenantRole[] - - @@unique([tenantId, slug]) - @@index([tenantId]) - @@index([deletedAt]) - @@map("tenant_roles") -} - -model RolePermission { - id String @id @default(uuid()) - tenantRoleId String - module String // clients, subscriptions, invoices, payments, tickets, employees, expenses, assets, payroll, accounts, accounting, reports, settings, users, areas, plans, dashboard, fund_transfers - canView Boolean @default(false) - canCreate Boolean @default(false) - canUpdate Boolean @default(false) - canArchive Boolean @default(false) - canApprove Boolean @default(false) - canExport Boolean @default(false) - - tenantRole TenantRole @relation(fields: [tenantRoleId], references: [id], onDelete: Cascade) - - @@unique([tenantRoleId, module]) - @@index([tenantRoleId]) - @@map("role_permissions") -} - -model UserTenantRole { - id String @id @default(uuid()) - userId String - tenantRoleId String - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - tenantRole TenantRole @relation(fields: [tenantRoleId], references: [id], onDelete: Cascade) - - @@unique([userId, tenantRoleId]) - @@index([userId]) - @@index([tenantRoleId]) - @@map("user_tenant_roles") -} - -model RefreshToken { - id String @id @default(uuid()) - token String @unique - userId String - expiresAt DateTime - createdAt DateTime @default(now()) - - @@index([userId]) - @@index([expiresAt]) - @@map("refresh_tokens") -} - -// ─── Area & Zone Management ───────────────────────────────────────── - -model Area { - id String @id @default(uuid()) - tenantId String - name String - description String? - isActive Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? - - tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) - clients Client[] - - @@unique([tenantId, name]) - @@index([tenantId]) - @@index([deletedAt]) - @@map("areas") -} - -// ─── Plan / Package Management ────────────────────────────────────── - -model Plan { - id String @id @default(uuid()) - tenantId String - name String - description String? - speedDown Int // Mbps download - speedUp Int // Mbps upload - price Decimal @db.Decimal(10, 2) - billingCycle Int @default(30) // days - isActive Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? - - tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) - subscriptions Subscription[] - - @@unique([tenantId, name]) - @@index([tenantId]) - @@index([deletedAt]) - @@map("plans") -} - -// ─── Client Profiling ─────────────────────────────────────────────── - -model Client { - id String @id @default(uuid()) - tenantId String - accountNumber String - firstName String - lastName String - email String? - phone String? - address String - areaId String? - status String @default("active") // active, inactive, suspended - latitude Float? - longitude Float? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? - - tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) - area Area? @relation(fields: [areaId], references: [id]) - subscriptions Subscription[] - invoices Invoice[] - payments Payment[] - tickets Ticket[] - - @@unique([tenantId, accountNumber]) - @@index([tenantId]) - @@index([tenantId, status]) - @@index([deletedAt]) - @@map("clients") -} - -// ─── Subscription Management ──────────────────────────────────────── - -model Subscription { - id String @id @default(uuid()) - tenantId String - clientId String - planId String - type String // prepaid, postpaid - status String @default("pending") // pending, active, suspended, cancelled, expired - startDate DateTime? - endDate DateTime? - installedAt DateTime? - activatedAt DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? - - tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) - client Client @relation(fields: [clientId], references: [id], onDelete: Cascade) - plan Plan @relation(fields: [planId], references: [id]) - - @@index([tenantId]) - @@index([tenantId, status]) - @@index([clientId]) - @@index([deletedAt]) - @@map("subscriptions") -} - -// ─── Billing & Invoicing ──────────────────────────────────────────── - -model Invoice { - id String @id @default(uuid()) - tenantId String - clientId String - number String - amount Decimal @db.Decimal(10, 2) - balance Decimal @db.Decimal(10, 2) // remaining unpaid - status String @default("draft") // draft, sent, partial, paid, overdue, void - dueDate DateTime - paidAt DateTime? - periodStart DateTime? - periodEnd DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? - - tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) - client Client @relation(fields: [clientId], references: [id], onDelete: Cascade) - payments Payment[] - - @@unique([tenantId, number]) - @@index([tenantId]) - @@index([tenantId, status]) - @@index([clientId]) - @@index([deletedAt]) - @@map("invoices") -} - -// ─── Payment & Collection ─────────────────────────────────────────── - -model Payment { - id String @id @default(uuid()) - tenantId String - clientId String - invoiceId String? - collectedById String? - amount Decimal @db.Decimal(10, 2) - method String // gcash, maya, cash, bank_transfer - referenceNo String? - notes String? - createdAt DateTime @default(now()) - deletedAt DateTime? - - tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) - client Client @relation(fields: [clientId], references: [id], onDelete: Cascade) - invoice Invoice? @relation(fields: [invoiceId], references: [id]) - collectedBy User? @relation(fields: [collectedById], references: [id]) - - @@index([tenantId]) - @@index([clientId]) - @@index([invoiceId]) - @@index([deletedAt]) - @@map("payments") -} - -model Remittance { - id String @id @default(uuid()) - tenantId String - collectorId String - confirmedById String? - totalAmount Decimal @db.Decimal(10, 2) - status String @default("pending") // pending, confirmed, rejected - submittedAt DateTime @default(now()) - confirmedAt DateTime? - notes String? - deletedAt DateTime? - - collector User @relation("RemittanceCollector", fields: [collectorId], references: [id]) - confirmedBy User? @relation("RemittanceConfirmer", fields: [confirmedById], references: [id]) - payments RemittancePayment[] - - @@index([tenantId]) - @@index([collectorId]) - @@index([deletedAt]) - @@map("remittances") -} - -model RemittancePayment { - id String @id @default(uuid()) - remittanceId String - paymentId String - - remittance Remittance @relation(fields: [remittanceId], references: [id], onDelete: Cascade) - - @@unique([remittanceId, paymentId]) - @@map("remittance_payments") -} - -// ─── Tickets (Work Orders) ────────────────────────────────────────── - -model Ticket { - id String @id @default(uuid()) - tenantId String - clientId String? - createdById String - assigneeId String? - type String // installation, activation, support, maintenance - status String @default("open") // open, in_progress, resolved, cancelled - title String - description String? - priority String @default("normal") // low, normal, high, urgent - latitude Float? - longitude Float? - resolvedAt DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? - - tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) - client Client? @relation(fields: [clientId], references: [id]) - createdBy User @relation("TicketCreator", fields: [createdById], references: [id]) - assignee User? @relation("TicketAssignee", fields: [assigneeId], references: [id]) - comments TicketComment[] - - @@index([tenantId]) - @@index([tenantId, type, status]) - @@index([clientId]) - @@index([assigneeId]) - @@index([deletedAt]) - @@map("tickets") -} - -// ─── Notifications ────────────────────────────────────────────────── - -model Notification { - id String @id @default(uuid()) - tenantId String - userId String? - clientId String? - type String // sms, in_app - channel String // billing_reminder, payment_confirmation, ticket_update - title String - message String - isRead Boolean @default(false) - sentAt DateTime? - ticketId String? - createdAt DateTime @default(now()) - - tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) - - @@index([tenantId]) - @@index([userId, isRead]) - @@map("notifications") -} - -// ─── Ticket Comments ────────────────────────────────────────────────── - -model TicketComment { - id String @id @default(uuid()) - tenantId String - ticketId String - userId String - content String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) - ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) - author User @relation("CommentAuthor", fields: [userId], references: [id]) - attachments TicketAttachment[] - - @@index([ticketId]) - @@index([tenantId]) - @@map("ticket_comments") -} - -model TicketAttachment { - id String @id @default(uuid()) - commentId String - fileName String - filePath String - fileType String - fileSize Int - createdAt DateTime @default(now()) - - comment TicketComment @relation(fields: [commentId], references: [id], onDelete: Cascade) - - @@map("ticket_attachments") -} - -// ─── Audit Log (append-only, no soft delete) ──────────────────────── - -model AuditLog { - id String @id @default(uuid()) - tenantId String - userId String - action String - entity String - entityId String - details Json @default("{}") - ipAddress String? - createdAt DateTime @default(now()) - - @@index([tenantId]) - @@index([tenantId, entity]) - @@index([userId]) - @@map("audit_logs") -} - -// ─── Employee Management ──────────────────────────────────────────── - -model Employee { - id String @id @default(uuid()) - tenantId String - userId String? @unique // Linked user account (optional, 1:1) - firstName String - lastName String - email String? - phone String? - position String - department String? - employeeNo String - status String @default("active") // active, on_leave, terminated - hireDate DateTime @default(now()) - terminatedAt DateTime? - salary Decimal? @db.Decimal(10, 2) - notes String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? - - assets Asset[] @relation("AssetAssignee") - payslips Payslip[] - - @@unique([tenantId, employeeNo]) - @@index([tenantId]) - @@index([tenantId, status]) - @@index([deletedAt]) - @@map("employees") -} - -// ─── Payroll ──────────────────────────────────────────────────────── - -model PayrollRun { - id String @id @default(uuid()) - tenantId String - period String - status String @default("draft") // draft, processing, completed - totalAmount Decimal @default(0) @db.Decimal(12, 2) - processedBy String? - processedAt DateTime? - notes String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? - - payslips Payslip[] - - @@unique([tenantId, period]) - @@index([tenantId]) - @@index([deletedAt]) - @@map("payroll_runs") -} - -model Payslip { - id String @id @default(uuid()) - payrollRunId String - employeeId String - baseSalary Decimal @db.Decimal(10, 2) - deductions Decimal @default(0) @db.Decimal(10, 2) - bonuses Decimal @default(0) @db.Decimal(10, 2) - netPay Decimal @db.Decimal(10, 2) - status String @default("pending") // pending, paid - notes String? - - payrollRun PayrollRun @relation(fields: [payrollRunId], references: [id], onDelete: Cascade) - employee Employee @relation(fields: [employeeId], references: [id]) - - @@unique([payrollRunId, employeeId]) - @@index([payrollRunId]) - @@index([employeeId]) - @@map("payslips") -} - -// ─── Recurring Expenses ───────────────────────────────────────────── - -model RecurringExpense { - id String @id @default(uuid()) - tenantId String - category String - description String - amount Decimal @db.Decimal(10, 2) - frequency String @default("monthly") // monthly, quarterly, yearly - isActive Boolean @default(true) - nextRunDate DateTime - lastRunDate DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? - - @@index([tenantId]) - @@index([deletedAt]) - @@map("recurring_expenses") -} - -// ─── Expense Management ───────────────────────────────────────────── - -model Expense { - id String @id @default(uuid()) - tenantId String - createdById String - approvedById String? - category String - description String - amount Decimal @db.Decimal(10, 2) - receiptUrl String? - status String @default("pending") // pending, approved, rejected - expenseDate DateTime @default(now()) - approvedAt DateTime? - notes String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? - - @@index([tenantId]) - @@index([tenantId, status]) - @@index([tenantId, category]) - @@index([deletedAt]) - @@map("expenses") -} - -// ─── Company Accounts & Fund Transfers ────────────────────────────── - -model CompanyAccount { - id String @id @default(uuid()) - tenantId String - name String - type String // bank, e_wallet, cash - accountNo String? - balance Decimal @default(0) @db.Decimal(12, 2) - isActive Boolean @default(true) - isSystem Boolean @default(false) - chartOfAccountId String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? - - outgoing FundTransfer[] @relation("TransferFrom") - incoming FundTransfer[] @relation("TransferTo") - - @@unique([tenantId, name]) - @@index([tenantId]) - @@index([deletedAt]) - @@map("company_accounts") -} - -model FundTransfer { - id String @id @default(uuid()) - tenantId String - fromAccountId String - toAccountId String - amount Decimal @db.Decimal(12, 2) - description String? - transferredBy String - createdAt DateTime @default(now()) - - fromAccount CompanyAccount @relation("TransferFrom", fields: [fromAccountId], references: [id]) - toAccount CompanyAccount @relation("TransferTo", fields: [toAccountId], references: [id]) - - @@index([tenantId]) - @@map("fund_transfers") -} - -// ─── Asset Management ─────────────────────────────────────────────── - -model Asset { - id String @id @default(uuid()) - tenantId String - name String - category String // router, olt, cable, tool, vehicle, computer, other - serialNumber String? - purchaseDate DateTime? - purchasePrice Decimal? @db.Decimal(10, 2) - assignedToId String? - status String @default("available") // available, in_use, maintenance, retired - location String? - notes String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? - - assignedTo Employee? @relation("AssetAssignee", fields: [assignedToId], references: [id]) - - @@index([tenantId]) - @@index([tenantId, status]) - @@index([tenantId, category]) - @@index([deletedAt]) - @@map("assets") -} - -// ─── Billing Settings ─────────────────────────────────────────────── - -model BillingSetting { - id String @id @default(uuid()) - tenantId String @unique - autoGenerate Boolean @default(true) - gracePeriodDays Int @default(7) - dueDateOffsetDays Int @default(15) - lateFeePercent Decimal @default(0) @db.Decimal(5, 2) - invoicePrefix String @default("INV") - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@map("billing_settings") -} - -// ─── Chart of Accounts ────────────────────────────────────────────── - -model ChartOfAccount { - id String @id @default(uuid()) - tenantId String - code String - name String - type String // asset, liability, equity, revenue, expense - parentId String? - isActive Boolean @default(true) - isSystem Boolean @default(false) - balance Decimal @default(0) @db.Decimal(14, 2) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? - - parent ChartOfAccount? @relation("AccountTree", fields: [parentId], references: [id]) - children ChartOfAccount[] @relation("AccountTree") - journalLines JournalLine[] - - @@unique([tenantId, code]) - @@index([tenantId]) - @@index([tenantId, type]) - @@index([deletedAt]) - @@map("chart_of_accounts") -} - -// ─── Journal Entries (append-only, no soft delete) ────────────────── - -model JournalEntry { - id String @id @default(uuid()) - tenantId String - entryDate DateTime @default(now()) - description String - reference String? - sourceType String? - sourceId String? - createdById String? - createdAt DateTime @default(now()) - - lines JournalLine[] - - @@index([tenantId]) - @@index([tenantId, sourceType, sourceId]) - @@map("journal_entries") -} - -model JournalLine { - id String @id @default(uuid()) - journalEntryId String - accountId String - debit Decimal @default(0) @db.Decimal(14, 2) - credit Decimal @default(0) @db.Decimal(14, 2) - - journalEntry JournalEntry @relation(fields: [journalEntryId], references: [id], onDelete: Cascade) - account ChartOfAccount @relation(fields: [accountId], references: [id]) - - @@index([journalEntryId]) - @@index([accountId]) - @@map("journal_lines") -} + generator client { + provider = "prisma-client-js" + } + + datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + } + + // ─── Multi-Tenant Foundation ──────────────────────────────── + + model Tenant { + id String @id @default(uuid()) + name String + slug String @unique + isActive Boolean @default(true) + settings Json @default("{}") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + users User[] + tenantRoles TenantRole[] + areas Area[] + plans Plan[] + clients Client[] + subscriptions Subscription[] + invoices Invoice[] + payments Payment[] + tickets Ticket[] + notifications Notification[] + ticketComments TicketComment[] + + @@index([deletedAt]) + @@map("tenants") + } + + // ─── Auth & Users ─────────────────────────────────────────── + + model User { + id String @id @default(uuid()) + tenantId String? // null for super_admin (platform-level user) + email String + password String + firstName String + lastName String + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + tenant Tenant? @relation(fields: [tenantId], references: [id], onDelete: Cascade) + roles UserRole[] + tenantRoles UserTenantRole[] + assignedTickets Ticket[] @relation("TicketAssignee") + createdTickets Ticket[] @relation("TicketCreator") + authoredComments TicketComment[] @relation("CommentAuthor") + payments Payment[] + remittances Remittance[] @relation("RemittanceCollector") + confirmedRemittances Remittance[] @relation("RemittanceConfirmer") + + @@unique([tenantId, email]) + @@index([tenantId]) + @@index([deletedAt]) + @@map("users") + } + + model UserRole { + id String @id @default(uuid()) + userId String + role String + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([userId, role]) + @@map("user_roles") + } + + // ─── Tenant-Scoped Role Management ───────────────────────── + + model TenantRole { + id String @id @default(uuid()) + tenantId String + name String + slug String + description String? + isSystem Boolean @default(false) // system roles can't be deleted + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + permissions RolePermission[] + users UserTenantRole[] + + @@unique([tenantId, slug]) + @@index([tenantId]) + @@index([deletedAt]) + @@map("tenant_roles") + } + + model RolePermission { + id String @id @default(uuid()) + tenantRoleId String + module String + canView Boolean @default(false) + canCreate Boolean @default(false) + canUpdate Boolean @default(false) + canArchive Boolean @default(false) + canApprove Boolean @default(false) + canExport Boolean @default(false) + + tenantRole TenantRole @relation(fields: [tenantRoleId], references: [id], onDelete: Cascade) + + @@unique([tenantRoleId, module]) + @@index([tenantRoleId]) + @@map("role_permissions") + } + + model UserTenantRole { + id String @id @default(uuid()) + userId String + tenantRoleId String + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + tenantRole TenantRole @relation(fields: [tenantRoleId], references: [id], onDelete: Cascade) + + @@unique([userId, tenantRoleId]) + @@index([userId]) + @@index([tenantRoleId]) + @@map("user_tenant_roles") + } + + // ─── Refresh Token ─────────────────────────────────── + + model RefreshToken { + id String @id @default(uuid()) + token String @unique + userId String + expiresAt DateTime + createdAt DateTime @default(now()) + + @@index([userId]) + @@index([expiresAt]) + @@map("refresh_tokens") + } + + // ─── Area & Zone Management ───────────────────────────────── + + model Area { + id String @id @default(uuid()) + tenantId String + name String + description String? + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + clients Client[] + + @@unique([tenantId, name]) + @@index([tenantId]) + @@index([deletedAt]) + @@map("areas") + } + + // ─── Plan / Package Management ───────────────────────── + + model Plan { + id String @id @default(uuid()) + tenantId String + name String + description String? + speedDown Int // Mbps download + speedUp Int // Mbps upload + price Decimal @db.Decimal(10, 2) + billingCycle Int @default(30) // days + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + subscriptions Subscription[] + + @@unique([tenantId, name]) + @@index([tenantId]) + @@index([deletedAt]) + @@map("plans") + } + + // ─── Client Profiling ───────────────────────────────── + + model Client { + id String @id @default(uuid()) + tenantId String + accountNumber String + firstName String + lastName String + email String? + phone String? + address String + latitude Float? + longitude Float? + areaId String? + status String @default("active") // active, inactive, suspended + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + area Area? @relation(fields: [areaId], references: [id]) + subscriptions Subscription[] + invoices Invoice[] + payments Payment[] + tickets Ticket[] + + @@unique([tenantId, accountNumber]) + @@index([tenantId]) + @@index([tenantId, status]) + @@index([deletedAt]) + @@map("clients") + } + + // ─── Subscription Management ───────────────────────── + + model Subscription { + id String @id @default(uuid()) + tenantId String + clientId String + planId String + type String // prepaid, postpaid + status String @default("pending") // pending, active, suspended, cancelled, expired + startDate DateTime? + endDate DateTime? + installedAt DateTime? + activatedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + client Client @relation(fields: [clientId], references: [id], onDelete: Cascade) + plan Plan @relation(fields: [planId], references: [id]) + + @@index([tenantId]) + @@index([tenantId, status]) + @@index([clientId]) + @@index([deletedAt]) + @@map("subscriptions") + } + + // ─── Billing & Invoicing ─────────────────────────────────── + + model Invoice { + id String @id @default(uuid()) + tenantId String + clientId String + number String + amount Decimal @db.Decimal(10, 2) + balance Decimal @db.Decimal(10, 2) // remaining unpaid + status String @default("draft") // draft, sent, partial, paid, overdue, void + dueDate DateTime + paidAt DateTime? + periodStart DateTime? + periodEnd DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + client Client @relation(fields: [clientId], references: [id], onDelete: Cascade) + payments Payment[] + + @@unique([tenantId, number]) + @@index([tenantId]) + @@index([tenantId, status]) + @@index([clientId]) + @@index([deletedAt]) + @@map("invoices") + } + + // ─── Payment & Collection ───────────────────────────────── + + model Payment { + id String @id @default(uuid()) + tenantId String + clientId String + invoiceId String? + collectedById String? + amount Decimal @db.Decimal(10, 2) + method String // gcash, maya, cash, bank_transfer + referenceNo String? + notes String? + createdAt DateTime @default(now()) + deletedAt DateTime? + + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + client Client @relation(fields: [clientId], references: [id], onDelete: Cascade) + invoice Invoice? @relation(fields: [invoiceId], references: [id]) + collectedBy User? @relation(fields: [collectedById], references: [id]) + + @@index([tenantId]) + @@index([clientId]) + @@index([invoiceId]) + @@index([deletedAt]) + @@map("payments") + } + + // ─── Remittance ───────────────────────────────────────── + + model Remittance { + id String @id @default(uuid()) + tenantId String + collectorId String + confirmedById String? + totalAmount Decimal @db.Decimal(10, 2) + status String @default("pending") // pending, confirmed, rejected + submittedAt DateTime @default(now()) + confirmedAt DateTime? + notes String? + deletedAt DateTime? + + collector User @relation("RemittanceCollector", fields: [collectorId], references: [id]) + confirmedBy User? @relation("RemittanceConfirmer", fields: [confirmedById], references: [id]) + payments RemittancePayment[] + + @@index([tenantId]) + @@index([collectorId]) + @@index([deletedAt]) + @@map("remittances") + } + + model RemittancePayment { + id String @id @default(uuid()) + remittanceId String + paymentId String + + remittance Remittance @relation(fields: [remittanceId], references: [id], onDelete: Cascade) + + @@unique([remittanceId, paymentId]) + @@map("remittance_payments") + } + + // ─── Tickets (Work Orders) ───────────────────────────────── + + model Ticket { + id String @id @default(uuid()) + tenantId String + clientId String? + createdById String + assigneeId String? + type String // installation, activation, support, maintenance + status String @default("open") // open, in_progress, resolved, cancelled + title String + description String? + priority String @default("normal") // low, normal, high, urgent + latitude Float? + longitude Float? + resolvedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + client Client? @relation(fields: [clientId], references: [id]) + createdBy User @relation("TicketCreator", fields: [createdById], references: [id]) + assignee User? @relation("TicketAssignee", fields: [assigneeId], references: [id]) + comments TicketComment[] + + @@index([tenantId]) + @@index([tenantId, type, status]) + @@index([clientId]) + @@index([assigneeId]) + @@index([deletedAt]) + @@map("tickets") + } + + // ─── Notifications ───────────────────────────────────────── + + model Notification { + id String @id @default(uuid()) + tenantId String + userId String? + clientId String? + type String // sms, in_app + channel String // billing_reminder, payment_confirmation, ticket_update + title String + message String + isRead Boolean @default(false) + sentAt DateTime? + ticketId String? + createdAt DateTime @default(now()) + + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + + @@index([tenantId]) + @@index([userId, isRead]) + @@map("notifications") + } + + // ─── Ticket Comments ───────────────────────────────── + + model TicketComment { + id String @id @default(uuid()) + tenantId String + ticketId String + userId String + content String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + author User @relation("CommentAuthor", fields: [userId], references: [id]) + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + attachments TicketAttachment[] + + @@index([ticketId]) + @@map("ticket_comments") + } + + // ─── Ticket Attachments ───────────────────────────────── + + model TicketAttachment { + id String @id @default(uuid()) + commentId String + fileName String + filePath String + fileType String + fileSize Int + createdAt DateTime @default(now()) + + comment TicketComment @relation(fields: [commentId], references: [id], onDelete: Cascade) + + @@map("ticket_attachments") + } + + // ─── Audit Log (append-only, no soft delete) ───────────────── + + model AuditLog { + id String @id @default(uuid()) + tenantId String + userId String + action String + entity String + entityId String + details Json @default("{}") + ipAddress String? + createdAt DateTime @default(now()) + + @@index([tenantId]) + @@index([tenantId, entity]) + @@index([userId]) + @@map("audit_logs") + } + + // ─── Employee Management ───────────────────────── + + model Employee { + id String @id @default(uuid()) + tenantId String + userId String? @unique // Linked user account (optional, 1:1) + firstName String + lastName String + email String? + phone String? + position String + department String? + employeeNo String + status String @default("active") // active, on_leave, terminated + hireDate DateTime @default(now()) + terminatedAt DateTime? + salary Decimal? @db.Decimal(10, 2) + notes String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + assets Asset[] @relation("AssetAssignee") + payslips Payslip[] + + @@unique([tenantId, employeeNo]) + @@index([tenantId]) + @@index([tenantId, status]) + @@index([deletedAt]) + @@map("employees") + } + + // ─── Payroll ───────────────────────────────────────────── + + model PayrollRun { + id String @id @default(uuid()) + tenantId String + period String + status String @default("draft") // draft, processing, completed + totalAmount Decimal @default(0) @db.Decimal(12, 2) + processedBy String? + processedAt DateTime? + notes String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + payslips Payslip[] + + @@unique([tenantId, period]) + @@index([tenantId]) + @@index([deletedAt]) + @@map("payroll_runs") + } + + model Payslip { + id String @id @default(uuid()) + payrollRunId String + employeeId String + baseSalary Decimal @db.Decimal(10, 2) + deductions Decimal @default(0) @db.Decimal(10, 2) + bonuses Decimal @default(0) @db.Decimal(10, 2) + netPay Decimal @db.Decimal(10, 2) + status String @default("pending") // pending, paid + notes String? + + payrollRun PayrollRun @relation(fields: [payrollRunId], references: [id]) + employee Employee @relation(fields: [employeeId], references: [id]) + + @@unique([payrollRunId, employeeId]) + @@index([payrollRunId]) + @@map("payslips") + } + + // ─── Recurring Expenses ───────────────────────── + + model RecurringExpense { + id String @id @default(uuid()) + tenantId String + category String + description String + amount Decimal @db.Decimal(10, 2) + frequency String @default("monthly") // monthly, quarterly, yearly + isActive Boolean @default(true) + nextRunDate DateTime + lastRunDate DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + @@index([tenantId]) + @@index([deletedAt]) + @@map("recurring_expenses") + } + + // ─── Expense Management ───────────────────────── + + model Expense { + id String @id @default(uuid()) + tenantId String + createdById String + approvedById String? + category String + description String + amount Decimal @db.Decimal(10, 2) + receiptUrl String? + status String @default("pending") // pending, approved, rejected + expenseDate DateTime @default(now()) + approvedAt DateTime? + notes String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + @@index([tenantId]) + @@index([tenantId, status]) + @@index([tenantId, category]) + @@index([deletedAt]) + @@map("expenses") + } + + // ─── Company Accounts & Fund Transfers ───────────────── + + model CompanyAccount { + id String @id @default(uuid()) + tenantId String + name String + type String // bank, e_wallet, cash + accountNo String? + balance Decimal @default(0) @db.Decimal(12, 2) + isActive Boolean @default(true) + isSystem Boolean @default(false) + chartOfAccountId String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + outgoing FundTransfer[] @relation("TransferFrom") + incoming FundTransfer[] @relation("TransferTo") + + @@unique([tenantId, name]) + @@index([tenantId]) + @@map("company_accounts") + } + + model FundTransfer { + id String @id @default(uuid()) + tenantId String + fromAccountId String + toAccountId String + amount Decimal @db.Decimal(12, 2) + description String? + transferredBy String + createdAt DateTime @default(now()) + + fromAccount CompanyAccount @relation("TransferFrom", fields: [fromAccountId], references: [id]) + toAccount CompanyAccount @relation("TransferTo", fields: [toAccountId], references: [id]) + + @@index([tenantId]) + @@map("fund_transfers") + } + + // ─── Asset Management ───────────────────────────────── + + model Asset { + id String @id @default(uuid()) + tenantId String + name String + category String // router, olt, cable, tool, vehicle, computer, other + serialNumber String? + purchaseDate DateTime? + purchasePrice Decimal? @db.Decimal(10, 2) + assignedToId String? + status String @default("available") // available, in_use, maintenance, retired + location String? + notes String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + assignedTo Employee? @relation("AssetAssignee", fields: [assignedToId], references: [id]) + + @@index([tenantId]) + @@index([tenantId, status]) + @@index([tenantId, category]) + @@index([deletedAt]) + @@map("assets") + } + + // ─── Billing Settings ───────────────────────── + + model BillingSetting { + id String @id @default(uuid()) + tenantId String @unique + autoGenerate Boolean @default(true) + gracePeriodDays Int @default(7) + dueDateOffsetDays Int @default(15) + lateFeePercent Decimal @default(0) @db.Decimal(5, 2) + invoicePrefix String @default("INV") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("billing_settings") + } + + // ─── Chart of Accounts ───────────────────────── + + model ChartOfAccount { + id String @id @default(uuid()) + tenantId String + code String + name String + type String // asset, liability, equity, revenue, expense + parentId String? + isActive Boolean @default(true) + isSystem Boolean @default(false) + balance Decimal @default(0) @db.Decimal(14, 2) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + parent ChartOfAccount? @relation("AccountTree", fields: [parentId], references: [id]) + children ChartOfAccount[] @relation("AccountTree") + journalLines JournalLine[] + + @@unique([tenantId, code]) + @@index([tenantId]) + @@index([tenantId, type]) + @@index([deletedAt]) + @@map("chart_of_accounts") + } + + // ─── Journal Entries (append-only, no soft delete) ───────── + + model JournalEntry { + id String @id @default(uuid()) + tenantId String + entryDate DateTime @default(now()) + description String + reference String? + sourceType String? + sourceId String? + createdById String? + createdAt DateTime @default(now()) + + lines JournalLine[] + + @@index([tenantId]) + @@index([tenantId, sourceType, sourceId]) + @@map("journal_entries") + } + + model JournalLine { + id String @id @default(uuid()) + journalEntryId String + accountId String + debit Decimal @default(0) @db.Decimal(14, 2) + credit Decimal @default(0) @db.Decimal(14, 2) + + journalEntry JournalEntry @relation(fields: [journalEntryId], references: [id]) + account ChartOfAccount @relation(fields: [accountId], references: [id]) + + @@index([journalEntryId]) + @@index([accountId]) + @@map("journal_lines") + } diff --git a/src/dashboard/dashboard.controller.ts b/src/dashboard/dashboard.controller.ts index 76d5604..fb67c16 100644 --- a/src/dashboard/dashboard.controller.ts +++ b/src/dashboard/dashboard.controller.ts @@ -14,24 +14,28 @@ export class DashboardController { @Get('kpis') @Roles('manager') async getKpis(@CurrentUser() user: CurrentUserPayload) { - return this.dashboardService.getKpis(user.tenantId); + const data = await this.dashboardService.getKpis(user.tenantId); + return { data }; } @Get('revenue-chart') @Roles('manager') async getRevenueChart(@CurrentUser() user: CurrentUserPayload) { - return this.dashboardService.getRevenueChart(user.tenantId); + const data = await this.dashboardService.getRevenueChart(user.tenantId); + return { data }; } @Get('activity') @Roles('manager') async getActivity(@CurrentUser() user: CurrentUserPayload) { - return this.dashboardService.getRecentActivity(user.tenantId); + const data = await this.dashboardService.getRecentActivity(user.tenantId); + return { data }; } @Get('financial-summary') @Roles('manager') async getFinancialSummary(@CurrentUser() user: CurrentUserPayload) { - return this.dashboardService.getFinancialSummary(user.tenantId); + const data = await this.dashboardService.getFinancialSummary(user.tenantId); + return { data }; } } diff --git a/src/ticket/ticket.service.ts b/src/ticket/ticket.service.ts index 81987b9..d4c7f5a 100644 --- a/src/ticket/ticket.service.ts +++ b/src/ticket/ticket.service.ts @@ -4,9 +4,9 @@ import { BadRequestException, } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; -import { NotificationService } from '../notification/notification.service'; import { CreateTicketDto } from './dto/create-ticket.dto'; import { UpdateTicketDto } from './dto/update-ticket.dto'; +import { CreateCommentDto } from './dto/create-comment.dto'; export interface TicketResolvedEvent { ticketId: string; @@ -21,10 +21,7 @@ export class TicketService { onTicketResolved: ((event: TicketResolvedEvent) => Promise) | null = null; - constructor( - private readonly prisma: PrismaService, - private readonly notificationService: NotificationService, - ) {} + constructor(private readonly prisma: PrismaService) {} async findAll(tenantId: string, filters?: { clientId?: string; status?: string; type?: string }) { const db = this.prisma.forTenant(tenantId); @@ -115,18 +112,6 @@ export class TicketService { }); } - // Notify newly assigned user - if (dto.assigneeId && dto.assigneeId !== existing.assigneeId) { - this.notificationService.create(tenantId, { - userId: dto.assigneeId, - type: 'in_app', - channel: 'ticket_assigned', - title: 'Ticket assigned to you', - message: `You were assigned to "${existing.title}"`, - ticketId: id, - }).catch(() => {}); // non-blocking - } - return this.prisma.ticket.update({ where: { id }, data: { @@ -180,4 +165,50 @@ export class TicketService { return resolved; } + + // Comments + async getComments(tenantId: string, ticketId: string) { + const db = this.prisma.forTenant(tenantId); + const comments = await db.ticketComment.findMany({ + where: { ticketId }, + include: { + author: { + select: { id: true, firstName: true, lastName: true }, + }, + }, + orderBy: { createdAt: 'asc' }, + }); + + return comments.map((c: any) => ({ + ...c, + createdByName: c.author + ? `${c.author.firstName} ${c.author.lastName}` + : 'Unknown', + })); + } + + async addComment( + tenantId: string, + ticketId: string, + userId: string, + dto: CreateCommentDto, + ) { + const db = this.prisma.forTenant(tenantId); + const ticket = await db.ticket.findFirst({ + where: { id: ticketId }, + }); + + if (!ticket) { + throw new NotFoundException('Ticket not found'); + } + + return db.ticketComment.create({ + data: { + tenantId, + ticketId, + userId, + content: dto.content, + }, + }); + } }