Skip to content

Commit 0d7d0a5

Browse files
authored
Merge pull request #451 from abdullahilateefat03-boop/Session-Scheduling
Session Scheduling system
2 parents 9322884 + eb6a99a commit 0d7d0a5

10 files changed

Lines changed: 1537 additions & 0 deletions

File tree

‎src/app.module.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { HealthModule } from './modules/health/health.module';
99
import { RedisModule } from './redis/redis.module';
1010
import { AuthModule } from './modules/auth/auth.module';
1111
import { UserModule } from './modules/user/user.module';
12+
import { SessionsModule } from './modules/sessions/sessions.module';
1213
import { ShutdownModule } from './common/services/shutdown.module';
1314

1415
@Module({
@@ -25,6 +26,7 @@ import { ShutdownModule } from './common/services/shutdown.module';
2526
RedisModule.forRoot(),
2627
AuthModule,
2728
UserModule,
29+
SessionsModule,
2830
HealthModule,
2931
ShutdownModule,
3032
],
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { MigrationInterface, QueryRunner, Table, TableIndex, TableForeignKey } from 'typeorm';
2+
3+
export class CreateSessionsTable1745800000000 implements MigrationInterface {
4+
name = 'CreateSessionsTable1745800000000';
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
// Create sessions table
8+
await queryRunner.createTable(
9+
new Table({
10+
name: 'sessions',
11+
columns: [
12+
{
13+
name: 'id',
14+
type: 'uuid',
15+
isPrimary: true,
16+
isGenerated: true,
17+
generationStrategy: 'uuid',
18+
},
19+
{
20+
name: 'mentorId',
21+
type: 'uuid',
22+
},
23+
{
24+
name: 'menteeId',
25+
type: 'uuid',
26+
},
27+
{
28+
name: 'startTime',
29+
type: 'timestamp',
30+
},
31+
{
32+
name: 'endTime',
33+
type: 'timestamp',
34+
},
35+
{
36+
name: 'status',
37+
type: 'enum',
38+
enum: ['pending', 'confirmed', 'completed', 'cancelled', 'no_show'],
39+
default: "'pending'",
40+
},
41+
{
42+
name: 'meetingUrl',
43+
type: 'varchar',
44+
isNullable: true,
45+
},
46+
{
47+
name: 'notes',
48+
type: 'text',
49+
isNullable: true,
50+
},
51+
{
52+
name: 'rating',
53+
type: 'int',
54+
isNullable: true,
55+
},
56+
{
57+
name: 'review',
58+
type: 'text',
59+
isNullable: true,
60+
},
61+
{
62+
name: 'cancelledAt',
63+
type: 'timestamp',
64+
isNullable: true,
65+
},
66+
{
67+
name: 'cancellationReason',
68+
type: 'varchar',
69+
isNullable: true,
70+
},
71+
{
72+
name: 'createdAt',
73+
type: 'timestamp',
74+
default: 'CURRENT_TIMESTAMP',
75+
},
76+
{
77+
name: 'updatedAt',
78+
type: 'timestamp',
79+
default: 'CURRENT_TIMESTAMP',
80+
onUpdate: 'CURRENT_TIMESTAMP',
81+
},
82+
],
83+
}),
84+
true,
85+
);
86+
87+
// Create indexes
88+
await queryRunner.createIndex(
89+
'sessions',
90+
new TableIndex({
91+
name: 'IDX_SESSIONS_MENTOR_ID',
92+
columnNames: ['mentorId'],
93+
}),
94+
);
95+
96+
await queryRunner.createIndex(
97+
'sessions',
98+
new TableIndex({
99+
name: 'IDX_SESSIONS_MENTEE_ID',
100+
columnNames: ['menteeId'],
101+
}),
102+
);
103+
104+
await queryRunner.createIndex(
105+
'sessions',
106+
new TableIndex({
107+
name: 'IDX_SESSIONS_STATUS',
108+
columnNames: ['status'],
109+
}),
110+
);
111+
112+
await queryRunner.createIndex(
113+
'sessions',
114+
new TableIndex({
115+
name: 'IDX_SESSIONS_TIME_RANGE',
116+
columnNames: ['startTime', 'endTime'],
117+
}),
118+
);
119+
120+
await queryRunner.createIndex(
121+
'sessions',
122+
new TableIndex({
123+
name: 'IDX_SESSIONS_MENTOR_TIME',
124+
columnNames: ['mentorId', 'startTime', 'endTime'],
125+
}),
126+
);
127+
128+
// Create foreign keys
129+
await queryRunner.createForeignKey(
130+
'sessions',
131+
new TableForeignKey({
132+
name: 'FK_SESSIONS_MENTOR',
133+
columnNames: ['mentorId'],
134+
referencedTableName: 'users',
135+
referencedColumnNames: ['id'],
136+
onDelete: 'CASCADE',
137+
}),
138+
);
139+
140+
await queryRunner.createForeignKey(
141+
'sessions',
142+
new TableForeignKey({
143+
name: 'FK_SESSIONS_MENTEE',
144+
columnNames: ['menteeId'],
145+
referencedTableName: 'users',
146+
referencedColumnNames: ['id'],
147+
onDelete: 'CASCADE',
148+
}),
149+
);
150+
}
151+
152+
public async down(queryRunner: QueryRunner): Promise<void> {
153+
// Drop foreign keys
154+
const table = await queryRunner.getTable('sessions');
155+
const mentorForeignKey = table?.foreignKeys.find((fk) => fk.name === 'FK_SESSIONS_MENTOR');
156+
const menteeForeignKey = table?.foreignKeys.find((fk) => fk.name === 'FK_SESSIONS_MENTEE');
157+
158+
if (mentorForeignKey) {
159+
await queryRunner.dropForeignKey('sessions', mentorForeignKey);
160+
}
161+
if (menteeForeignKey) {
162+
await queryRunner.dropForeignKey('sessions', menteeForeignKey);
163+
}
164+
165+
// Drop indexes
166+
await queryRunner.dropIndex('sessions', 'IDX_SESSIONS_MENTOR_TIME');
167+
await queryRunner.dropIndex('sessions', 'IDX_SESSIONS_TIME_RANGE');
168+
await queryRunner.dropIndex('sessions', 'IDX_SESSIONS_STATUS');
169+
await queryRunner.dropIndex('sessions', 'IDX_SESSIONS_MENTEE_ID');
170+
await queryRunner.dropIndex('sessions', 'IDX_SESSIONS_MENTOR_ID');
171+
172+
// Drop table
173+
await queryRunner.dropTable('sessions');
174+
}
175+
}

‎src/modules/auth/entities/user.entity.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import {
99
JoinTable,
1010
Index,
1111
OneToOne,
12+
OneToMany,
1213
} from 'typeorm';
1314
import { Role } from './role.entity';
1415
import { MentorProfile } from '../../user/entities/mentor-profile.entity';
1516
import { MenteeProfile } from '../../user/entities/mentee-profile.entity';
17+
import { Session } from '../../sessions/entities/session.entity';
1618

1719
export enum UserRole {
1820
USER = 'user',
@@ -86,6 +88,12 @@ export class User {
8688
@OneToOne(() => MenteeProfile, (menteeProfile) => menteeProfile.user, { nullable: true, cascade: true })
8789
menteeProfile: MenteeProfile;
8890

91+
@OneToMany(() => Session, (session) => session.mentor)
92+
mentorSessions: Session[];
93+
94+
@OneToMany(() => Session, (session) => session.mentee)
95+
menteeSessions: Session[];
96+
8997
@CreateDateColumn()
9098
createdAt: Date;
9199

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { IsString, IsDateString, IsOptional, IsEnum, Min, Max, IsNumber } from 'class-validator';
2+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
3+
4+
export class CreateSessionDto {
5+
@ApiProperty({ description: 'Mentor ID to book session with' })
6+
@IsString()
7+
mentorId: string;
8+
9+
@ApiProperty({ description: 'Session start time in ISO format' })
10+
@IsDateString()
11+
startTime: string;
12+
13+
@ApiProperty({ description: 'Session end time in ISO format' })
14+
@IsDateString()
15+
endTime: string;
16+
17+
@ApiPropertyOptional({ description: 'Session notes or agenda' })
18+
@IsString()
19+
@IsOptional()
20+
notes?: string;
21+
}
22+
23+
export class UpdateSessionStatusDto {
24+
@ApiProperty({
25+
enum: ['pending', 'confirmed', 'completed', 'cancelled', 'no_show'],
26+
description: 'New session status'
27+
})
28+
@IsEnum(['pending', 'confirmed', 'completed', 'cancelled', 'no_show'])
29+
status: string;
30+
}
31+
32+
export class CancelSessionDto {
33+
@ApiPropertyOptional({ description: 'Reason for cancellation' })
34+
@IsString()
35+
@IsOptional()
36+
reason?: string;
37+
}
38+
39+
export class RescheduleSessionDto {
40+
@ApiProperty({ description: 'New session start time in ISO format' })
41+
@IsDateString()
42+
startTime: string;
43+
44+
@ApiProperty({ description: 'New session end time in ISO format' })
45+
@IsDateString()
46+
endTime: string;
47+
}
48+
49+
export class RateSessionDto {
50+
@ApiProperty({ description: 'Rating from 1 to 5', minimum: 1, maximum: 5 })
51+
@IsNumber()
52+
@Min(1)
53+
@Max(5)
54+
rating: number;
55+
56+
@ApiPropertyOptional({ description: 'Review text' })
57+
@IsString()
58+
@IsOptional()
59+
review?: string;
60+
}
61+
62+
export class SessionQueryDto {
63+
@ApiPropertyOptional({ description: 'Filter by status' })
64+
@IsEnum(['pending', 'confirmed', 'completed', 'cancelled', 'no_show'])
65+
@IsOptional()
66+
status?: string;
67+
68+
@ApiPropertyOptional({ description: 'Page number', default: 1 })
69+
@IsNumber()
70+
@IsOptional()
71+
page?: number;
72+
73+
@ApiPropertyOptional({ description: 'Items per page', default: 20 })
74+
@IsNumber()
75+
@IsOptional()
76+
limit?: number;
77+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import {
2+
Entity,
3+
PrimaryGeneratedColumn,
4+
Column,
5+
CreateDateColumn,
6+
UpdateDateColumn,
7+
ManyToOne,
8+
JoinColumn,
9+
Index,
10+
} from 'typeorm';
11+
import { User } from '../../auth/entities/user.entity';
12+
13+
export enum SessionStatus {
14+
PENDING = 'pending',
15+
CONFIRMED = 'confirmed',
16+
COMPLETED = 'completed',
17+
CANCELLED = 'cancelled',
18+
NO_SHOW = 'no_show',
19+
}
20+
21+
@Entity('sessions')
22+
@Index(['mentorId'])
23+
@Index(['menteeId'])
24+
@Index(['status'])
25+
@Index(['startTime', 'endTime'])
26+
@Index(['mentorId', 'startTime', 'endTime'])
27+
export class Session {
28+
@PrimaryGeneratedColumn('uuid')
29+
id: string;
30+
31+
@Column()
32+
@Index()
33+
mentorId: string;
34+
35+
@Column()
36+
@Index()
37+
menteeId: string;
38+
39+
@Column({ type: 'timestamp' })
40+
@Index()
41+
startTime: Date;
42+
43+
@Column({ type: 'timestamp' })
44+
@Index()
45+
endTime: Date;
46+
47+
@Column({
48+
type: 'enum',
49+
enum: SessionStatus,
50+
default: SessionStatus.PENDING,
51+
})
52+
status: SessionStatus;
53+
54+
@Column({ nullable: true })
55+
meetingUrl: string;
56+
57+
@Column({ type: 'text', nullable: true })
58+
notes: string;
59+
60+
@Column({ type: 'int', nullable: true })
61+
rating: number; // 1-5 rating
62+
63+
@Column({ type: 'text', nullable: true })
64+
review: string;
65+
66+
@Column({ type: 'timestamp', nullable: true })
67+
cancelledAt: Date;
68+
69+
@Column({ nullable: true })
70+
cancellationReason: string;
71+
72+
@ManyToOne(() => User, { eager: false })
73+
@JoinColumn({ name: 'mentorId' })
74+
mentor: User;
75+
76+
@ManyToOne(() => User, { eager: false })
77+
@JoinColumn({ name: 'menteeId' })
78+
mentee: User;
79+
80+
@CreateDateColumn()
81+
createdAt: Date;
82+
83+
@UpdateDateColumn()
84+
updatedAt: Date;
85+
}

‎src/modules/sessions/index.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export { Session, SessionStatus } from './entities/session.entity';
2+
export { SessionsModule } from './sessions.module';
3+
export { SessionsService } from './sessions.service';
4+
export { SessionsController } from './sessions.controller';
5+
export {
6+
CreateSessionDto,
7+
UpdateSessionStatusDto,
8+
CancelSessionDto,
9+
RescheduleSessionDto,
10+
RateSessionDto,
11+
SessionQueryDto,
12+
} from './dto/session.dto';

0 commit comments

Comments
 (0)