Commit e83d0f8c authored by Administrator's avatar Administrator

Update 12 files via Son of Anton

parent ef9a81cb
...@@ -52,6 +52,9 @@ import { UnavailabilityModule } from './modules/unavailability/unavailability.mo ...@@ -52,6 +52,9 @@ import { UnavailabilityModule } from './modules/unavailability/unavailability.mo
import { SchedulesModule } from './modules/schedules/schedules.module'; import { SchedulesModule } from './modules/schedules/schedules.module';
import { MeetingsModule } from './modules/meetings/meetings.module'; import { MeetingsModule } from './modules/meetings/meetings.module';
// ─── Phase 2D: Reports & Daily Operations ───────────────────
import { ReportsModule } from './modules/reports/reports.module';
import { JwtAuthGuard } from './common/guards/jwt-auth.guard'; import { JwtAuthGuard } from './common/guards/jwt-auth.guard';
import { RolesGuard } from './common/guards/roles.guard'; import { RolesGuard } from './common/guards/roles.guard';
import { TransformInterceptor } from './common/interceptors/transform.interceptor'; import { TransformInterceptor } from './common/interceptors/transform.interceptor';
...@@ -103,6 +106,8 @@ import { RateLimitMiddleware } from './common/middleware/rate-limit.middleware'; ...@@ -103,6 +106,8 @@ import { RateLimitMiddleware } from './common/middleware/rate-limit.middleware';
UnavailabilityModule, UnavailabilityModule,
SchedulesModule, SchedulesModule,
MeetingsModule, MeetingsModule,
// Phase 2D
ReportsModule,
], ],
providers: [ providers: [
{ provide: APP_GUARD, useClass: JwtAuthGuard }, { provide: APP_GUARD, useClass: JwtAuthGuard },
......
import {
IsString,
IsOptional,
IsArray,
ValidateNested,
MinLength,
} from 'class-validator';
import { Type } from 'class-transformer';
import { ReportTaskEntryDto } from './create-report.dto';
export class CreateAmendmentDto {
@IsString()
reportId: string;
@IsString()
@MinLength(30, { message: 'Amendment reason must be at least 30 characters' })
reason: string;
@IsArray()
@ValidateNested({ each: true })
@Type(() => ReportTaskEntryDto)
amendedTaskEntries: ReportTaskEntryDto[];
@IsOptional()
@IsString()
amendedBlockers?: string;
@IsOptional()
@IsString()
amendedNotes?: string;
}
export class ReviewAmendmentDto {
@IsString()
decision: string; // APPROVED, REJECTED
@IsOptional()
@IsString()
notes?: string;
}
\ No newline at end of file
import {
IsString,
IsOptional,
IsArray,
IsInt,
IsDateString,
IsBoolean,
ValidateNested,
Min,
MinLength,
MaxLength,
} from 'class-validator';
import { Type } from 'class-transformer';
export class ReportTaskEntryDto {
@IsOptional()
@IsString()
cardId?: string;
@IsString()
@MinLength(50, { message: 'Work description must be at least 50 characters' })
workDescription: string;
@IsInt()
@Min(15, { message: 'Minimum time per task is 15 minutes' })
timeMinutes: number;
@IsString()
taskStatus: string; // IN_PROGRESS, COMPLETED, BLOCKED
}
export class CreateReportDto {
@IsDateString()
reportDate: string;
@IsArray()
@ValidateNested({ each: true })
@Type(() => ReportTaskEntryDto)
taskEntries: ReportTaskEntryDto[];
@IsOptional()
@IsString()
blockers?: string;
@IsOptional()
@IsString()
additionalNotes?: string;
@IsOptional()
@IsString()
mood?: string; // FRUSTRATED, NEUTRAL, GOOD, ON_FIRE
}
export class SaveDraftDto {
@IsDateString()
reportDate: string;
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => DraftTaskEntryDto)
taskEntries?: DraftTaskEntryDto[];
@IsOptional()
@IsString()
blockers?: string;
@IsOptional()
@IsString()
additionalNotes?: string;
@IsOptional()
@IsString()
mood?: string;
}
export class DraftTaskEntryDto {
@IsOptional()
@IsString()
cardId?: string;
@IsOptional()
@IsString()
workDescription?: string;
@IsOptional()
@IsInt()
@Min(0)
timeMinutes?: number;
@IsOptional()
@IsString()
taskStatus?: string;
}
\ No newline at end of file
import { IsOptional, IsString, IsDateString, IsBoolean } from 'class-validator';
import { Type } from 'class-transformer';
import { PaginationDto } from '../../../common/dto/pagination.dto';
export class ReportFilterDto extends PaginationDto {
@IsOptional()
@IsString()
userId?: string;
@IsOptional()
@IsString()
status?: string;
@IsOptional()
@IsDateString()
dateFrom?: string;
@IsOptional()
@IsDateString()
dateTo?: string;
@IsOptional()
@IsString()
boardId?: string;
@IsOptional()
@Type(() => Boolean)
@IsBoolean()
isLate?: boolean;
@IsOptional()
@IsString()
reviewedById?: string;
}
\ No newline at end of file
export class ReportResponseDto {
id: string;
userId: string;
reportDate: string;
status: string;
totalMinutes: number;
totalHours: number;
isLate: boolean;
submittedAt: string | null;
autoApproved: boolean;
blockers: string | null;
additionalNotes: string | null;
mood: string | null;
flagReason: string | null;
reviewedAt: string | null;
reviewNotes: string | null;
user?: {
id: string;
firstName: string;
lastName: string;
avatar: string | null;
};
reviewedBy?: {
id: string;
firstName: string;
lastName: string;
} | null;
taskEntries: TaskEntryResponseDto[];
amendmentCount: number;
createdAt: string;
updatedAt: string;
}
export class TaskEntryResponseDto {
id: string;
cardId: string | null;
cardNumber: string | null;
cardTitle: string | null;
workDescription: string;
timeMinutes: number;
taskStatus: string;
}
export class ReviewDashboardDto {
reportedToday: { userId: string; name: string; status: string }[];
notReportedToday: { userId: string; name: string }[];
pendingReview: number;
flaggedThisMonth: number;
}
\ No newline at end of file
import { IsString, IsOptional, IsArray, MinLength } from 'class-validator';
export class ReviewReportDto {
@IsString()
action: string; // APPROVE, FLAG_VAGUE, FLAG_INCONSISTENT, REQUEST_REVISION
@IsOptional()
@IsString()
@MinLength(10)
notes?: string;
}
export class BulkApproveDto {
@IsArray()
@IsString({ each: true })
reportIds: string[];
}
\ No newline at end of file
import {
Injectable,
NotFoundException,
ForbiddenException,
BadRequestException,
Logger,
} from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { NotificationsService } from '../notifications/notifications.service';
import { CreateAmendmentDto, ReviewAmendmentDto } from './dto/create-amendment.dto';
import { RequestUser } from '../../common/decorators/current-user.decorator';
@Injectable()
export class ReportAmendmentService {
private readonly logger = new Logger(ReportAmendmentService.name);
constructor(
private readonly prisma: PrismaService,
private readonly notificationsService: NotificationsService,
) {}
async create(dto: CreateAmendmentDto, currentUser: RequestUser): Promise<any> {
const report = await this.prisma.dailyReport.findUnique({
where: { id: dto.reportId },
include: { user: { select: { id: true, firstName: true, lastName: true } } },
});
if (!report) throw new NotFoundException('Report not found');
if (report.userId !== currentUser.id) {
throw new ForbiddenException('You can only submit amendments for your own reports');
}
// Can only amend submitted/approved/flagged reports (not drafts or unreported)
const amendableStatuses = [
'SUBMITTED', 'LATE', 'APPROVED', 'AUTO_APPROVED',
'FLAGGED_VAGUE', 'FLAGGED_INCONSISTENT', 'REVISION_REQUESTED',
];
if (!amendableStatuses.includes(report.status)) {
throw new BadRequestException(`Reports in "${report.status}" status cannot be amended`);
}
// Amendment window: 7 calendar days from report date
const reportDate = new Date(report.reportDate);
const windowEnd = new Date(reportDate);
windowEnd.setDate(windowEnd.getDate() + 7);
if (new Date() > windowEnd) {
throw new BadRequestException(
'Amendment window has expired. Reports can only be amended within 7 calendar days of the report date.',
);
}
// Validate amended task entries
if (!dto.amendedTaskEntries || dto.amendedTaskEntries.length === 0) {
throw new BadRequestException('At least one task entry is required in the amendment');
}
for (const entry of dto.amendedTaskEntries) {
if (!entry.workDescription || entry.workDescription.length < 50) {
throw new BadRequestException('Each task description must be at least 50 characters');
}
if (entry.timeMinutes < 15) {
throw new BadRequestException('Minimum time per task is 15 minutes');
}
}
// Store amended entries as JSON
const amendedContent = dto.amendedTaskEntries.map((entry, i) => ({
cardId: entry.cardId || null,
workDescription: entry.workDescription,
timeMinutes: entry.timeMinutes,
taskStatus: entry.taskStatus,
position: i,
}));
const amendment = await this.prisma.reportAmendment.create({
data: {
reportId: dto.reportId,
requestedById: currentUser.id,
reason: dto.reason,
amendedTaskEntries: amendedContent,
amendedBlockers: dto.amendedBlockers || null,
amendedNotes: dto.amendedNotes || null,
status: 'PENDING',
},
include: {
requestedBy: { select: { id: true, firstName: true, lastName: true } },
},
});
// Notify reviewers (PL + Admin)
const notifyUsers = await this.prisma.user.findMany({
where: {
OR: [
{ role: { in: ['SUPER_ADMIN', 'ADMIN'] }, status: 'ACTIVE', deletedAt: null },
{ id: report.user?.id ? undefined : 'none' }, // PL
],
},
select: { id: true },
});
// Also notify the assigned PL
const contractor = await this.prisma.user.findUnique({
where: { id: report.userId },
select: { assignedProjectLeaderId: true },
});
if (contractor?.assignedProjectLeaderId) {
notifyUsers.push({ id: contractor.assignedProjectLeaderId });
}
const uniqueNotifyIds = [...new Set(notifyUsers.map((u) => u.id))];
for (const userId of uniqueNotifyIds) {
try {
await this.notificationsService.create({
userId,
type: 'IMPORTANT',
category: 'REPORT',
title: 'Report Amendment Submitted',
message: `${report.user?.firstName} submitted an amendment for their ${report.reportDate.toISOString().split('T')[0]} report. Reason: ${dto.reason.substring(0, 100)}`,
actionUrl: '/reports/review',
entityType: 'reportAmendment',
entityId: amendment.id,
});
} catch { /* non-critical */ }
}
this.logger.log(`Amendment submitted for report ${dto.reportId} by ${currentUser.email}`);
return amendment;
}
async review(
amendmentId: string,
dto: ReviewAmendmentDto,
currentUser: RequestUser,
): Promise<any> {
if (currentUser.role === 'CONTRACTOR') {
throw new ForbiddenException('Contractors cannot review amendments');
}
if (!['APPROVED', 'REJECTED'].includes(dto.decision)) {
throw new BadRequestException('Decision must be APPROVED or REJECTED');
}
const amendment = await this.prisma.reportAmendment.findUnique({
where: { id: amendmentId },
include: {
report: {
include: {
user: { select: { id: true, firstName: true, lastName: true, assignedProjectLeaderId: true } },
},
},
},
});
if (!amendment) throw new NotFoundException('Amendment not found');
if (amendment.status !== 'PENDING') {
throw new BadRequestException('This amendment has already been reviewed');
}
// PL can only review their team
if (currentUser.role === 'TEAM_LEAD') {
if (amendment.report.user?.assignedProjectLeaderId !== currentUser.id) {
throw new ForbiddenException('You can only review amendments from your team');
}
}
const updated = await this.prisma.reportAmendment.update({
where: { id: amendmentId },
data: {
status: dto.decision,
reviewedById: currentUser.id,
reviewedAt: new Date(),
reviewNotes: dto.notes || null,
},
});
if (dto.decision === 'APPROVED') {
// Apply the amendment: update the report's task entries
const report = amendment.report;
const amendedEntries = amendment.amendedTaskEntries as any[];
// Delete old task entries
await this.prisma.reportTaskEntry.deleteMany({
where: { reportId: report.id },
});
// Create new entries from amendment
let totalMinutes = 0;
for (const entry of amendedEntries) {
let cardNumber: string | null = null;
let cardTitle: string | null = null;
if (entry.cardId) {
const card = await this.prisma.card.findUnique({
where: { id: entry.cardId },
select: { cardNumber: true, title: true },
});
if (card) {
cardNumber = card.cardNumber;
cardTitle = card.title;
}
}
await this.prisma.reportTaskEntry.create({
data: {
reportId: report.id,
cardId: entry.cardId || null,
cardNumber,
cardTitle,
workDescription: entry.workDescription,
timeMinutes: entry.timeMinutes,
taskStatus: entry.taskStatus,
position: entry.position || 0,
},
});
totalMinutes += entry.timeMinutes;
}
// Update report status and content
await this.prisma.dailyReport.update({
where: { id: report.id },
data: {
status: 'AMENDED',
totalMinutes,
blockers: amendment.amendedBlockers ?? report.blockers,
additionalNotes: amendment.amendedNotes ?? report.additionalNotes,
},
});
this.logger.log(`Amendment ${amendmentId} approved — report ${report.id} updated`);
}
// Notify the contractor
try {
await this.notificationsService.create({
userId: amendment.report.userId,
type: 'IMPORTANT',
category: 'REPORT',
title: `Amendment ${dto.decision === 'APPROVED' ? 'Approved' : 'Rejected'}`,
message: `Your amendment for the ${amendment.report.reportDate.toISOString().split('T')[0]} report was ${dto.decision.toLowerCase()}.${dto.notes ? ` Notes: ${dto.notes}` : ''}`,
actionUrl: '/reports',
entityType: 'reportAmendment',
entityId: amendmentId,
});
} catch { /* non-critical */ }
this.logger.log(`Amendment ${amendmentId} ${dto.decision} by ${currentUser.email}`);
return updated;
}
async findByReport(reportId: string, currentUser: RequestUser): Promise<any[]> {
const report = await this.prisma.dailyReport.findUnique({ where: { id: reportId } });
if (!report) throw new NotFoundException('Report not found');
if (currentUser.role === 'CONTRACTOR' && report.userId !== currentUser.id) {
throw new ForbiddenException('You can only view amendments for your own reports');
}
return this.prisma.reportAmendment.findMany({
where: { reportId },
orderBy: { createdAt: 'desc' },
include: {
requestedBy: { select: { id: true, firstName: true, lastName: true } },
reviewedBy: { select: { id: true, firstName: true, lastName: true } },
},
});
}
}
\ No newline at end of file
This diff is collapsed.
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
Query,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { ReportsService } from './reports.service';
import { ReportReviewService } from './report-review.service';
import { ReportAmendmentService } from './report-amendment.service';
import { CreateReportDto, SaveDraftDto } from './dto/create-report.dto';
import { ReviewReportDto, BulkApproveDto } from './dto/review-report.dto';
import { CreateAmendmentDto, ReviewAmendmentDto } from './dto/create-amendment.dto';
import { ReportFilterDto } from './dto/report-filter.dto';
import { CurrentUser, RequestUser } from '../../common/decorators/current-user.decorator';
import { Roles } from '../../common/decorators/roles.decorator';
@Controller('reports')
export class ReportsController {
constructor(
private readonly reportsService: ReportsService,
private readonly reviewService: ReportReviewService,
private readonly amendmentService: ReportAmendmentService,
) {}
// ─── SUBMISSION ──────────────────────────────────────
@Post('draft')
async saveDraft(@Body() dto: SaveDraftDto, @CurrentUser() user: RequestUser) {
return this.reportsService.saveDraft(dto, user);
}
@Post()
async createAndSubmit(@Body() dto: CreateReportDto, @CurrentUser() user: RequestUser) {
return this.reportsService.createAndSubmit(dto, user);
}
@Post(':id/submit')
@HttpCode(HttpStatus.OK)
async submit(@Param('id') id: string, @CurrentUser() user: RequestUser) {
return this.reportsService.submit(id, user);
}
// ─── READING ─────────────────────────────────────────
@Get()
async findAll(@Query() filter: ReportFilterDto, @CurrentUser() user: RequestUser) {
return this.reportsService.findAll(filter, user);
}
@Get('today')
async getTodayStatus(@CurrentUser() user: RequestUser) {
return this.reportsService.getTodayStatus(user);
}
@Get('my-cards')
async getMyAssignedCards(@CurrentUser() user: RequestUser) {
return this.reportsService.getMyAssignedCards(user);
}
@Get('stats/:userId')
async getReportingStats(
@Param('userId') userId: string,
@Query('month') month: string,
@Query('year') year: string,
@CurrentUser() user: RequestUser,
) {
// Contractors can only view own stats
if (user.role === 'CONTRACTOR' && userId !== user.id) {
return { expected: 0, reported: 0, onTime: 0, late: 0, unreported: 0, flagged: 0, approved: 0 };
}
return this.reportsService.getReportingStats(
userId,
month ? parseInt(month, 10) : new Date().getMonth() + 1,
year ? parseInt(year, 10) : new Date().getFullYear(),
);
}
@Get('date/:date')
async getMyReportForDate(@Param('date') date: string, @CurrentUser() user: RequestUser) {
return this.reportsService.getMyReportForDate(date, user);
}
@Get(':id')
async findById(@Param('id') id: string, @CurrentUser() user: RequestUser) {
return this.reportsService.findById(id, user);
}
// ─── REVIEW ──────────────────────────────────────────
@Get('review/dashboard')
@Roles('SUPER_ADMIN', 'ADMIN', 'TEAM_LEAD')
async getReviewDashboard(@CurrentUser() user: RequestUser) {
return this.reviewService.getReviewDashboard(user);
}
@Put(':id/review')
@Roles('SUPER_ADMIN', 'ADMIN', 'TEAM_LEAD')
@HttpCode(HttpStatus.OK)
async reviewReport(
@Param('id') id: string,
@Body() dto: ReviewReportDto,
@CurrentUser() user: RequestUser,
) {
switch (dto.action) {
case 'APPROVE':
return this.reviewService.approve(id, dto.notes, user);
case 'FLAG_VAGUE':
return this.reviewService.flagVague(id, dto.notes, user);
case 'FLAG_INCONSISTENT':
return this.reviewService.flagInconsistent(id, dto.notes, user);
case 'REQUEST_REVISION':
return this.reviewService.requestRevision(id, dto.notes, user);
default:
throw new Error(`Unknown review action: ${dto.action}`);
}
}
@Post('review/bulk-approve')
@Roles('SUPER_ADMIN', 'ADMIN', 'TEAM_LEAD')
@HttpCode(HttpStatus.OK)
async bulkApprove(@Body() dto: BulkApproveDto, @CurrentUser() user: RequestUser) {
return this.reviewService.bulkApprove(dto.reportIds, user);
}
// ─── AMENDMENTS ──────────────────────────────────────
@Post('amendments')
async createAmendment(@Body() dto: CreateAmendmentDto, @CurrentUser() user: RequestUser) {
return this.amendmentService.create(dto, user);
}
@Put('amendments/:id/review')
@Roles('SUPER_ADMIN', 'ADMIN', 'TEAM_LEAD')
@HttpCode(HttpStatus.OK)
async reviewAmendment(
@Param('id') id: string,
@Body() dto: ReviewAmendmentDto,
@CurrentUser() user: RequestUser,
) {
return this.amendmentService.review(id, dto, user);
}
@Get(':id/amendments')
async getAmendments(@Param('id') id: string, @CurrentUser() user: RequestUser) {
return this.amendmentService.findByReport(id, user);
}
// ─── ADMIN ACTIONS ───────────────────────────────────
@Put(':id')
@Roles('SUPER_ADMIN')
async updateReport(@Param('id') id: string, @Body() data: any, @CurrentUser() user: RequestUser) {
const report = await this.reportsService.findById(id, user);
if (!report) throw new Error('Report not found');
const updateData: any = {};
if (data.status !== undefined) updateData.status = data.status;
if (data.reviewNotes !== undefined) updateData.reviewNotes = data.reviewNotes;
const prisma = (this.reportsService as any).prisma;
return prisma.dailyReport.update({ where: { id }, data: updateData });
}
@Delete(':id')
@Roles('SUPER_ADMIN')
@HttpCode(HttpStatus.OK)
async deleteReport(@Param('id') id: string, @CurrentUser() user: RequestUser) {
const prisma = (this.reportsService as any).prisma;
const report = await prisma.dailyReport.findUnique({ where: { id } });
if (!report) throw new Error('Report not found');
// Delete task entries and amendments first
await prisma.reportTaskEntry.deleteMany({ where: { reportId: id } });
await prisma.reportAmendment.deleteMany({ where: { reportId: id } });
await prisma.dailyReport.delete({ where: { id } });
return { message: 'Report deleted' };
}
}
\ No newline at end of file
import { Module } from '@nestjs/common';
import { ReportsController } from './reports.controller';
import { ReportsService } from './reports.service';
import { ReportReviewService } from './report-review.service';
import { ReportAmendmentService } from './report-amendment.service';
import { NotificationsModule } from '../notifications/notifications.module';
@Module({
imports: [NotificationsModule],
controllers: [ReportsController],
providers: [ReportsService, ReportReviewService, ReportAmendmentService],
exports: [ReportsService, ReportReviewService],
})
export class ReportsModule {}
\ No newline at end of file
This diff is collapsed.
// ─── Phase 2D: Daily Reports & Check-in ─────────────────────
// NOTE: After adding this file, you must add reverse relations
// to the User model in schema.prisma:
// reports DailyReport[] @relation("UserReports")
// reviewedReports DailyReport[] @relation("ReportReviewer")
// amendmentRequests ReportAmendment[] @relation("AmendmentRequester")
// amendmentReviews ReportAmendment[] @relation("AmendmentReviewer")
//
// And to the Card model in schema-boards.prisma:
// reportTaskEntries ReportTaskEntry[]
model DailyReport {
id String @id @default(uuid())
userId String
user User @relation("UserReports", fields: [userId], references: [id], onDelete: Cascade)
reportDate DateTime @db.Date
status String @default("DRAFT")
// DRAFT, SUBMITTED, LATE, APPROVED, AUTO_APPROVED,
// FLAGGED_VAGUE, FLAGGED_INCONSISTENT, REVISION_REQUESTED,
// AMENDED, UNREPORTED
// ─── Task Entries ──────────────────────────────────────
taskEntries ReportTaskEntry[]
// ─── Content Fields ────────────────────────────────────
blockers String?
additionalNotes String?
mood String? // FRUSTRATED, NEUTRAL, GOOD, ON_FIRE
// ─── Computed ──────────────────────────────────────────
totalMinutes Int @default(0)
// ─── Submission Timing ─────────────────────────────────
submittedAt DateTime?
isLate Boolean @default(false)
// ─── Review ────────────────────────────────────────────
reviewedById String?
reviewedBy User? @relation("ReportReviewer", fields: [reviewedById], references: [id], onDelete: SetNull)
reviewedAt DateTime?
reviewNotes String?
flagReason String?
flagCount Int @default(0) // How many times this report has been flagged
// ─── Auto-Approval ────────────────────────────────────
autoApproved Boolean @default(false)
// ─── Amendments ────────────────────────────────────────
amendments ReportAmendment[]
// ─── Draft ─────────────────────────────────────────────
lastDraftSavedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([userId, reportDate])
@@index([userId])
@@index([status])
@@index([reportDate])
@@index([reviewedById])
@@index([submittedAt])
}
model ReportTaskEntry {
id String @id @default(uuid())
reportId String
report DailyReport @relation(fields: [reportId], references: [id], onDelete: Cascade)
cardId String?
card Card? @relation(fields: [cardId], references: [id], onDelete: SetNull)
// Snapshot fields preserved even if card is deleted/renamed
cardNumber String?
cardTitle String?
workDescription String // Min 50 characters per spec
timeMinutes Int // Min 15 (0:15 in 15-min increments)
taskStatus String // IN_PROGRESS, COMPLETED, BLOCKED
position Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([reportId])
@@index([cardId])
}
model ReportAmendment {
id String @id @default(uuid())
reportId String
report DailyReport @relation(fields: [reportId], references: [id], onDelete: Cascade)
requestedById String
requestedBy User @relation("AmendmentRequester", fields: [requestedById], references: [id], onDelete: Cascade)
reason String // Min 30 chars
amendedTaskEntries Json // Array of amended task entry objects
amendedBlockers String?
amendedNotes String?
status String @default("PENDING") // PENDING, APPROVED, REJECTED
reviewedById String?
reviewedBy User? @relation("AmendmentReviewer", fields: [reviewedById], references: [id], onDelete: SetNull)
reviewedAt DateTime?
reviewNotes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([reportId])
@@index([requestedById])
@@index([status])
}
\ No newline at end of file
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 to comment