Merge dev to main #1
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "clients" ADD COLUMN "latitude" DOUBLE PRECISION;
|
||||
ALTER TABLE "clients" ADD COLUMN "longitude" DOUBLE PRECISION;
|
||||
@@ -7,7 +7,7 @@ datasource db {
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
// ─── Multi-Tenant Foundation ────────────────────────────────────────
|
||||
// ─── Multi-Tenant Foundation ────────────────────────────────
|
||||
|
||||
model Tenant {
|
||||
id String @id @default(uuid())
|
||||
@@ -28,14 +28,14 @@ model Tenant {
|
||||
invoices Invoice[]
|
||||
payments Payment[]
|
||||
tickets Ticket[]
|
||||
comments TicketComment[]
|
||||
notifications Notification[]
|
||||
ticketComments TicketComment[]
|
||||
|
||||
@@index([deletedAt])
|
||||
@@map("tenants")
|
||||
}
|
||||
|
||||
// ─── Auth & Users ───────────────────────────────────────────────────
|
||||
// ─── Auth & Users ───────────────────────────────────────────
|
||||
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
@@ -45,7 +45,6 @@ model User {
|
||||
firstName String
|
||||
lastName String
|
||||
isActive Boolean @default(true)
|
||||
mustChangePassword Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime?
|
||||
@@ -69,7 +68,7 @@ model User {
|
||||
model UserRole {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
role String // super_admin (platform-level only, legacy compat)
|
||||
role String
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@ -77,7 +76,7 @@ model UserRole {
|
||||
@@map("user_roles")
|
||||
}
|
||||
|
||||
// ─── Tenant-Scoped Role Management ─────────────────────────────────
|
||||
// ─── Tenant-Scoped Role Management ─────────────────────────
|
||||
|
||||
model TenantRole {
|
||||
id String @id @default(uuid())
|
||||
@@ -104,7 +103,7 @@ model TenantRole {
|
||||
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
|
||||
module String
|
||||
canView Boolean @default(false)
|
||||
canCreate Boolean @default(false)
|
||||
canUpdate Boolean @default(false)
|
||||
@@ -133,6 +132,8 @@ model UserTenantRole {
|
||||
@@map("user_tenant_roles")
|
||||
}
|
||||
|
||||
// ─── Refresh Token ───────────────────────────────────
|
||||
|
||||
model RefreshToken {
|
||||
id String @id @default(uuid())
|
||||
token String @unique
|
||||
@@ -145,7 +146,7 @@ model RefreshToken {
|
||||
@@map("refresh_tokens")
|
||||
}
|
||||
|
||||
// ─── Area & Zone Management ─────────────────────────────────────────
|
||||
// ─── Area & Zone Management ─────────────────────────────────
|
||||
|
||||
model Area {
|
||||
id String @id @default(uuid())
|
||||
@@ -166,7 +167,7 @@ model Area {
|
||||
@@map("areas")
|
||||
}
|
||||
|
||||
// ─── Plan / Package Management ──────────────────────────────────────
|
||||
// ─── Plan / Package Management ─────────────────────────
|
||||
|
||||
model Plan {
|
||||
id String @id @default(uuid())
|
||||
@@ -191,7 +192,7 @@ model Plan {
|
||||
@@map("plans")
|
||||
}
|
||||
|
||||
// ─── Client Profiling ───────────────────────────────────────────────
|
||||
// ─── Client Profiling ─────────────────────────────────
|
||||
|
||||
model Client {
|
||||
id String @id @default(uuid())
|
||||
@@ -202,10 +203,10 @@ model Client {
|
||||
email String?
|
||||
phone String?
|
||||
address String
|
||||
areaId String?
|
||||
status String @default("active") // active, inactive, suspended
|
||||
latitude Float?
|
||||
longitude Float?
|
||||
areaId String?
|
||||
status String @default("active") // active, inactive, suspended
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime?
|
||||
@@ -224,7 +225,7 @@ model Client {
|
||||
@@map("clients")
|
||||
}
|
||||
|
||||
// ─── Subscription Management ────────────────────────────────────────
|
||||
// ─── Subscription Management ─────────────────────────
|
||||
|
||||
model Subscription {
|
||||
id String @id @default(uuid())
|
||||
@@ -252,7 +253,7 @@ model Subscription {
|
||||
@@map("subscriptions")
|
||||
}
|
||||
|
||||
// ─── Billing & Invoicing ────────────────────────────────────────────
|
||||
// ─── Billing & Invoicing ───────────────────────────────────
|
||||
|
||||
model Invoice {
|
||||
id String @id @default(uuid())
|
||||
@@ -282,7 +283,7 @@ model Invoice {
|
||||
@@map("invoices")
|
||||
}
|
||||
|
||||
// ─── Payment & Collection ───────────────────────────────────────────
|
||||
// ─── Payment & Collection ─────────────────────────────────
|
||||
|
||||
model Payment {
|
||||
id String @id @default(uuid())
|
||||
@@ -309,6 +310,8 @@ model Payment {
|
||||
@@map("payments")
|
||||
}
|
||||
|
||||
// ─── Remittance ─────────────────────────────────────────
|
||||
|
||||
model Remittance {
|
||||
id String @id @default(uuid())
|
||||
tenantId String
|
||||
@@ -342,7 +345,7 @@ model RemittancePayment {
|
||||
@@map("remittance_payments")
|
||||
}
|
||||
|
||||
// ─── Tickets (Work Orders) ──────────────────────────────────────────
|
||||
// ─── Tickets (Work Orders) ─────────────────────────────────
|
||||
|
||||
model Ticket {
|
||||
id String @id @default(uuid())
|
||||
@@ -376,7 +379,7 @@ model Ticket {
|
||||
@@map("tickets")
|
||||
}
|
||||
|
||||
// ─── Notifications ──────────────────────────────────────────────────
|
||||
// ─── Notifications ─────────────────────────────────────────
|
||||
|
||||
model Notification {
|
||||
id String @id @default(uuid())
|
||||
@@ -399,7 +402,7 @@ model Notification {
|
||||
@@map("notifications")
|
||||
}
|
||||
|
||||
// ─── Ticket Comments ──────────────────────────────────────────────────
|
||||
// ─── Ticket Comments ─────────────────────────────────
|
||||
|
||||
model TicketComment {
|
||||
id String @id @default(uuid())
|
||||
@@ -410,16 +413,17 @@ model TicketComment {
|
||||
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])
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
||||
attachments TicketAttachment[]
|
||||
|
||||
@@index([ticketId])
|
||||
@@index([tenantId])
|
||||
@@map("ticket_comments")
|
||||
}
|
||||
|
||||
// ─── Ticket Attachments ─────────────────────────────────
|
||||
|
||||
model TicketAttachment {
|
||||
id String @id @default(uuid())
|
||||
commentId String
|
||||
@@ -434,7 +438,7 @@ model TicketAttachment {
|
||||
@@map("ticket_attachments")
|
||||
}
|
||||
|
||||
// ─── Audit Log (append-only, no soft delete) ────────────────────────
|
||||
// ─── Audit Log (append-only, no soft delete) ─────────────────
|
||||
|
||||
model AuditLog {
|
||||
id String @id @default(uuid())
|
||||
@@ -453,7 +457,7 @@ model AuditLog {
|
||||
@@map("audit_logs")
|
||||
}
|
||||
|
||||
// ─── Employee Management ────────────────────────────────────────────
|
||||
// ─── Employee Management ─────────────────────────
|
||||
|
||||
model Employee {
|
||||
id String @id @default(uuid())
|
||||
@@ -485,7 +489,7 @@ model Employee {
|
||||
@@map("employees")
|
||||
}
|
||||
|
||||
// ─── Payroll ────────────────────────────────────────────────────────
|
||||
// ─── Payroll ─────────────────────────────────────────────
|
||||
|
||||
model PayrollRun {
|
||||
id String @id @default(uuid())
|
||||
@@ -519,16 +523,15 @@ model Payslip {
|
||||
status String @default("pending") // pending, paid
|
||||
notes String?
|
||||
|
||||
payrollRun PayrollRun @relation(fields: [payrollRunId], references: [id], onDelete: Cascade)
|
||||
payrollRun PayrollRun @relation(fields: [payrollRunId], references: [id])
|
||||
employee Employee @relation(fields: [employeeId], references: [id])
|
||||
|
||||
@@unique([payrollRunId, employeeId])
|
||||
@@index([payrollRunId])
|
||||
@@index([employeeId])
|
||||
@@map("payslips")
|
||||
}
|
||||
|
||||
// ─── Recurring Expenses ─────────────────────────────────────────────
|
||||
// ─── Recurring Expenses ─────────────────────────
|
||||
|
||||
model RecurringExpense {
|
||||
id String @id @default(uuid())
|
||||
@@ -549,7 +552,7 @@ model RecurringExpense {
|
||||
@@map("recurring_expenses")
|
||||
}
|
||||
|
||||
// ─── Expense Management ─────────────────────────────────────────────
|
||||
// ─── Expense Management ─────────────────────────
|
||||
|
||||
model Expense {
|
||||
id String @id @default(uuid())
|
||||
@@ -575,7 +578,7 @@ model Expense {
|
||||
@@map("expenses")
|
||||
}
|
||||
|
||||
// ─── Company Accounts & Fund Transfers ──────────────────────────────
|
||||
// ─── Company Accounts & Fund Transfers ─────────────────
|
||||
|
||||
model CompanyAccount {
|
||||
id String @id @default(uuid())
|
||||
@@ -596,7 +599,6 @@ model CompanyAccount {
|
||||
|
||||
@@unique([tenantId, name])
|
||||
@@index([tenantId])
|
||||
@@index([deletedAt])
|
||||
@@map("company_accounts")
|
||||
}
|
||||
|
||||
@@ -617,7 +619,7 @@ model FundTransfer {
|
||||
@@map("fund_transfers")
|
||||
}
|
||||
|
||||
// ─── Asset Management ───────────────────────────────────────────────
|
||||
// ─── Asset Management ─────────────────────────────────
|
||||
|
||||
model Asset {
|
||||
id String @id @default(uuid())
|
||||
@@ -644,7 +646,7 @@ model Asset {
|
||||
@@map("assets")
|
||||
}
|
||||
|
||||
// ─── Billing Settings ───────────────────────────────────────────────
|
||||
// ─── Billing Settings ─────────────────────────
|
||||
|
||||
model BillingSetting {
|
||||
id String @id @default(uuid())
|
||||
@@ -660,7 +662,7 @@ model BillingSetting {
|
||||
@@map("billing_settings")
|
||||
}
|
||||
|
||||
// ─── Chart of Accounts ──────────────────────────────────────────────
|
||||
// ─── Chart of Accounts ─────────────────────────
|
||||
|
||||
model ChartOfAccount {
|
||||
id String @id @default(uuid())
|
||||
@@ -687,7 +689,7 @@ model ChartOfAccount {
|
||||
@@map("chart_of_accounts")
|
||||
}
|
||||
|
||||
// ─── Journal Entries (append-only, no soft delete) ──────────────────
|
||||
// ─── Journal Entries (append-only, no soft delete) ─────────
|
||||
|
||||
model JournalEntry {
|
||||
id String @id @default(uuid())
|
||||
@@ -714,7 +716,7 @@ model JournalLine {
|
||||
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)
|
||||
journalEntry JournalEntry @relation(fields: [journalEntryId], references: [id])
|
||||
account ChartOfAccount @relation(fields: [accountId], references: [id])
|
||||
|
||||
@@index([journalEntryId])
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void>) | 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user