Merge branch 'master' into feature/tab-warning-flag

This commit is contained in:
2026-06-22 11:20:17 +03:30
92 changed files with 6547 additions and 1535 deletions

View File

@@ -0,0 +1,118 @@
export const MINUTES_PER_DAY = 24 * 60;
export type WorkingHoursBlockInput = {
dayOfWeek: number;
startMinute: number;
endMinute: number;
sortOrder?: number;
};
export type WorkingHoursDayBlock = {
startMinute: number;
endMinute: number;
};
export function localDayOfWeekMondayZero(dayOfWeekJs: number): number {
return dayOfWeekJs === 0 ? 6 : dayOfWeekJs - 1;
}
export function validateWorkingHoursBlocks(blocks: WorkingHoursBlockInput[]): string | null {
if (!Array.isArray(blocks)) {
return 'Working hours blocks must be an array';
}
const byDay = new Map<number, WorkingHoursBlockInput[]>();
for (const block of blocks) {
if (!Number.isInteger(block.dayOfWeek) || block.dayOfWeek < 0 || block.dayOfWeek > 6) {
return 'dayOfWeek must be an integer from 0 (Monday) to 6 (Sunday)';
}
if (!Number.isInteger(block.startMinute) || block.startMinute < 0 || block.startMinute >= MINUTES_PER_DAY) {
return 'startMinute must be between 0 and 1439';
}
if (!Number.isInteger(block.endMinute) || block.endMinute <= 0 || block.endMinute > MINUTES_PER_DAY) {
return 'endMinute must be between 1 and 1440';
}
if (block.endMinute <= block.startMinute) {
return 'Each shift end time must be after its start time';
}
const list = byDay.get(block.dayOfWeek) ?? [];
list.push(block);
byDay.set(block.dayOfWeek, list);
}
for (const [, dayBlocks] of byDay) {
const sorted = [...dayBlocks].sort((a, b) => a.startMinute - b.startMinute);
for (let i = 1; i < sorted.length; i += 1) {
if (sorted[i].startMinute < sorted[i - 1].endMinute) {
return 'Shifts on the same day cannot overlap';
}
}
}
return null;
}
export function blocksForDay(
blocks: WorkingHoursBlockInput[],
dayOfWeek: number,
): WorkingHoursDayBlock[] {
return blocks
.filter((b) => b.dayOfWeek === dayOfWeek)
.sort((a, b) => a.startMinute - b.startMinute || a.endMinute - b.endMinute)
.map((b) => ({ startMinute: b.startMinute, endMinute: b.endMinute }));
}
export function isMinuteWithinWorkingBlocks(minute: number, dayBlocks: WorkingHoursDayBlock[]): boolean {
return dayBlocks.some((b) => minute >= b.startMinute && minute < b.endMinute);
}
export function isRangeWithinWorkingBlocks(
startMinute: number,
endMinute: number,
dayBlocks: WorkingHoursDayBlock[],
): boolean {
if (endMinute <= startMinute) {
return false;
}
for (let m = startMinute; m < endMinute; m += 1) {
if (!isMinuteWithinWorkingBlocks(m, dayBlocks)) {
return false;
}
}
return true;
}
export function dateToLocalMinutes(date: Date): number {
return date.getHours() * 60 + date.getMinutes();
}
export function appointmentWithinWorkingHours(
startAt: Date,
endAt: Date,
dayBlocks: WorkingHoursDayBlock[],
): boolean {
const startMinute = dateToLocalMinutes(startAt);
const endMinute = dateToLocalMinutes(endAt);
return isRangeWithinWorkingBlocks(startMinute, endMinute, dayBlocks);
}
export function unionDayBlockRange(dayBlocksList: WorkingHoursDayBlock[][]): {
startMinute: number;
endMinute: number;
} | null {
let startMinute: number | null = null;
let endMinute: number | null = null;
for (const dayBlocks of dayBlocksList) {
for (const block of dayBlocks) {
startMinute = startMinute == null ? block.startMinute : Math.min(startMinute, block.startMinute);
endMinute = endMinute == null ? block.endMinute : Math.max(endMinute, block.endMinute);
}
}
if (startMinute == null || endMinute == null) {
return null;
}
return { startMinute, endMinute };
}

View File

