Commit 41932462 by Augusto

logs

parent 4abd89bc
CREATE TABLE `occurrence_logs` (
`id` varchar(25) NOT NULL,
`occurrenceId` varchar(25) NOT NULL,
`log_action` enum('CREATED','UPDATED','ASSIGNED','DUAL_ASSIGNED','UNASSIGNED','STATUS_CHANGED','PRIORITY_CHANGED','COMMENT_ADDED','ATTACHMENT_ADDED','ATTACHMENT_REMOVED','CONCLUSION_ADDED','CONCLUSION_UPDATED','DELETED') NOT NULL,
`description` text NOT NULL,
`oldValue` text,
`newValue` text,
`performedBy` varchar(25) NOT NULL,
`createdAt` timestamp NOT NULL DEFAULT now(),
CONSTRAINT `occurrence_logs_id` PRIMARY KEY(`id`)
);
--> statement-breakpoint
CREATE INDEX `log_occurrence_idx` ON `occurrence_logs` (`occurrenceId`);--> statement-breakpoint
CREATE INDEX `log_performed_by_idx` ON `occurrence_logs` (`performedBy`);--> statement-breakpoint
CREATE INDEX `log_action_idx` ON `occurrence_logs` (`log_action`);--> statement-breakpoint
CREATE INDEX `log_created_at_idx` ON `occurrence_logs` (`createdAt`);
\ No newline at end of file
{
"version": "5",
"dialect": "mysql",
"id": "bd99a92c-372f-428a-b4f6-ad46d9f0a991",
"prevId": "bc9c1809-8c1b-40b5-abc1-fd3ff12d8f47",
"tables": {
"attachments": {
"name": "attachments",
"columns": {
"id": {
"name": "id",
"type": "varchar(25)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"filename": {
"name": "filename",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"originalName": {
"name": "originalName",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"mimeType": {
"name": "mimeType",
"type": "varchar(100)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"size": {
"name": "size",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"path": {
"name": "path",
"type": "varchar(500)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"occurrenceId": {
"name": "occurrenceId",
"type": "varchar(25)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {
"attachment_occurrence_idx": {
"name": "attachment_occurrence_idx",
"columns": [
"occurrenceId"
],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"attachments_id": {
"name": "attachments_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"comments": {
"name": "comments",
"columns": {
"id": {
"name": "id",
"type": "varchar(25)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"isInternal": {
"name": "isInternal",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"occurrenceId": {
"name": "occurrenceId",
"type": "varchar(25)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"authorId": {
"name": "authorId",
"type": "varchar(25)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {
"occurrence_idx": {
"name": "occurrence_idx",
"columns": [
"occurrenceId"
],
"isUnique": false
},
"author_idx": {
"name": "author_idx",
"columns": [
"authorId"
],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"comments_id": {
"name": "comments_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"conclusions": {
"name": "conclusions",
"columns": {
"id": {
"name": "id",
"type": "varchar(25)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"fieldMaterial": {
"name": "fieldMaterial",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"materialUsed": {
"name": "materialUsed",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"occurrenceId": {
"name": "occurrenceId",
"type": "varchar(25)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {
"conclusion_occurrence_idx": {
"name": "conclusion_occurrence_idx",
"columns": [
"occurrenceId"
],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"conclusions_id": {
"name": "conclusions_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"conclusions_occurrenceId_unique": {
"name": "conclusions_occurrenceId_unique",
"columns": [
"occurrenceId"
]
}
},
"checkConstraint": {}
},
"occurrence_logs": {
"name": "occurrence_logs",
"columns": {
"id": {
"name": "id",
"type": "varchar(25)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"occurrenceId": {
"name": "occurrenceId",
"type": "varchar(25)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"log_action": {
"name": "log_action",
"type": "enum('CREATED','UPDATED','ASSIGNED','DUAL_ASSIGNED','UNASSIGNED','STATUS_CHANGED','PRIORITY_CHANGED','COMMENT_ADDED','ATTACHMENT_ADDED','ATTACHMENT_REMOVED','CONCLUSION_ADDED','CONCLUSION_UPDATED','DELETED')",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"oldValue": {
"name": "oldValue",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"newValue": {
"name": "newValue",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"performedBy": {
"name": "performedBy",
"type": "varchar(25)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {
"log_occurrence_idx": {
"name": "log_occurrence_idx",
"columns": [
"occurrenceId"
],
"isUnique": false
},
"log_performed_by_idx": {
"name": "log_performed_by_idx",
"columns": [
"performedBy"
],
"isUnique": false
},
"log_action_idx": {
"name": "log_action_idx",
"columns": [
"log_action"
],
"isUnique": false
},
"log_created_at_idx": {
"name": "log_created_at_idx",
"columns": [
"createdAt"
],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"occurrence_logs_id": {
"name": "occurrence_logs_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"occurrences": {
"name": "occurrences",
"columns": {
"id": {
"name": "id",
"type": "varchar(25)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"status": {
"name": "status",
"type": "enum('OPEN','IN_PROGRESS','RESOLVED','CLOSED','PARCIAL_RESOLVED','CANCELLED')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'OPEN'"
},
"priority": {
"name": "priority",
"type": "enum('LOW','MEDIUM','HIGH','URGENT')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'MEDIUM'"
},
"category": {
"name": "category",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"location": {
"name": "location",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"reporterId": {
"name": "reporterId",
"type": "varchar(25)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"assigneeId": {
"name": "assigneeId",
"type": "varchar(25)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"managerId": {
"name": "managerId",
"type": "varchar(25)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
},
"closedAt": {
"name": "closedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"reporter_idx": {
"name": "reporter_idx",
"columns": [
"reporterId"
],
"isUnique": false
},
"assignee_idx": {
"name": "assignee_idx",
"columns": [
"assigneeId"
],
"isUnique": false
},
"manager_idx": {
"name": "manager_idx",
"columns": [
"managerId"
],
"isUnique": false
},
"status_idx": {
"name": "status_idx",
"columns": [
"status"
],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"occurrences_id": {
"name": "occurrences_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "varchar(25)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"firstName": {
"name": "firstName",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"lastName": {
"name": "lastName",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"user_role": {
"name": "user_role",
"type": "enum('USER','MODERATOR','ADMIN')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'USER'"
},
"password": {
"name": "password",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {
"email_idx": {
"name": "email_idx",
"columns": [
"email"
],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"users_id": {
"name": "users_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"users_email_unique": {
"name": "users_email_unique",
"columns": [
"email"
]
}
},
"checkConstraint": {}
}
},
"views": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"tables": {},
"indexes": {}
}
}
\ No newline at end of file
...@@ -8,6 +8,13 @@ ...@@ -8,6 +8,13 @@
"when": 1757350101348, "when": 1757350101348,
"tag": "0000_add_manager_to_occurrences", "tag": "0000_add_manager_to_occurrences",
"breakpoints": true "breakpoints": true
},
{
"idx": 1,
"version": "5",
"when": 1757352023356,
"tag": "0001_perpetual_human_fly",
"breakpoints": true
} }
] ]
} }
\ No newline at end of file
...@@ -32,6 +32,21 @@ export const priorityEnum = mysqlEnum('priority', [ ...@@ -32,6 +32,21 @@ export const priorityEnum = mysqlEnum('priority', [
'HIGH', 'HIGH',
'URGENT', 'URGENT',
]); ]);
export const logActionEnum = mysqlEnum('log_action', [
'CREATED',
'UPDATED',
'ASSIGNED',
'DUAL_ASSIGNED',
'UNASSIGNED',
'STATUS_CHANGED',
'PRIORITY_CHANGED',
'COMMENT_ADDED',
'ATTACHMENT_ADDED',
'ATTACHMENT_REMOVED',
'CONCLUSION_ADDED',
'CONCLUSION_UPDATED',
'DELETED',
]);
// Users table // Users table
export const users = mysqlTable( export const users = mysqlTable(
...@@ -84,6 +99,7 @@ export const comments = mysqlTable( ...@@ -84,6 +99,7 @@ export const comments = mysqlTable(
id: varchar('id', { length: 25 }).primaryKey(), id: varchar('id', { length: 25 }).primaryKey(),
content: text('content').notNull(), content: text('content').notNull(),
isInternal: boolean('isInternal').notNull().default(false), isInternal: boolean('isInternal').notNull().default(false),
status: occurrenceStatusEnum('status'),
occurrenceId: varchar('occurrenceId', { length: 25 }).notNull(), occurrenceId: varchar('occurrenceId', { length: 25 }).notNull(),
authorId: varchar('authorId', { length: 25 }).notNull(), authorId: varchar('authorId', { length: 25 }).notNull(),
createdAt: timestamp('createdAt').notNull().defaultNow(), createdAt: timestamp('createdAt').notNull().defaultNow(),
...@@ -130,12 +146,34 @@ export const conclusions = mysqlTable( ...@@ -130,12 +146,34 @@ export const conclusions = mysqlTable(
}), }),
); );
// Occurrence logs table
export const occurrenceLogs = mysqlTable(
'occurrence_logs',
{
id: varchar('id', { length: 25 }).primaryKey(),
occurrenceId: varchar('occurrenceId', { length: 25 }).notNull(),
action: logActionEnum.notNull(),
description: text('description').notNull(),
oldValue: text('oldValue'),
newValue: text('newValue'),
performedBy: varchar('performedBy', { length: 25 }).notNull(),
createdAt: timestamp('createdAt').notNull().defaultNow(),
},
(table) => ({
occurrenceIdx: index('log_occurrence_idx').on(table.occurrenceId),
performedByIdx: index('log_performed_by_idx').on(table.performedBy),
actionIdx: index('log_action_idx').on(table.action),
createdAtIdx: index('log_created_at_idx').on(table.createdAt),
}),
);
// Relations // Relations
export const usersRelations = relations(users, ({ many }) => ({ export const usersRelations = relations(users, ({ many }) => ({
reportedOccurrences: many(occurrences, { relationName: 'reportedBy' }), reportedOccurrences: many(occurrences, { relationName: 'reportedBy' }),
assignedOccurrences: many(occurrences, { relationName: 'assignedTo' }), assignedOccurrences: many(occurrences, { relationName: 'assignedTo' }),
managedOccurrences: many(occurrences, { relationName: 'managedBy' }), managedOccurrences: many(occurrences, { relationName: 'managedBy' }),
comments: many(comments), comments: many(comments),
logs: many(occurrenceLogs),
})); }));
export const occurrencesRelations = relations(occurrences, ({ one, many }) => ({ export const occurrencesRelations = relations(occurrences, ({ one, many }) => ({
...@@ -157,6 +195,7 @@ export const occurrencesRelations = relations(occurrences, ({ one, many }) => ({ ...@@ -157,6 +195,7 @@ export const occurrencesRelations = relations(occurrences, ({ one, many }) => ({
comments: many(comments), comments: many(comments),
attachments: many(attachments), attachments: many(attachments),
conclusion: one(conclusions), conclusion: one(conclusions),
logs: many(occurrenceLogs),
})); }));
export const commentsRelations = relations(comments, ({ one }) => ({ export const commentsRelations = relations(comments, ({ one }) => ({
...@@ -184,6 +223,17 @@ export const conclusionsRelations = relations(conclusions, ({ one }) => ({ ...@@ -184,6 +223,17 @@ export const conclusionsRelations = relations(conclusions, ({ one }) => ({
}), }),
})); }));
export const occurrenceLogsRelations = relations(occurrenceLogs, ({ one }) => ({
occurrence: one(occurrences, {
fields: [occurrenceLogs.occurrenceId],
references: [occurrences.id],
}),
performedBy: one(users, {
fields: [occurrenceLogs.performedBy],
references: [users.id],
}),
}));
// Type exports for TypeScript // Type exports for TypeScript
export type User = typeof users.$inferSelect; export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert; export type NewUser = typeof users.$inferInsert;
...@@ -195,6 +245,8 @@ export type Attachment = typeof attachments.$inferSelect; ...@@ -195,6 +245,8 @@ export type Attachment = typeof attachments.$inferSelect;
export type NewAttachment = typeof attachments.$inferInsert; export type NewAttachment = typeof attachments.$inferInsert;
export type Conclusion = typeof conclusions.$inferSelect; export type Conclusion = typeof conclusions.$inferSelect;
export type NewConclusion = typeof conclusions.$inferInsert; export type NewConclusion = typeof conclusions.$inferInsert;
export type OccurrenceLog = typeof occurrenceLogs.$inferSelect;
export type NewOccurrenceLog = typeof occurrenceLogs.$inferInsert;
// Enum types // Enum types
export type UserRole = 'USER' | 'MODERATOR' | 'ADMIN'; export type UserRole = 'USER' | 'MODERATOR' | 'ADMIN';
...@@ -206,6 +258,20 @@ export type OccurrenceStatus = ...@@ -206,6 +258,20 @@ export type OccurrenceStatus =
| 'PARCIAL_RESOLVED' | 'PARCIAL_RESOLVED'
| 'CANCELLED'; | 'CANCELLED';
export type Priority = 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT'; export type Priority = 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
export type LogAction =
| 'CREATED'
| 'UPDATED'
| 'ASSIGNED'
| 'DUAL_ASSIGNED'
| 'UNASSIGNED'
| 'STATUS_CHANGED'
| 'PRIORITY_CHANGED'
| 'COMMENT_ADDED'
| 'ATTACHMENT_ADDED'
| 'ATTACHMENT_REMOVED'
| 'CONCLUSION_ADDED'
| 'CONCLUSION_UPDATED'
| 'DELETED';
// Enum constants for runtime usage // Enum constants for runtime usage
export const UserRoleEnum = { export const UserRoleEnum = {
...@@ -229,3 +295,19 @@ export const PriorityEnum = { ...@@ -229,3 +295,19 @@ export const PriorityEnum = {
HIGH: 'HIGH' as const, HIGH: 'HIGH' as const,
URGENT: 'URGENT' as const, URGENT: 'URGENT' as const,
} as const; } as const;
export const LogActionEnum = {
CREATED: 'CREATED' as const,
UPDATED: 'UPDATED' as const,
ASSIGNED: 'ASSIGNED' as const,
DUAL_ASSIGNED: 'DUAL_ASSIGNED' as const,
UNASSIGNED: 'UNASSIGNED' as const,
STATUS_CHANGED: 'STATUS_CHANGED' as const,
PRIORITY_CHANGED: 'PRIORITY_CHANGED' as const,
COMMENT_ADDED: 'COMMENT_ADDED' as const,
ATTACHMENT_ADDED: 'ATTACHMENT_ADDED' as const,
ATTACHMENT_REMOVED: 'ATTACHMENT_REMOVED' as const,
CONCLUSION_ADDED: 'CONCLUSION_ADDED' as const,
CONCLUSION_UPDATED: 'CONCLUSION_UPDATED' as const,
DELETED: 'DELETED' as const,
} as const;
...@@ -46,6 +46,6 @@ async function bootstrap() { ...@@ -46,6 +46,6 @@ async function bootstrap() {
const document = SwaggerModule.createDocument(app, config); const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, document); SwaggerModule.setup('docs', app, document);
await app.listen(process.env.PORT ?? 3001); await app.listen(process.env.PORT ?? 3000);
} }
bootstrap(); bootstrap();
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { LogAction, LogActionEnum } from '../../../types';
export class LogResponseDto {
@ApiProperty({
description: 'Unique identifier for the log entry',
example: 'clxyz123abc456def',
})
id: string;
@ApiProperty({
description: 'ID of the occurrence this log entry belongs to',
example: 'clxyz123abc456def',
})
occurrenceId: string;
@ApiProperty({
description: 'Type of action that was performed',
enum: LogActionEnum,
example: LogActionEnum.STATUS_CHANGED,
})
action: LogAction;
@ApiProperty({
description: 'Human-readable description of what happened',
example: 'Status changed from OPEN to IN_PROGRESS',
})
description: string;
@ApiPropertyOptional({
description: 'Previous value before the change (if applicable)',
example: 'OPEN',
})
oldValue?: string | null;
@ApiPropertyOptional({
description: 'New value after the change (if applicable)',
example: 'IN_PROGRESS',
})
newValue?: string | null;
@ApiProperty({
description: 'ID of the user who performed the action',
example: 'clxyz789def012ghi',
})
performedBy: string;
@ApiProperty({
description: 'Date and time when the action was performed',
example: '2024-01-01T12:00:00.000Z',
})
createdAt: Date;
@ApiPropertyOptional({
description: 'User who performed the action',
type: 'object',
properties: {
id: { type: 'string', example: 'clxyz789def012ghi' },
firstName: { type: 'string', example: 'John' },
lastName: { type: 'string', example: 'Doe' },
email: { type: 'string', example: 'john.doe@example.com' },
},
})
performedByUser?: {
id: string;
firstName: string;
lastName: string;
email: string;
};
}
import { Injectable } from '@nestjs/common';
import { DrizzleService } from '../../common/drizzle.service';
import { LogResponseDto } from './dto/log-response.dto';
import { LogAction } from '../../types';
import { occurrenceLogs, users } from '../../drizzle/schema';
import { eq, desc } from 'drizzle-orm';
@Injectable()
export class OccurrenceLogService {
constructor(private readonly drizzle: DrizzleService) {}
async createLog(
occurrenceId: string,
action: LogAction,
description: string,
performedBy: string,
oldValue?: string,
newValue?: string,
): Promise<void> {
const logId = this.drizzle.generateId();
await this.drizzle.db.insert(occurrenceLogs).values({
id: logId,
occurrenceId,
action,
description,
oldValue: oldValue || null,
newValue: newValue || null,
performedBy,
});
}
async getTimeline(occurrenceId: string): Promise<LogResponseDto[]> {
const logs = await this.drizzle.db
.select({
id: occurrenceLogs.id,
occurrenceId: occurrenceLogs.occurrenceId,
action: occurrenceLogs.action,
description: occurrenceLogs.description,
oldValue: occurrenceLogs.oldValue,
newValue: occurrenceLogs.newValue,
performedBy: occurrenceLogs.performedBy,
createdAt: occurrenceLogs.createdAt,
performedByUser: {
id: users.id,
firstName: users.firstName,
lastName: users.lastName,
email: users.email,
},
})
.from(occurrenceLogs)
.leftJoin(users, eq(occurrenceLogs.performedBy, users.id))
.where(eq(occurrenceLogs.occurrenceId, occurrenceId))
.orderBy(desc(occurrenceLogs.createdAt));
return logs.map((log) => ({
id: log.id,
occurrenceId: log.occurrenceId,
action: log.action,
description: log.description,
oldValue: log.oldValue,
newValue: log.newValue,
performedBy: log.performedBy,
createdAt: log.createdAt,
performedByUser: log.performedByUser || undefined,
}));
}
// Helper methods for common logging scenarios
async logOccurrenceCreated(
occurrenceId: string,
performedBy: string,
title: string,
): Promise<void> {
await this.createLog(
occurrenceId,
'CREATED',
`Occurrence "${title}" was created`,
performedBy,
);
}
async logOccurrenceUpdated(
occurrenceId: string,
performedBy: string,
changes: string[],
): Promise<void> {
await this.createLog(
occurrenceId,
'UPDATED',
`Occurrence updated: ${changes.join(', ')}`,
performedBy,
);
}
async logStatusChanged(
occurrenceId: string,
performedBy: string,
oldStatus: string,
newStatus: string,
): Promise<void> {
await this.createLog(
occurrenceId,
'STATUS_CHANGED',
`Status changed from ${oldStatus} to ${newStatus}`,
performedBy,
oldStatus,
newStatus,
);
}
async logPriorityChanged(
occurrenceId: string,
performedBy: string,
oldPriority: string,
newPriority: string,
): Promise<void> {
await this.createLog(
occurrenceId,
'PRIORITY_CHANGED',
`Priority changed from ${oldPriority} to ${newPriority}`,
performedBy,
oldPriority,
newPriority,
);
}
async logAssignment(
occurrenceId: string,
performedBy: string,
assigneeName: string,
): Promise<void> {
await this.createLog(
occurrenceId,
'ASSIGNED',
`Occurrence assigned to ${assigneeName}`,
performedBy,
);
}
async logDualAssignment(
occurrenceId: string,
performedBy: string,
assigneeName: string,
managerName: string,
): Promise<void> {
await this.createLog(
occurrenceId,
'DUAL_ASSIGNED',
`Occurrence assigned to ${assigneeName} with manager ${managerName}`,
performedBy,
);
}
async logUnassignment(
occurrenceId: string,
performedBy: string,
): Promise<void> {
await this.createLog(
occurrenceId,
'UNASSIGNED',
'Occurrence was unassigned',
performedBy,
);
}
async logCommentAdded(
occurrenceId: string,
performedBy: string,
isInternal: boolean = false,
): Promise<void> {
const commentType = isInternal ? 'internal comment' : 'comment';
await this.createLog(
occurrenceId,
'COMMENT_ADDED',
`A ${commentType} was added`,
performedBy,
);
}
async logAttachmentAdded(
occurrenceId: string,
performedBy: string,
filename: string,
): Promise<void> {
await this.createLog(
occurrenceId,
'ATTACHMENT_ADDED',
`Attachment "${filename}" was added`,
performedBy,
);
}
async logAttachmentRemoved(
occurrenceId: string,
performedBy: string,
filename: string,
): Promise<void> {
await this.createLog(
occurrenceId,
'ATTACHMENT_REMOVED',
`Attachment "${filename}" was removed`,
performedBy,
);
}
async logConclusionAdded(
occurrenceId: string,
performedBy: string,
): Promise<void> {
await this.createLog(
occurrenceId,
'CONCLUSION_ADDED',
'A conclusion was added',
performedBy,
);
}
async logConclusionUpdated(
occurrenceId: string,
performedBy: string,
): Promise<void> {
await this.createLog(
occurrenceId,
'CONCLUSION_UPDATED',
'The conclusion was updated',
performedBy,
);
}
async logOccurrenceDeleted(
occurrenceId: string,
performedBy: string,
title: string,
): Promise<void> {
await this.createLog(
occurrenceId,
'DELETED',
`Occurrence "${title}" was deleted`,
performedBy,
);
}
}
...@@ -21,11 +21,13 @@ import { ...@@ -21,11 +21,13 @@ import {
ApiQuery, ApiQuery,
} from '@nestjs/swagger'; } from '@nestjs/swagger';
import { OccurrenceService } from './occurrence.service'; import { OccurrenceService } from './occurrence.service';
import { OccurrenceLogService } from './occurrence-log.service';
import { CreateOccurrenceDto } from './dto/create-occurrence.dto'; import { CreateOccurrenceDto } from './dto/create-occurrence.dto';
import { UpdateOccurrenceDto } from './dto/update-occurrence.dto'; import { UpdateOccurrenceDto } from './dto/update-occurrence.dto';
import { OccurrenceResponseDto } from './dto/occurrence-response.dto'; import { OccurrenceResponseDto } from './dto/occurrence-response.dto';
import { AssignOccurrenceDto } from './dto/assign-occurrence.dto'; import { AssignOccurrenceDto } from './dto/assign-occurrence.dto';
import { DualAssignOccurrenceDto } from './dto/dual-assign-occurrence.dto'; import { DualAssignOccurrenceDto } from './dto/dual-assign-occurrence.dto';
import { LogResponseDto } from './dto/log-response.dto';
import { import {
OccurrenceStatus, OccurrenceStatus,
Priority, Priority,
...@@ -36,12 +38,17 @@ import { ...@@ -36,12 +38,17 @@ import {
} from '../../types'; } from '../../types';
import { RolesGuard } from '../auth/guards/roles.guard'; import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator'; import { Roles } from '../auth/decorators/roles.decorator';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { UserResponseDto } from '../user/dto/user-response.dto';
@ApiTags('occurrences') @ApiTags('occurrences')
@ApiBearerAuth('JWT-auth') @ApiBearerAuth('JWT-auth')
@Controller('occurrences') @Controller('occurrences')
export class OccurrenceController { export class OccurrenceController {
constructor(private readonly occurrenceService: OccurrenceService) {} constructor(
private readonly occurrenceService: OccurrenceService,
private readonly occurrenceLogService: OccurrenceLogService,
) {}
@Post() @Post()
@ApiOperation({ summary: 'Create a new occurrence' }) @ApiOperation({ summary: 'Create a new occurrence' })
...@@ -308,6 +315,32 @@ export class OccurrenceController { ...@@ -308,6 +315,32 @@ export class OccurrenceController {
return this.occurrenceService.findOne(id); return this.occurrenceService.findOne(id);
} }
@Get(':id/timeline')
@ApiOperation({
summary: 'Get occurrence timeline/logs',
description:
'Returns a chronological list of all actions and changes made to the occurrence',
})
@ApiParam({
name: 'id',
description: 'Occurrence unique identifier',
example: 'clxyz123abc456def',
})
@ApiResponse({
status: 200,
description: 'Timeline of occurrence changes',
type: [LogResponseDto],
})
@ApiResponse({
status: 404,
description: 'Occurrence not found',
})
async getTimeline(@Param('id') id: string): Promise<LogResponseDto[]> {
// First check if occurrence exists
await this.occurrenceService.findOne(id);
return this.occurrenceLogService.getTimeline(id);
}
@Patch(':id') @Patch(':id')
@ApiOperation({ summary: 'Update occurrence by ID' }) @ApiOperation({ summary: 'Update occurrence by ID' })
@ApiParam({ @ApiParam({
...@@ -368,8 +401,13 @@ export class OccurrenceController { ...@@ -368,8 +401,13 @@ export class OccurrenceController {
async assignOccurrence( async assignOccurrence(
@Param('id') id: string, @Param('id') id: string,
@Body() assignOccurrenceDto: AssignOccurrenceDto, @Body() assignOccurrenceDto: AssignOccurrenceDto,
@CurrentUser() user: UserResponseDto,
): Promise<OccurrenceResponseDto> { ): Promise<OccurrenceResponseDto> {
return this.occurrenceService.assignOccurrence(id, assignOccurrenceDto); return this.occurrenceService.assignOccurrence(
id,
assignOccurrenceDto,
user.id,
);
} }
@Patch(':id/dual-assign') @Patch(':id/dual-assign')
...@@ -407,10 +445,12 @@ export class OccurrenceController { ...@@ -407,10 +445,12 @@ export class OccurrenceController {
async dualAssignOccurrence( async dualAssignOccurrence(
@Param('id') id: string, @Param('id') id: string,
@Body() dualAssignOccurrenceDto: DualAssignOccurrenceDto, @Body() dualAssignOccurrenceDto: DualAssignOccurrenceDto,
@CurrentUser() user: UserResponseDto,
): Promise<OccurrenceResponseDto> { ): Promise<OccurrenceResponseDto> {
return this.occurrenceService.dualAssignOccurrence( return this.occurrenceService.dualAssignOccurrence(
id, id,
dualAssignOccurrenceDto, dualAssignOccurrenceDto,
user.id,
); );
} }
...@@ -438,8 +478,9 @@ export class OccurrenceController { ...@@ -438,8 +478,9 @@ export class OccurrenceController {
}) })
async unassignOccurrence( async unassignOccurrence(
@Param('id') id: string, @Param('id') id: string,
@CurrentUser() user: UserResponseDto,
): Promise<OccurrenceResponseDto> { ): Promise<OccurrenceResponseDto> {
return this.occurrenceService.unassignOccurrence(id); return this.occurrenceService.unassignOccurrence(id, user.id);
} }
@Delete(':id') @Delete(':id')
......
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { OccurrenceService } from './occurrence.service'; import { OccurrenceService } from './occurrence.service';
import { OccurrenceController } from './occurrence.controller'; import { OccurrenceController } from './occurrence.controller';
import { OccurrenceLogService } from './occurrence-log.service';
import { DrizzleService } from '../../common/drizzle.service'; import { DrizzleService } from '../../common/drizzle.service';
@Module({ @Module({
controllers: [OccurrenceController], controllers: [OccurrenceController],
providers: [OccurrenceService, DrizzleService], providers: [OccurrenceService, OccurrenceLogService, DrizzleService],
exports: [OccurrenceService], // Export service for use in other modules exports: [OccurrenceService, OccurrenceLogService], // Export services for use in other modules
}) })
export class OccurrenceModule {} export class OccurrenceModule {}
...@@ -9,6 +9,7 @@ import { UpdateOccurrenceDto } from './dto/update-occurrence.dto'; ...@@ -9,6 +9,7 @@ import { UpdateOccurrenceDto } from './dto/update-occurrence.dto';
import { OccurrenceResponseDto } from './dto/occurrence-response.dto'; import { OccurrenceResponseDto } from './dto/occurrence-response.dto';
import { AssignOccurrenceDto } from './dto/assign-occurrence.dto'; import { AssignOccurrenceDto } from './dto/assign-occurrence.dto';
import { DualAssignOccurrenceDto } from './dto/dual-assign-occurrence.dto'; import { DualAssignOccurrenceDto } from './dto/dual-assign-occurrence.dto';
import { OccurrenceLogService } from './occurrence-log.service';
import { import {
OccurrenceStatus, OccurrenceStatus,
Priority, Priority,
...@@ -26,7 +27,10 @@ import { eq, and, or, like, desc, count, sql } from 'drizzle-orm'; ...@@ -26,7 +27,10 @@ import { eq, and, or, like, desc, count, sql } from 'drizzle-orm';
@Injectable() @Injectable()
export class OccurrenceService { export class OccurrenceService {
constructor(private readonly drizzle: DrizzleService) {} constructor(
private readonly drizzle: DrizzleService,
private readonly logService: OccurrenceLogService,
) {}
private async getOccurrenceWithRelations( private async getOccurrenceWithRelations(
occurrenceId: string, occurrenceId: string,
...@@ -162,6 +166,13 @@ export class OccurrenceService { ...@@ -162,6 +166,13 @@ export class OccurrenceService {
...createOccurrenceDto, ...createOccurrenceDto,
}); });
// Log the creation
await this.logService.logOccurrenceCreated(
occurrenceId,
createOccurrenceDto.reporterId,
createOccurrenceDto.title,
);
// Get the full occurrence with relations // Get the full occurrence with relations
const occurrence = await this.getOccurrenceWithRelations(occurrenceId); const occurrence = await this.getOccurrenceWithRelations(occurrenceId);
...@@ -445,6 +456,7 @@ export class OccurrenceService { ...@@ -445,6 +456,7 @@ export class OccurrenceService {
async assignOccurrence( async assignOccurrence(
occurrenceId: string, occurrenceId: string,
assignOccurrenceDto: AssignOccurrenceDto, assignOccurrenceDto: AssignOccurrenceDto,
performedBy: string,
): Promise<OccurrenceResponseDto> { ): Promise<OccurrenceResponseDto> {
// Check if occurrence exists // Check if occurrence exists
await this.findOne(occurrenceId); await this.findOne(occurrenceId);
...@@ -481,11 +493,19 @@ export class OccurrenceService { ...@@ -481,11 +493,19 @@ export class OccurrenceService {
}); });
} }
// Log the assignment
await this.logService.logAssignment(
occurrenceId,
performedBy,
`${assignee.firstName} ${assignee.lastName}`,
);
return this.getOccurrenceWithRelations(occurrenceId); return this.getOccurrenceWithRelations(occurrenceId);
} }
async unassignOccurrence( async unassignOccurrence(
occurrenceId: string, occurrenceId: string,
performedBy: string,
): Promise<OccurrenceResponseDto> { ): Promise<OccurrenceResponseDto> {
// Check if occurrence exists // Check if occurrence exists
await this.findOne(occurrenceId); await this.findOne(occurrenceId);
...@@ -499,12 +519,16 @@ export class OccurrenceService { ...@@ -499,12 +519,16 @@ export class OccurrenceService {
}) })
.where(eq(occurrences.id, occurrenceId)); .where(eq(occurrences.id, occurrenceId));
// Log the unassignment
await this.logService.logUnassignment(occurrenceId, performedBy);
return this.getOccurrenceWithRelations(occurrenceId); return this.getOccurrenceWithRelations(occurrenceId);
} }
async dualAssignOccurrence( async dualAssignOccurrence(
occurrenceId: string, occurrenceId: string,
dualAssignOccurrenceDto: DualAssignOccurrenceDto, dualAssignOccurrenceDto: DualAssignOccurrenceDto,
performedBy: string,
): Promise<OccurrenceResponseDto> { ): Promise<OccurrenceResponseDto> {
// Check if occurrence exists // Check if occurrence exists
await this.findOne(occurrenceId); await this.findOne(occurrenceId);
...@@ -553,6 +577,14 @@ export class OccurrenceService { ...@@ -553,6 +577,14 @@ export class OccurrenceService {
}); });
} }
// Log the dual assignment
await this.logService.logDualAssignment(
occurrenceId,
performedBy,
`${assignee.firstName} ${assignee.lastName}`,
`${manager.firstName} ${manager.lastName}`,
);
return this.getOccurrenceWithRelations(occurrenceId); return this.getOccurrenceWithRelations(occurrenceId);
} }
} }
...@@ -8,6 +8,20 @@ export type OccurrenceStatus = ...@@ -8,6 +8,20 @@ export type OccurrenceStatus =
| 'PARCIAL_RESOLVED' | 'PARCIAL_RESOLVED'
| 'CANCELLED'; | 'CANCELLED';
export type Priority = 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT'; export type Priority = 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
export type LogAction =
| 'CREATED'
| 'UPDATED'
| 'ASSIGNED'
| 'DUAL_ASSIGNED'
| 'UNASSIGNED'
| 'STATUS_CHANGED'
| 'PRIORITY_CHANGED'
| 'COMMENT_ADDED'
| 'ATTACHMENT_ADDED'
| 'ATTACHMENT_REMOVED'
| 'CONCLUSION_ADDED'
| 'CONCLUSION_UPDATED'
| 'DELETED';
// Enum constants for runtime usage // Enum constants for runtime usage
export const UserRoleEnum = { export const UserRoleEnum = {
...@@ -31,3 +45,19 @@ export const PriorityEnum = { ...@@ -31,3 +45,19 @@ export const PriorityEnum = {
HIGH: 'HIGH' as const, HIGH: 'HIGH' as const,
URGENT: 'URGENT' as const, URGENT: 'URGENT' as const,
} as const; } as const;
export const LogActionEnum = {
CREATED: 'CREATED' as const,
UPDATED: 'UPDATED' as const,
ASSIGNED: 'ASSIGNED' as const,
DUAL_ASSIGNED: 'DUAL_ASSIGNED' as const,
UNASSIGNED: 'UNASSIGNED' as const,
STATUS_CHANGED: 'STATUS_CHANGED' as const,
PRIORITY_CHANGED: 'PRIORITY_CHANGED' as const,
COMMENT_ADDED: 'COMMENT_ADDED' as const,
ATTACHMENT_ADDED: 'ATTACHMENT_ADDED' as const,
ATTACHMENT_REMOVED: 'ATTACHMENT_REMOVED' as const,
CONCLUSION_ADDED: 'CONCLUSION_ADDED' as const,
CONCLUSION_UPDATED: 'CONCLUSION_UPDATED' as const,
DELETED: 'DELETED' as const,
} as const;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment