Commit ef9a81cb authored by Administrator's avatar Administrator

Update 27 files via Son of Anton

parent 8494e6c4
...@@ -46,6 +46,12 @@ import { EvaluationsModule } from './modules/evaluations/evaluations.module'; ...@@ -46,6 +46,12 @@ import { EvaluationsModule } from './modules/evaluations/evaluations.module';
import { PIPModule } from './modules/pip/pip.module'; import { PIPModule } from './modules/pip/pip.module';
import { LearningModule } from './modules/learning/learning.module'; import { LearningModule } from './modules/learning/learning.module';
// ─── Phase 2C: Time & Scheduling ────────────────────────────
import { HolidaysModule } from './modules/holidays/holidays.module';
import { UnavailabilityModule } from './modules/unavailability/unavailability.module';
import { SchedulesModule } from './modules/schedules/schedules.module';
import { MeetingsModule } from './modules/meetings/meetings.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';
...@@ -92,6 +98,11 @@ import { RateLimitMiddleware } from './common/middleware/rate-limit.middleware'; ...@@ -92,6 +98,11 @@ import { RateLimitMiddleware } from './common/middleware/rate-limit.middleware';
EvaluationsModule, EvaluationsModule,
PIPModule, PIPModule,
LearningModule, LearningModule,
// Phase 2C
HolidaysModule,
UnavailabilityModule,
SchedulesModule,
MeetingsModule,
], ],
providers: [ providers: [
{ provide: APP_GUARD, useClass: JwtAuthGuard }, { provide: APP_GUARD, useClass: JwtAuthGuard },
......
import { IsString, IsBoolean, IsOptional, IsDateString, MinLength, MaxLength } from 'class-validator';
export class CreateHolidayDto {
@IsString()
@MinLength(2)
@MaxLength(200)
name: string;
@IsDateString()
startDate: string;
@IsDateString()
endDate: string;
@IsOptional()
@IsBoolean()
isRecurring?: boolean;
@IsOptional()
@IsString()
notes?: 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 HolidayFilterDto extends PaginationDto {
@IsOptional()
@IsDateString()
dateFrom?: string;
@IsOptional()
@IsDateString()
dateTo?: string;
@IsOptional()
@Type(() => Boolean)
@IsBoolean()
isRecurring?: boolean;
@IsOptional()
@IsString()
year?: string;
}
\ No newline at end of file
import { IsString, IsBoolean, IsOptional, IsDateString, MinLength, MaxLength } from 'class-validator';
export class UpdateHolidayDto {
@IsOptional()
@IsString()
@MinLength(2)
@MaxLength(200)
name?: string;
@IsOptional()
@IsDateString()
startDate?: string;
@IsOptional()
@IsDateString()
endDate?: string;
@IsOptional()
@IsBoolean()
isRecurring?: boolean;
@IsOptional()
@IsString()
notes?: string;
}
\ No newline at end of file
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
Query,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { HolidaysService } from './holidays.service';
import { CreateHolidayDto } from './dto/create-holiday.dto';
import { UpdateHolidayDto } from './dto/update-holiday.dto';
import { HolidayFilterDto } from './dto/holiday-filter.dto';
import { CurrentUser, RequestUser } from '../../common/decorators/current-user.decorator';
import { Roles } from '../../common/decorators/roles.decorator';
@Controller('holidays')
export class HolidaysController {
constructor(private readonly holidaysService: HolidaysService) {}
@Post()
@Roles('SUPER_ADMIN', 'ADMIN')
async create(@Body() dto: CreateHolidayDto, @CurrentUser() user: RequestUser) {
return this.holidaysService.create(dto, user);
}
@Get()
async findAll(@Query() filter: HolidayFilterDto, @CurrentUser() user: RequestUser) {
return this.holidaysService.findAll(filter, user);
}
@Get('upcoming')
async getUpcoming(@Query('days') days?: string) {
return this.holidaysService.getUpcoming(days ? parseInt(days, 10) : 90);
}
@Get(':id')
async findById(@Param('id') id: string) {
return this.holidaysService.findById(id);
}
@Put(':id')
@Roles('SUPER_ADMIN', 'ADMIN')
async update(
@Param('id') id: string,
@Body() dto: UpdateHolidayDto,
@CurrentUser() user: RequestUser,
) {
return this.holidaysService.update(id, dto, user);
}
@Delete(':id')
@Roles('SUPER_ADMIN', 'ADMIN')
@HttpCode(HttpStatus.OK)
async delete(@Param('id') id: string, @CurrentUser() user: RequestUser) {
await this.holidaysService.delete(id, user);
return { message: 'Holiday deleted' };
}
}
\ No newline at end of file
import { Module } from '@nestjs/common';
import { HolidaysController } from './holidays.controller';
import { HolidaysService } from './holidays.service';
@Module({
controllers: [HolidaysController],
providers: [HolidaysService],
exports: [HolidaysService],
})
export class HolidaysModule {}
\ No newline at end of file
import {
Injectable,
NotFoundException,
ForbiddenException,
BadRequestException,
Logger,
} from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { CreateHolidayDto } from './dto/create-holiday.dto';
import { UpdateHolidayDto } from './dto/update-holiday.dto';
import { HolidayFilterDto } from './dto/holiday-filter.dto';
import { RequestUser } from '../../common/decorators/current-user.decorator';
import { getSkip, buildPaginatedResponse, PaginatedResult } from '../../common/utils/pagination.util';
@Injectable()
export class HolidaysService {
private readonly logger = new Logger(HolidaysService.name);
constructor(private readonly prisma: PrismaService) {}
async create(dto: CreateHolidayDto, currentUser: RequestUser): Promise<any> {
if (currentUser.role !== 'SUPER_ADMIN' && currentUser.role !== 'ADMIN') {
throw new ForbiddenException('Only Super Admin and Admin can manage holidays');
}
const startDate = new Date(dto.startDate);
const endDate = new Date(dto.endDate);
if (endDate < startDate) {
throw new BadRequestException('End date cannot be before start date');
}
// Check for overlapping holidays
const overlap = await this.prisma.holiday.findFirst({
where: {
OR: [
{ startDate: { lte: endDate }, endDate: { gte: startDate } },
],
},
});
if (overlap) {
throw new BadRequestException(
`This holiday overlaps with "${overlap.name}" (${overlap.startDate.toISOString().split('T')[0]} - ${overlap.endDate.toISOString().split('T')[0]})`,
);
}
const holiday = await this.prisma.holiday.create({
data: {
name: dto.name,
startDate,
endDate,
isRecurring: dto.isRecurring ?? false,
notes: dto.notes || null,
createdById: currentUser.id,
},
include: {
createdBy: { select: { id: true, firstName: true, lastName: true } },
},
});
this.logger.log(`Holiday "${dto.name}" created by ${currentUser.email}`);
return holiday;
}
async findAll(filter: HolidayFilterDto, currentUser: RequestUser): Promise<PaginatedResult<any>> {
const page = filter.page || 1;
const limit = filter.limit || 50;
const where: any = {};
if (filter.dateFrom || filter.dateTo) {
if (filter.dateFrom) {
where.endDate = { ...(where.endDate || {}), gte: new Date(filter.dateFrom) };
}
if (filter.dateTo) {
where.startDate = { ...(where.startDate || {}), lte: new Date(filter.dateTo) };
}
}
if (filter.year) {
const year = parseInt(filter.year, 10);
const yearStart = new Date(year, 0, 1);
const yearEnd = new Date(year, 11, 31, 23, 59, 59, 999);
where.startDate = { ...(where.startDate || {}), lte: yearEnd };
where.endDate = { ...(where.endDate || {}), gte: yearStart };
}
if (filter.isRecurring !== undefined) {
where.isRecurring = filter.isRecurring;
}
const [data, total] = await Promise.all([
this.prisma.holiday.findMany({
where,
skip: getSkip(page, limit),
take: limit,
orderBy: { startDate: 'asc' },
include: {
createdBy: { select: { id: true, firstName: true, lastName: true } },
},
}),
this.prisma.holiday.count({ where }),
]);
return buildPaginatedResponse(data, total, { page, limit, sortOrder: 'asc' });
}
async findById(id: string): Promise<any> {
const holiday = await this.prisma.holiday.findUnique({
where: { id },
include: {
createdBy: { select: { id: true, firstName: true, lastName: true } },
},
});
if (!holiday) throw new NotFoundException('Holiday not found');
return holiday;
}
async update(id: string, dto: UpdateHolidayDto, currentUser: RequestUser): Promise<any> {
if (currentUser.role !== 'SUPER_ADMIN' && currentUser.role !== 'ADMIN') {
throw new ForbiddenException('Only Super Admin and Admin can manage holidays');
}
const holiday = await this.prisma.holiday.findUnique({ where: { id } });
if (!holiday) throw new NotFoundException('Holiday not found');
const startDate = dto.startDate ? new Date(dto.startDate) : holiday.startDate;
const endDate = dto.endDate ? new Date(dto.endDate) : holiday.endDate;
if (endDate < startDate) {
throw new BadRequestException('End date cannot be before start date');
}
// Check overlaps excluding self
const overlap = await this.prisma.holiday.findFirst({
where: {
id: { not: id },
OR: [
{ startDate: { lte: endDate }, endDate: { gte: startDate } },
],
},
});
if (overlap) {
throw new BadRequestException(
`This holiday would overlap with "${overlap.name}"`,
);
}
const updateData: any = {};
if (dto.name !== undefined) updateData.name = dto.name;
if (dto.startDate !== undefined) updateData.startDate = new Date(dto.startDate);
if (dto.endDate !== undefined) updateData.endDate = new Date(dto.endDate);
if (dto.isRecurring !== undefined) updateData.isRecurring = dto.isRecurring;
if (dto.notes !== undefined) updateData.notes = dto.notes;
const updated = await this.prisma.holiday.update({
where: { id },
data: updateData,
include: {
createdBy: { select: { id: true, firstName: true, lastName: true } },
},
});
this.logger.log(`Holiday "${updated.name}" updated by ${currentUser.email}`);
return updated;
}
async delete(id: string, currentUser: RequestUser): Promise<void> {
if (currentUser.role !== 'SUPER_ADMIN' && currentUser.role !== 'ADMIN') {
throw new ForbiddenException('Only Super Admin and Admin can manage holidays');
}
const holiday = await this.prisma.holiday.findUnique({ where: { id } });
if (!holiday) throw new NotFoundException('Holiday not found');
await this.prisma.holiday.delete({ where: { id } });
this.logger.log(`Holiday "${holiday.name}" deleted by ${currentUser.email}`);
}
async getUpcoming(days: number = 90): Promise<any[]> {
const now = new Date();
const future = new Date(now.getTime() + days * 24 * 60 * 60 * 1000);
return this.prisma.holiday.findMany({
where: {
startDate: { gte: now, lte: future },
},
orderBy: { startDate: 'asc' },
});
}
async isHoliday(date: Date): Promise<boolean> {
const dayStart = new Date(date);
dayStart.setHours(0, 0, 0, 0);
const dayEnd = new Date(date);
dayEnd.setHours(23, 59, 59, 999);
const holiday = await this.prisma.holiday.findFirst({
where: {
startDate: { lte: dayEnd },
endDate: { gte: dayStart },
},
});
return !!holiday;
}
async getHolidaysInRange(startDate: Date, endDate: Date): Promise<Date[]> {
const holidays = await this.prisma.holiday.findMany({
where: {
startDate: { lte: endDate },
endDate: { gte: startDate },
},
});
const holidayDates: Date[] = [];
for (const h of holidays) {
const current = new Date(Math.max(h.startDate.getTime(), startDate.getTime()));
const end = new Date(Math.min(h.endDate.getTime(), endDate.getTime()));
while (current <= end) {
holidayDates.push(new Date(current));
current.setDate(current.getDate() + 1);
}
}
return holidayDates;
}
}
\ No newline at end of file
import { IsString, IsOptional, IsDateString, IsArray, MinLength, MaxLength } from 'class-validator';
export class CreateMeetingDto {
@IsString()
@MinLength(3)
@MaxLength(200)
title: string;
@IsOptional()
@IsString()
description?: string;
@IsDateString()
meetingDate: string;
@IsDateString()
startTime: string;
@IsDateString()
endTime: string;
@IsOptional()
@IsString()
location?: string;
@IsOptional()
@IsString()
recurrence?: string; // NONE, WEEKLY, BIWEEKLY, MONTHLY
@IsOptional()
@IsString()
relatedEntityType?: string; // PIP, EVALUATION, CARD
@IsOptional()
@IsString()
relatedEntityId?: string;
@IsArray()
@IsString({ each: true })
inviteeIds: string[];
}
\ No newline at end of file
import { IsOptional, IsString, IsDateString } from 'class-validator';
import { PaginationDto } from '../../../common/dto/pagination.dto';
export class MeetingFilterDto extends PaginationDto {
@IsOptional()
@IsString()
status?: string;
@IsOptional()
@IsDateString()
dateFrom?: string;
@IsOptional()
@IsDateString()
dateTo?: string;
@IsOptional()
@IsString()
relatedEntityType?: string;
@IsOptional()
@IsString()
relatedEntityId?: string;
}
\ No newline at end of file
import { IsString, IsOptional, IsArray, MinLength } from 'class-validator';
export class CreateMeetingNotesDto {
@IsOptional()
@IsArray()
@IsString({ each: true })
attendeeIds?: string[];
@IsString()
@MinLength(20)
summary: string;
@IsOptional()
actionItems?: any; // Array of { description: string, assigneeId?: string }
}
\ No newline at end of file
import { IsString, IsOptional, IsDateString, IsArray, MaxLength } from 'class-validator';
export class UpdateMeetingDto {
@IsOptional()
@IsString()
@MaxLength(200)
title?: string;
@IsOptional()
@IsString()
description?: string;
@IsOptional()
@IsDateString()
meetingDate?: string;
@IsOptional()
@IsDateString()
startTime?: string;
@IsOptional()
@IsDateString()
endTime?: string;
@IsOptional()
@IsString()
location?: string;
@IsOptional()
@IsArray()
@IsString({ each: true })
inviteeIds?: string[];
}
\ No newline at end of file
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
Query,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { MeetingsService } from './meetings.service';
import { CreateMeetingDto } from './dto/create-meeting.dto';
import { UpdateMeetingDto } from './dto/update-meeting.dto';
import { CreateMeetingNotesDto } from './dto/meeting-notes.dto';
import { MeetingFilterDto } from './dto/meeting-filter.dto';
import { CurrentUser, RequestUser } from '../../common/decorators/current-user.decorator';
import { Roles } from '../../common/decorators/roles.decorator';
@Controller('meetings')
export class MeetingsController {
constructor(private readonly meetingsService: MeetingsService) {}
@Post()
@Roles('SUPER_ADMIN', 'ADMIN', 'TEAM_LEAD')
async create(@Body() dto: CreateMeetingDto, @CurrentUser() user: RequestUser) {
return this.meetingsService.create(dto, user);
}
@Get()
async findAll(@Query() filter: MeetingFilterDto, @CurrentUser() user: RequestUser) {
return this.meetingsService.findAll(filter, user);
}
@Get('upcoming')
async getUpcoming(
@CurrentUser() user: RequestUser,
@Query('days') days?: string,
) {
return this.meetingsService.getUpcoming(user, days ? parseInt(days, 10) : 7);
}
@Get(':id')
async findById(@Param('id') id: string, @CurrentUser() user: RequestUser) {
return this.meetingsService.findById(id, user);
}
@Put(':id')
@Roles('SUPER_ADMIN', 'ADMIN', 'TEAM_LEAD')
async update(
@Param('id') id: string,
@Body() dto: UpdateMeetingDto,
@CurrentUser() user: RequestUser,
) {
return this.meetingsService.update(id, dto, user);
}
@Post(':id/cancel')
@Roles('SUPER_ADMIN', 'ADMIN', 'TEAM_LEAD')
@HttpCode(HttpStatus.OK)
async cancel(@Param('id') id: string, @CurrentUser() user: RequestUser) {
return this.meetingsService.cancel(id, user);
}
@Post(':id/notes')
@Roles('SUPER_ADMIN', 'ADMIN', 'TEAM_LEAD')
async addNotes(
@Param('id') id: string,
@Body() dto: CreateMeetingNotesDto,
@CurrentUser() user: RequestUser,
) {
return this.meetingsService.addNotes(id, dto, user);
}
@Delete(':id')
@Roles('SUPER_ADMIN')
@HttpCode(HttpStatus.OK)
async delete(@Param('id') id: string, @CurrentUser() user: RequestUser) {
await this.meetingsService.delete(id, user);
return { message: 'Meeting deleted' };
}
}
\ No newline at end of file
import { Module } from '@nestjs/common';
import { MeetingsController } from './meetings.controller';
import { MeetingsService } from './meetings.service';
import { NotificationsModule } from '../notifications/notifications.module';
@Module({
imports: [NotificationsModule],
controllers: [MeetingsController],
providers: [MeetingsService],
exports: [MeetingsService],
})
export class MeetingsModule {}
\ No newline at end of file
This diff is collapsed.
import { IsString, IsOptional, MinLength } from 'class-validator';
export class ReviewScheduleChangeDto {
@IsString()
decision: string; // APPROVED, REJECTED
@IsOptional()
@IsString()
@MinLength(10)
rejectionReason?: string;
}
\ No newline at end of file
import { IsString, IsDateString, IsObject, MinLength } from 'class-validator';
export class CreateScheduleChangeRequestDto {
@IsObject()
proposedSchedule: Record<string, string>;
// { sunday: 'IN_OFFICE', monday: 'REMOTE', tuesday: 'OFF', ... }
@IsDateString()
effectiveDate: string;
@IsString()
@MinLength(50, { message: 'Reason must be at least 50 characters' })
reason: string;
}
\ No newline at end of file
import {
Controller,
Get,
Post,
Put,
Body,
Param,
Query,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { SchedulesService } from './schedules.service';
import { CreateScheduleChangeRequestDto } from './dto/schedule-change-request.dto';
import { ReviewScheduleChangeDto } from './dto/review-schedule-change.dto';
import { CurrentUser, RequestUser } from '../../common/decorators/current-user.decorator';
import { Roles } from '../../common/decorators/roles.decorator';
@Controller('schedules')
export class SchedulesController {
constructor(private readonly schedulesService: SchedulesService) {}
@Post('change-request')
async createRequest(
@Body() dto: CreateScheduleChangeRequestDto,
@CurrentUser() user: RequestUser,
) {
return this.schedulesService.createRequest(dto, user);
}
@Get('change-requests')
async findAll(
@CurrentUser() user: RequestUser,
@Query('page') page?: string,
@Query('limit') limit?: string,
) {
return this.schedulesService.findAll(
user,
page ? parseInt(page, 10) : 1,
limit ? parseInt(limit, 10) : 20,
);
}
@Get('change-requests/:id')
async findById(@Param('id') id: string, @CurrentUser() user: RequestUser) {
return this.schedulesService.findById(id, user);
}
@Put('change-requests/:id/review')
@Roles('SUPER_ADMIN', 'ADMIN')
@HttpCode(HttpStatus.OK)
async review(
@Param('id') id: string,
@Body() dto: ReviewScheduleChangeDto,
@CurrentUser() user: RequestUser,
) {
return this.schedulesService.review(id, dto, user);
}
@Put('direct-edit/:userId')
@Roles('SUPER_ADMIN')
@HttpCode(HttpStatus.OK)
async directEdit(
@Param('userId') userId: string,
@Body('schedule') schedule: Record<string, string>,
@CurrentUser() user: RequestUser,
) {
return this.schedulesService.directEdit(userId, schedule, user);
}
}
\ No newline at end of file
import { Module } from '@nestjs/common';
import { SchedulesController } from './schedules.controller';
import { SchedulesService } from './schedules.service';
import { NotificationsModule } from '../notifications/notifications.module';
import { HudModule } from '../hud/hud.module';
@Module({
imports: [NotificationsModule, HudModule],
controllers: [SchedulesController],
providers: [SchedulesService],
exports: [SchedulesService],
})
export class SchedulesModule {}
\ No newline at end of file
This diff is collapsed.
import { IsString, IsOptional, IsDateString, MinLength } from 'class-validator';
export class CreateUnavailabilityDto {
@IsOptional()
@IsString()
userId?: string; // If not provided, uses current user
@IsDateString()
startDate: string;
@IsDateString()
endDate: string;
@IsString()
reasonCategory: string; // PERSONAL, MEDICAL, RELIGIOUS, EMERGENCY, OTHER
@IsOptional()
@IsString()
notes?: string;
}
\ No newline at end of file
import { IsOptional, IsString, IsDateString } from 'class-validator';
import { PaginationDto } from '../../../common/dto/pagination.dto';
export class UnavailabilityFilterDto extends PaginationDto {
@IsOptional()
@IsString()
userId?: string;
@IsOptional()
@IsString()
reasonCategory?: string;
@IsOptional()
@IsDateString()
dateFrom?: string;
@IsOptional()
@IsDateString()
dateTo?: string;
}
\ No newline at end of file
import { IsString, IsOptional, IsDateString } from 'class-validator';
export class UpdateUnavailabilityDto {
@IsOptional()
@IsDateString()
startDate?: string;
@IsOptional()
@IsDateString()
endDate?: string;
@IsOptional()
@IsString()
reasonCategory?: string;
@IsOptional()
@IsString()
notes?: string;
}
\ No newline at end of file
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
Query,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { UnavailabilityService } from './unavailability.service';
import { CreateUnavailabilityDto } from './dto/create-unavailability.dto';
import { UpdateUnavailabilityDto } from './dto/update-unavailability.dto';
import { UnavailabilityFilterDto } from './dto/unavailability-filter.dto';
import { CurrentUser, RequestUser } from '../../common/decorators/current-user.decorator';
import { Roles } from '../../common/decorators/roles.decorator';
@Controller('unavailability')
export class UnavailabilityController {
constructor(private readonly unavailabilityService: UnavailabilityService) {}
@Post()
async create(@Body() dto: CreateUnavailabilityDto, @CurrentUser() user: RequestUser) {
return this.unavailabilityService.create(dto, user);
}
@Get()
async findAll(@Query() filter: UnavailabilityFilterDto, @CurrentUser() user: RequestUser) {
return this.unavailabilityService.findAll(filter, user);
}
@Get('team-availability')
@Roles('SUPER_ADMIN', 'ADMIN', 'TEAM_LEAD')
async getTeamAvailability(
@Query('startDate') startDate: string,
@Query('endDate') endDate: string,
@Query('boardId') boardId: string,
@CurrentUser() user: RequestUser,
) {
return this.unavailabilityService.getTeamAvailability(startDate, endDate, user, boardId);
}
@Get('stats/:userId')
async getStats(
@Param('userId') userId: string,
@Query('year') year: string,
@CurrentUser() user: RequestUser,
) {
if (user.role === 'CONTRACTOR' && userId !== user.id) {
return { totalDays: 0, byCategory: {}, recordCount: 0 };
}
return this.unavailabilityService.getUserUnavailabilityStats(
userId,
year ? parseInt(year, 10) : new Date().getFullYear(),
);
}
@Get(':id')
async findById(@Param('id') id: string, @CurrentUser() user: RequestUser) {
return this.unavailabilityService.findById(id, user);
}
@Put(':id')
async update(
@Param('id') id: string,
@Body() dto: UpdateUnavailabilityDto,
@CurrentUser() user: RequestUser,
) {
return this.unavailabilityService.update(id, dto, user);
}
@Delete(':id')
@HttpCode(HttpStatus.OK)
async delete(@Param('id') id: string, @CurrentUser() user: RequestUser) {
await this.unavailabilityService.delete(id, user);
return { message: 'Unavailability record deleted' };
}
}
\ No newline at end of file
import { Module } from '@nestjs/common';
import { UnavailabilityController } from './unavailability.controller';
import { UnavailabilityService } from './unavailability.service';
import { NotificationsModule } from '../notifications/notifications.module';
@Module({
imports: [NotificationsModule],
controllers: [UnavailabilityController],
providers: [UnavailabilityService],
exports: [UnavailabilityService],
})
export class UnavailabilityModule {}
\ No newline at end of file
This diff is collapsed.
// ─── TIME & SCHEDULING MODELS ──────────────────────────────────
// Phase 2C: Unavailability, Holidays, Schedule Changes, Meetings
model Holiday {
id String @id @default(uuid())
name String
startDate DateTime
endDate DateTime
isRecurring Boolean @default(false)
notes String?
createdById String?
createdBy User? @relation("HolidayCreator", fields: [createdById], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([startDate])
@@index([endDate])
}
model Unavailability {
id String @id @default(uuid())
userId String
user User @relation("UserUnavailability", fields: [userId], references: [id], onDelete: Cascade)
startDate DateTime
endDate DateTime
reasonCategory String // PERSONAL, MEDICAL, RELIGIOUS, EMERGENCY, OTHER
notes String?
createdById String? // null means self-logged
createdBy User? @relation("UnavailabilityCreator", fields: [createdById], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@index([startDate])
@@index([endDate])
@@index([userId, startDate, endDate])
}
model ScheduleChangeRequest {
id String @id @default(uuid())
userId String
user User @relation("ScheduleChangeRequests", fields: [userId], references: [id], onDelete: Cascade)
currentSchedule Json // snapshot of current weeklySchedule
proposedSchedule Json // proposed new weeklySchedule
currentBaseSalaryPiasters Int
proposedBaseSalaryPiasters Int
effectiveDate DateTime
reason String // min 50 chars
status String @default("PENDING") // PENDING, APPROVED, REJECTED
reviewedById String?
reviewedBy User? @relation("ScheduleChangeReviewer", fields: [reviewedById], references: [id], onDelete: SetNull)
reviewedAt DateTime?
rejectionReason String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@index([status])
@@index([userId, status])
}
model Meeting {
id String @id @default(uuid())
title String
description String?
meetingDate DateTime
startTime DateTime
endTime DateTime
location String? // physical location or video link
recurrence String? // NONE, WEEKLY, BIWEEKLY, MONTHLY
relatedEntityType String? // PIP, EVALUATION, CARD
relatedEntityId String?
status String @default("SCHEDULED") // SCHEDULED, COMPLETED, CANCELLED
reminderSent1h Boolean @default(false)
reminderSent24h Boolean @default(false)
createdById String
createdBy User @relation("MeetingCreator", fields: [createdById], references: [id], onDelete: Cascade)
cancelledAt DateTime?
cancelledById String?
cancelledBy User? @relation("MeetingCanceller", fields: [cancelledById], references: [id], onDelete: SetNull)
invitees MeetingInvitee[]
notes MeetingNotes[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([meetingDate])
@@index([createdById])
@@index([status])
@@index([startTime])
}
model MeetingInvitee {
id String @id @default(uuid())
meetingId String
meeting Meeting @relation(fields: [meetingId], references: [id], onDelete: Cascade)
userId String
user User @relation("MeetingInvitations", fields: [userId], references: [id], onDelete: Cascade)
attended Boolean? // null = not yet recorded, true = attended, false = absent
createdAt DateTime @default(now())
@@unique([meetingId, userId])
@@index([userId])
}
model MeetingNotes {
id String @id @default(uuid())
meetingId String
meeting Meeting @relation(fields: [meetingId], references: [id], onDelete: Cascade)
authorId String
author User @relation("MeetingNotesAuthor", fields: [authorId], references: [id], onDelete: Cascade)
attendees Json? // array of user IDs who actually attended
summary String
actionItems Json? // array of { description, assigneeId? }
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([meetingId])
}
\ No newline at end of file
export enum UnavailabilityReason {
PERSONAL = 'PERSONAL',
MEDICAL = 'MEDICAL',
RELIGIOUS = 'RELIGIOUS',
EMERGENCY = 'EMERGENCY',
OTHER = 'OTHER',
}
export enum ScheduleChangeStatus {
PENDING = 'PENDING',
APPROVED = 'APPROVED',
REJECTED = 'REJECTED',
}
export enum MeetingStatus {
SCHEDULED = 'SCHEDULED',
COMPLETED = 'COMPLETED',
CANCELLED = 'CANCELLED',
}
export enum MeetingRecurrence {
NONE = 'NONE',
WEEKLY = 'WEEKLY',
BIWEEKLY = 'BIWEEKLY',
MONTHLY = 'MONTHLY',
}
export enum DayType {
IN_OFFICE = 'IN_OFFICE',
REMOTE = 'REMOTE',
OFF = 'OFF',
}
\ 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