@@ -13,6 +13,7 @@ import {
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { AppointmentsService } from './appointments.service';
import { ColumnProvidersQueryDto } from './dto/column-providers-query.dto';
import { CreateAppointmentDto } from './dto/create-appointment.dto';
import { ListAppointmentsDto } from './dto/list-appointments.dto';
import { UpdateAppointmentDto } from './dto/update-appointment.dto';
@@ -29,9 +30,16 @@ export class AppointmentsController {
summary:
'Staff columns: active non-owner members with TAB_TREATMENT_EDIT. Owners are excluded. Requires TAB_APPOINTMENTS_READ or owner.',
})
columnProviders(@Req() req: { user: { id: string; organizationId?: string } }) {
columnProviders(
@Query() query: ColumnProvidersQueryDto,
@Req() req: { user: { id: string; organizationId?: string } },
) {
const organizationId = this.appointmentsService.getOrganizationIdFromUser(req.user);
return this.appointmentsService.listColumnProviders(organizationId, req.user.id);
return this.appointmentsService.listColumnProviders(
organizationId,
req.user.id,
query.date,
);
}
@Get()

View File

@@ -1,9 +1,11 @@
import { Module } from '@nestjs/common';
import { PrismaService } from '../../../prisma/prisma.service';
import { StaffModule } from '../staff/staff.module';
import { AppointmentsController } from './appointments.controller';
import { AppointmentsService } from './appointments.service';
@Module({
imports: [StaffModule],
controllers: [AppointmentsController],
providers: [AppointmentsService, PrismaService],
})

View File

@@ -5,6 +5,12 @@ import {
NotFoundException,
} from '@nestjs/common';
import { PrismaService } from '../../../prisma/prisma.service';
import {
appointmentWithinWorkingHours,
blocksForDay,
localDayOfWeekMondayZero,
} from '../../common/working-hours';
import { StaffWorkingHoursService } from '../staff/staff-working-hours.service';
import { CreateAppointmentDto } from './dto/create-appointment.dto';
import { ListAppointmentsDto } from './dto/list-appointments.dto';
import { UpdateAppointmentDto } from './dto/update-appointment.dto';
@@ -13,7 +19,10 @@ const MS_PER_DAY = 86_400_000;
@Injectable()
export class AppointmentsService {
constructor(private readonly prisma: PrismaService) {}
constructor(
private readonly prisma: PrismaService,
private readonly staffWorkingHoursService: StaffWorkingHoursService,
) {}
getOrganizationIdFromUser(user: { organizationId?: string }) {
if (!user?.organizationId) {
@@ -22,7 +31,7 @@ export class AppointmentsService {
return user.organizationId;
}
async listColumnProviders(organizationId: string, actorUserId: string) {
async listColumnProviders(organizationId: string, actorUserId: string, date?: string) {
await this.assertCanViewAppointments(actorUserId, organizationId);
const members = await this.prisma.membership.findMany({
@@ -44,7 +53,23 @@ export class AppointmentsService {
orderBy: [{ createdAt: 'asc' }],
});
const data = members.map((m) => ({ userId: m.user.id, name: m.user.name }));
const scheduleBlocksByMembership =
await this.staffWorkingHoursService.loadScheduleBlocksByMembershipIds(
members.map((m) => m.id),
);
const dayOfWeek = this.resolveDayOfWeekMondayZero(date);
const data = members.map((m) => {
const allBlocks = scheduleBlocksByMembership.get(m.id) ?? [];
const dayBlocks = blocksForDay(allBlocks, dayOfWeek);
return {
userId: m.user.id,
name: m.user.name,
hasWorkingHours: allBlocks.length > 0,
dayBlocks,
};
});
return { success: true, data };
}
@@ -109,6 +134,12 @@ export class AppointmentsService {
await this.ensurePatientInOrg(dto.patientId, organizationId);
await this.ensureProviderIsTreatmentEditor(dto.providerUserId, organizationId);
await this.ensureAppointmentWithinProviderWorkingHours(
dto.providerUserId,
organizationId,
startAt,
endAt,
);
const appointment = await this.prisma.appointment.create({
data: {
@@ -166,6 +197,12 @@ export class AppointmentsService {
await this.ensurePatientInOrg(patientId, organizationId);
await this.ensureProviderIsTreatmentEditor(providerUserId, organizationId);
await this.ensureAppointmentWithinProviderWorkingHours(
providerUserId,
organizationId,
startAt,
endAt,
);
const appointment = await this.prisma.appointment.update({
where: { id },
@@ -273,6 +310,47 @@ export class AppointmentsService {
}
}
private resolveDayOfWeekMondayZero(date?: string): number {
if (!date) {
return localDayOfWeekMondayZero(new Date().getDay());
}
const [y, m, d] = date.split('-').map(Number);
const parsed = new Date(y, m - 1, d, 12, 0, 0, 0);
if (Number.isNaN(parsed.getTime())) {
throw new BadRequestException('Invalid date query parameter');
}
return localDayOfWeekMondayZero(parsed.getDay());
}
private async ensureAppointmentWithinProviderWorkingHours(
providerUserId: string,
organizationId: string,
startAt: Date,
endAt: Date,
) {
const membership = await this.getMembership(providerUserId, organizationId);
if (!membership) {
throw new BadRequestException('Provider is not a member of this organization');
}
const scheduleBlocksByMembership =
await this.staffWorkingHoursService.loadScheduleBlocksByMembershipIds([membership.id]);
const allBlocks = scheduleBlocksByMembership.get(membership.id) ?? [];
if (allBlocks.length === 0) {
throw new BadRequestException('Provider has no working hours configured');
}
const dayOfWeek = localDayOfWeekMondayZero(startAt.getDay());
const dayBlocks = blocksForDay(allBlocks, dayOfWeek);
if (dayBlocks.length === 0) {
throw new BadRequestException('Provider is not working on this day');
}
if (!appointmentWithinWorkingHours(startAt, endAt, dayBlocks)) {
throw new BadRequestException('Appointment must fall within the provider working hours');
}
}
private async getMembership(userId: string, organizationId: string) {
return this.prisma.membership.findFirst({
where: { userId, organizationId },

View File

@@ -0,0 +1,9 @@
import { IsOptional, IsString, Matches } from 'class-validator';
export class ColumnProvidersQueryDto {
/** Local calendar date (YYYY-MM-DD) used to resolve weekday working hours. */
@IsOptional()
@IsString()
@Matches(/^\d{4}-\d{2}-\d{2}$/)
date?: string;
}

View File

@@ -9,7 +9,8 @@ import {
Res,
HttpCode,
HttpStatus,
Get
Get,
Patch,
} from '@nestjs/common';
import type { Response } from 'express';
import {
@@ -28,6 +29,7 @@ import { RegisterDto } from './dto/register.dto';
import { CreateOrganizationDto } from './dto/create-organization.dto';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { LocalAuthGuard } from './guards/local-auth.guard';
import { UpdateLanguageDto } from './dto/update-language.dto';
@ApiTags('auth')
@Controller('auth')
@@ -149,6 +151,14 @@ export class AuthController {
return this.authService.getProfile(req.user.id);
}
@Patch('profile/language')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth('JWT-auth')
@ApiOperation({ summary: 'Update user language preference' })
async updateLanguage(@Req() req, @Body() dto: UpdateLanguageDto) {
return this.authService.updateLanguage(req.user.id, dto);
}
@Get('subscription-alert')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth('JWT-auth')

View File

@@ -15,6 +15,10 @@ import { PrismaService } from '../../../prisma/prisma.service';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
import { CreateOrganizationDto } from './dto/create-organization.dto';
import {
SUPPORTED_USER_LANGUAGES,
UpdateLanguageDto,
} from './dto/update-language.dto';
import { JwtPayload } from './interfaces/jwt-payload.interface';
const ALL_PERMISSIONS = [
@@ -179,11 +183,7 @@ export class AuthService {
data: {
accessToken,
refreshToken,
user: {
id: user.id,
email: user.email,
name: user.name,
},
user: this.toPublicUser(user),
organizations,
},
};
@@ -343,11 +343,6 @@ export class AuthService {
};
}
/**
* Get user profile with all memberships and permissions
* @param userId - User ID from JWT token
* @returns User profile with organizations and permissions
*/
async getProfile(userId: string) {
try {
const user = await this.prisma.user.findUnique({
@@ -516,11 +511,7 @@ export class AuthService {
success: true,
data: {
accessToken: newAccessToken,
user: {
id: session.user.id,
email: session.user.email,
name: session.user.name,
},
user: this.toPublicUser(session.user),
organizations,
},
};
@@ -931,4 +922,44 @@ export class AuthService {
},
};
}
async updateLanguage(userId: string, dto: UpdateLanguageDto) {
const language = dto.language;
if (!SUPPORTED_USER_LANGUAGES.includes(language)) {
throw new BadRequestException('Language must be one of: en, fa, nl');
}
const user = await this.prisma.user.update({
where: { id: userId },
data: { language },
select: {
id: true,
email: true,
name: true,
language: true,
},
});
return {
success: true,
data: {
user: this.toPublicUser(user),
},
};
}
private toPublicUser(user: {
id: string;
email: string;
name: string;
language?: string | null;
}) {
return {
id: user.id,
email: user.email,
name: user.name,
language: user.language ?? 'en',
};
}
}

View File

@@ -0,0 +1,14 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsIn, IsString } from 'class-validator';
export const SUPPORTED_USER_LANGUAGES = ['en', 'fa', 'nl'] as const;
export type SupportedUserLanguage = (typeof SUPPORTED_USER_LANGUAGES)[number];
export class UpdateLanguageDto {
@ApiProperty({ enum: SUPPORTED_USER_LANGUAGES, example: 'en' })
@IsString()
@IsIn(SUPPORTED_USER_LANGUAGES, {
message: 'Language must be one of: en, fa, nl',
})
language: SupportedUserLanguage;
}

View File

@@ -0,0 +1,44 @@
import { Type } from 'class-transformer';
import {
ArrayMaxSize,
IsArray,
IsBoolean,
IsInt,
IsOptional,
Max,
Min,
ValidateNested,
} from 'class-validator';
export class WorkingHoursBlockDto {
@IsInt()
@Min(0)
@Max(6)
dayOfWeek: number;
@IsInt()
@Min(0)
@Max(1439)
startMinute: number;
@IsInt()
@Min(1)
@Max(1440)
endMinute: number;
@IsOptional()
@IsInt()
@Min(0)
sortOrder?: number;
}
export class UpsertWorkingHoursDto {
@IsBoolean()
autoRepeatWeekly: boolean;
@IsArray()
@ArrayMaxSize(42)
@ValidateNested({ each: true })
@Type(() => WorkingHoursBlockDto)
blocks: WorkingHoursBlockDto[];
}

View File

@@ -0,0 +1,264 @@
import {
BadRequestException,
ForbiddenException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { PrismaService } from '../../../prisma/prisma.service';
import {
appointmentWithinWorkingHours,
blocksForDay,
localDayOfWeekMondayZero,
validateWorkingHoursBlocks,
type WorkingHoursBlockInput,
} from '../../common/working-hours';
import { UpsertWorkingHoursDto } from './dto/upsert-working-hours.dto';
@Injectable()
export class StaffWorkingHoursService {
constructor(private readonly prisma: PrismaService) {}
async getWorkingHours(actorUserId: string, organizationId: string, membershipId: string) {
await this.assertCanViewStaff(actorUserId, organizationId);
const membership = await this.findMembership(membershipId, organizationId);
const schedule = await this.prisma.staffWorkingHoursSchedule.findUnique({
where: { membershipId: membership.id },
include: {
blocks: { orderBy: [{ dayOfWeek: 'asc' }, { sortOrder: 'asc' }, { startMinute: 'asc' }] },
},
});
if (!schedule) {
return {
success: true,
data: {
autoRepeatWeekly: true,
blocks: [],
hasWorkingHours: false,
},
};
}
return {
success: true,
data: {
autoRepeatWeekly: schedule.autoRepeatWeekly,
blocks: schedule.blocks.map((b) => ({
dayOfWeek: b.dayOfWeek,
startMinute: b.startMinute,
endMinute: b.endMinute,
sortOrder: b.sortOrder,
})),
hasWorkingHours: schedule.blocks.length > 0,
},
};
}
async upsertWorkingHours(
actorUserId: string,
organizationId: string,
membershipId: string,
dto: UpsertWorkingHoursDto,
) {
await this.assertCanEditStaff(actorUserId, organizationId);
const membership = await this.findMembership(membershipId, organizationId);
const validationError = validateWorkingHoursBlocks(dto.blocks);
if (validationError) {
throw new BadRequestException(validationError);
}
const normalizedBlocks = this.normalizeBlocks(dto.blocks);
await this.assertNoConflictingAppointments(
organizationId,
membership.userId,
normalizedBlocks,
);
await this.prisma.$transaction(async (tx) => {
const schedule = await tx.staffWorkingHoursSchedule.upsert({
where: { membershipId: membership.id },
create: {
membershipId: membership.id,
autoRepeatWeekly: dto.autoRepeatWeekly,
},
update: {
autoRepeatWeekly: dto.autoRepeatWeekly,
},
});
await tx.staffWorkingHoursBlock.deleteMany({ where: { scheduleId: schedule.id } });
if (normalizedBlocks.length > 0) {
await tx.staffWorkingHoursBlock.createMany({
data: normalizedBlocks.map((block, index) => ({
scheduleId: schedule.id,
dayOfWeek: block.dayOfWeek,
startMinute: block.startMinute,
endMinute: block.endMinute,
sortOrder: block.sortOrder ?? index,
})),
});
}
});
return {
success: true,
message: 'Working hours saved',
};
}
async loadScheduleBlocksByMembershipIds(membershipIds: string[]) {
if (membershipIds.length === 0) {
return new Map<string, WorkingHoursBlockInput[]>();
}
const schedules = await this.prisma.staffWorkingHoursSchedule.findMany({
where: { membershipId: { in: membershipIds } },
include: {
blocks: { orderBy: [{ dayOfWeek: 'asc' }, { sortOrder: 'asc' }, { startMinute: 'asc' }] },
},
});
const map = new Map<string, WorkingHoursBlockInput[]>();
for (const schedule of schedules) {
map.set(
schedule.membershipId,
schedule.blocks.map((b) => ({
dayOfWeek: b.dayOfWeek,
startMinute: b.startMinute,
endMinute: b.endMinute,
sortOrder: b.sortOrder,
})),
);
}
return map;
}
dayBlocksFromMembershipBlocks(blocks: WorkingHoursBlockInput[], dayOfWeekMondayZero: number) {
return blocksForDay(blocks, dayOfWeekMondayZero);
}
private normalizeBlocks(blocks: UpsertWorkingHoursDto['blocks']): WorkingHoursBlockInput[] {
return blocks.map((block, index) => ({
dayOfWeek: block.dayOfWeek,
startMinute: block.startMinute,
endMinute: block.endMinute,
sortOrder: block.sortOrder ?? index,
}));
}
private async assertNoConflictingAppointments(
organizationId: string,
providerUserId: string,
blocks: WorkingHoursBlockInput[],
) {
const now = new Date();
const appointments = await this.prisma.appointment.findMany({
where: {
organizationId,
providerUserId,
endAt: { gt: now },
},
include: {
patient: { select: { firstName: true, lastName: true } },
},
orderBy: { startAt: 'asc' },
});
const conflicts = appointments.filter((appointment) => {
const startAt = new Date(appointment.startAt);
const endAt = new Date(appointment.endAt);
const dayOfWeek = localDayOfWeekMondayZero(startAt.getDay());
const dayBlocks = blocksForDay(blocks, dayOfWeek);
if (dayBlocks.length === 0) {
return true;
}
return !appointmentWithinWorkingHours(startAt, endAt, dayBlocks);
});
if (conflicts.length === 0) {
return;
}
const examples = conflicts.slice(0, 3).map((appointment) => {
const startAt = new Date(appointment.startAt);
const patientName = `${appointment.patient.firstName} ${appointment.patient.lastName}`;
const when = startAt.toLocaleString(undefined, {
weekday: 'short',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
});
return `${patientName} (${when})`;
});
const extra =
conflicts.length > examples.length
? ` and ${conflicts.length - examples.length} more`
: '';
throw new BadRequestException(
`Cannot save working hours: ${conflicts.length} upcoming appointment${conflicts.length === 1 ? '' : 's'} fall outside the new schedule (${examples.join(', ')}${extra}). Reschedule or remove those appointments first.`,
);
}
private async findMembership(membershipId: string, organizationId: string) {
const membership = await this.prisma.membership.findFirst({
where: { id: membershipId, organizationId },
select: { id: true, isOwner: true, userId: true },
});
if (!membership) {
throw new NotFoundException('Member not found');
}
if (membership.isOwner) {
throw new BadRequestException('Working hours cannot be set for the organization owner');
}
return membership;
}
private async assertCanViewStaff(userId: string, organizationId: string) {
const actor = await this.getActorMembership(userId, organizationId);
if (!actor || !this.canViewStaff(actor)) {
throw new ForbiddenException('You do not have access to staff management');
}
}
private async assertCanEditStaff(userId: string, organizationId: string) {
const actor = await this.getActorMembership(userId, organizationId);
if (!actor || !this.canEditStaff(actor)) {
throw new ForbiddenException('You cannot manage staff working hours');
}
}
private async getActorMembership(userId: string, organizationId: string) {
return this.prisma.membership.findFirst({
where: { userId, organizationId },
include: {
permissions: { include: { permission: true } },
organization: { select: { planId: true } },
},
});
}
private canViewStaff(m: {
isOwner: boolean;
permissions: { permission: { name: string } }[];
}): boolean {
if (m.isOwner) return true;
return m.permissions.some(
(p) => p.permission.name === 'TAB_STAFF_READ' || p.permission.name === 'TAB_STAFF_EDIT',
);
}
private canEditStaff(m: {
isOwner: boolean;
organization?: { planId: string | null };
permissions: { permission: { name: string } }[];
}): boolean {
if (m.isOwner) return Boolean(m.organization?.planId);
return m.permissions.some((p) => p.permission.name === 'TAB_STAFF_EDIT');
}
}

View File

@@ -6,6 +6,7 @@ import {
Param,
Patch,
Post,
Put,
Query,
Req,
UseGuards,
@@ -16,13 +17,18 @@ import { AcceptStaffInviteDto } from './dto/accept-staff-invite.dto';
import { InviteStaffDto } from './dto/invite-staff.dto';
import { PreviewStaffInviteDto } from './dto/preview-staff-invite.dto';
import { UpdateStaffMemberDto } from './dto/update-staff-member.dto';
import { UpsertWorkingHoursDto } from './dto/upsert-working-hours.dto';
import { StaffService } from './staff.service';
import { StaffWorkingHoursService } from './staff-working-hours.service';
@ApiTags('staff')
@ApiBearerAuth('JWT-auth')
@Controller('staff')
export class StaffController {
constructor(private readonly staffService: StaffService) {}
constructor(
private readonly staffService: StaffService,
private readonly staffWorkingHoursService: StaffWorkingHoursService,
) {}
@Get('invitations/preview')
@ApiOperation({ summary: 'Preview invite info by token (public)' })
@@ -80,6 +86,38 @@ export class StaffController {
return this.staffService.updateMember(req.user.id, organizationId, membershipId, dto);
}
@Get('members/:membershipId/working-hours')
@UseGuards(JwtAuthGuard)
@ApiOperation({ summary: 'Get weekly working hours for a staff member' })
getWorkingHours(
@Req() req: { user: { id: string; organizationId?: string } },
@Param('membershipId') membershipId: string,
) {
const organizationId = this.staffService.getOrganizationIdFromUser(req.user);
return this.staffWorkingHoursService.getWorkingHours(
req.user.id,
organizationId,
membershipId,
);
}
@Put('members/:membershipId/working-hours')
@UseGuards(JwtAuthGuard)
@ApiOperation({ summary: 'Save weekly working hours for a staff member' })
upsertWorkingHours(
@Req() req: { user: { id: string; organizationId?: string } },
@Param('membershipId') membershipId: string,
@Body() dto: UpsertWorkingHoursDto,
) {
const organizationId = this.staffService.getOrganizationIdFromUser(req.user);
return this.staffWorkingHoursService.upsertWorkingHours(
req.user.id,
organizationId,
membershipId,
dto,
);
}
@Patch('members/:membershipId/enable')
@UseGuards(JwtAuthGuard)
@ApiOperation({

View File

@@ -2,9 +2,11 @@ import { Module } from '@nestjs/common';
import { PrismaService } from '../../../prisma/prisma.service';
import { StaffController } from './staff.controller';
import { StaffService } from './staff.service';
import { StaffWorkingHoursService } from './staff-working-hours.service';
@Module({
controllers: [StaffController],
providers: [StaffService, PrismaService],
providers: [StaffService, StaffWorkingHoursService, PrismaService],
exports: [StaffWorkingHoursService],
})
export class StaffModule {}