Skip to content

Commit ef9e246

Browse files
Edit positions and departments (#418)
* first pass add positions/departments, modify backend to sort positions and departments, added some custom pipes for optional int parsing * add search and create functionality for departments * update imports * done adding, deleting, editing departments -- so many backend and frontend changes im dead * new migration * actually working now * fix most tsc, have to fix findAll types for departments and positions in tests * add some queries to get departments from positions * everything works, need to fix tsc and tests * fix tests * vibe code some tests * fix some tests and sorting * style fixes * add timeout to ensure sort order * fix test again * backend sorting for findAssignedTo and findCreatedBy for form instances, default sort by updatedAt desc, so the most recent forms show up at the top * fix tsc * invalidate queries after signing, remove layout for success sign page * reorganize test * adjust dropdown arrow * fix more styling --------- Co-authored-by: Elvin Cheng <elvincheng3@gmail.com>
1 parent 597e5f8 commit ef9e246

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+2408
-323
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "Position" ALTER COLUMN "departmentId" DROP NOT NULL;

apps/server/prisma/schema.prisma

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ model Position {
5555
assignedGroups AssignedGroup[]
5656
employees Employee[]
5757
58-
department Department @relation(fields: [departmentId], references: [id], onDelete: Cascade)
59-
departmentId String @db.Uuid
58+
department Department? @relation(fields: [departmentId], references: [id], onDelete: Cascade)
59+
departmentId String? @db.Uuid
6060
6161
@@unique([name, departmentId])
6262
}

apps/server/src/auth/auth.service.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ describe('AuthService', () => {
176176
expect(decodedAccessObj.firstName).toEqual(user.firstName);
177177
expect(decodedAccessObj.lastName).toEqual(user.lastName);
178178
expect(decodedAccessObj.departmentId).toEqual(
179-
user.position?.department.id,
179+
user.position?.department?.id,
180180
);
181181
expect(decodedAccessObj.scope).toEqual(user.scope);
182182
expect(decodedAccessObj.sub).toEqual(user.id);

apps/server/src/auth/auth.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export class AuthService {
5151
lastName: user.lastName,
5252
sub: user.id,
5353
positionId: user.positionId,
54-
departmentId: user.position?.department.id,
54+
departmentId: user.position?.department?.id,
5555
scope: user.scope,
5656
position: user.position,
5757
};

apps/server/src/departments/departments.controller.spec.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { Test, TestingModule } from '@nestjs/testing';
22
import { DepartmentsController } from './departments.controller';
33
import { DepartmentsService } from './departments.service';
44
import { PrismaService } from '../prisma/prisma.service';
5-
import { DepartmentEntity } from './entities/department.entity';
5+
import {
6+
DepartmentEntity,
7+
DepartmentEntityHydrated,
8+
} from './entities/department.entity';
69
import { CreateDepartmentDto } from './dto/create-department.dto';
710
import { NotFoundException } from '@nestjs/common';
811
import { Prisma } from '@prisma/client';
@@ -34,27 +37,55 @@ describe('DepartmentsController', () => {
3437
name: 'department-name',
3538
createdAt: new Date(1672531200),
3639
updatedAt: new Date(1672531200),
40+
positions: [
41+
{
42+
id: 'position-id',
43+
name: 'position-name',
44+
departmentId: 'department-id',
45+
},
46+
],
3747
},
3848
{
3949
id: 'department-id-one',
4050
name: 'department-name-one',
4151
createdAt: new Date(1672531201),
4252
updatedAt: new Date(1672531201),
53+
positions: [
54+
{
55+
id: 'position-id-one',
56+
name: 'position-name-one',
57+
departmentId: 'department-id-one',
58+
},
59+
],
4360
},
4461
];
4562

4663
const expected = [
47-
new DepartmentEntity({
64+
new DepartmentEntityHydrated({
4865
id: 'department-id',
4966
name: 'department-name',
5067
createdAt: new Date(1672531200),
5168
updatedAt: new Date(1672531200),
69+
positions: [
70+
{
71+
id: 'position-id',
72+
name: 'position-name',
73+
departmentId: 'department-id',
74+
},
75+
],
5276
}),
53-
new DepartmentEntity({
77+
new DepartmentEntityHydrated({
5478
id: 'department-id-one',
5579
name: 'department-name-one',
5680
createdAt: new Date(1672531201),
5781
updatedAt: new Date(1672531201),
82+
positions: [
83+
{
84+
id: 'position-id-one',
85+
name: 'position-name-one',
86+
departmentId: 'department-id-one',
87+
},
88+
],
5889
}),
5990
];
6091

apps/server/src/departments/departments.controller.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
Query,
1111
UseGuards,
1212
ValidationPipe,
13-
ParseIntPipe,
1413
} from '@nestjs/common';
1514
import { DepartmentsService } from './departments.service';
1615
import { CreateDepartmentDto } from './dto/create-department.dto';
@@ -21,16 +20,22 @@ import {
2120
ApiForbiddenResponse,
2221
ApiNotFoundResponse,
2322
ApiOkResponse,
23+
ApiQuery,
2424
ApiTags,
2525
ApiUnprocessableEntityResponse,
2626
} from '@nestjs/swagger';
27-
import { DepartmentEntity } from './entities/department.entity';
27+
import {
28+
DepartmentEntity,
29+
DepartmentEntityHydrated,
30+
} from './entities/department.entity';
2831
import { Prisma } from '@prisma/client';
2932
import { AppErrorMessage } from '../app.errors';
3033
import { DepartmentsErrorMessage } from './departments.errors';
3134
import { LoggerServiceImpl } from '../logger/logger.service';
3235
import { AdminAuthGuard } from '../auth/guards/admin-auth.guard';
3336
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
37+
import { OptionalParseIntPipe } from '../pipes/OptionalParseInt.pipe';
38+
import { SortOption } from '../utils';
3439

3540
@ApiTags('departments')
3641
@Controller('departments')
@@ -60,12 +65,32 @@ export class DepartmentsController {
6065

6166
@Get()
6267
@UseGuards(JwtAuthGuard)
63-
@ApiOkResponse({ type: [DepartmentEntity] })
68+
@ApiOkResponse({ type: [DepartmentEntityHydrated] })
6469
@ApiForbiddenResponse({ description: AppErrorMessage.FORBIDDEN })
6570
@ApiBadRequestResponse({ description: AppErrorMessage.UNPROCESSABLE_ENTITY })
66-
async findAll(@Query('limit', ParseIntPipe) limit?: number) {
67-
const departments = await this.departmentsService.findAll(limit);
68-
return departments.map((department) => new DepartmentEntity(department));
71+
@ApiQuery({
72+
name: 'limit',
73+
type: Number,
74+
description: 'Limit on number of positions to return',
75+
required: false,
76+
})
77+
@ApiQuery({
78+
name: 'sortBy',
79+
enum: SortOption,
80+
description: 'Departments sorting option',
81+
required: false,
82+
})
83+
async findAll(
84+
@Query('limit', OptionalParseIntPipe) limit?: number,
85+
@Query('sortBy') sortBy?: SortOption,
86+
) {
87+
const departments = await this.departmentsService.findAll({
88+
limit,
89+
sortBy,
90+
});
91+
return departments.map(
92+
(department) => new DepartmentEntityHydrated(department),
93+
);
6994
}
7095

7196
@Get(':id')

apps/server/src/departments/departments.integration.spec.ts

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { TestingModule, Test } from '@nestjs/testing';
22
import { PrismaService } from '../prisma/prisma.service';
33
import { DepartmentsService } from './departments.service';
4+
import { SortOption } from '../utils';
45

56
describe('DepartmentServiceIntegrationTest', () => {
67
let module: TestingModule;
@@ -43,16 +44,107 @@ describe('DepartmentServiceIntegrationTest', () => {
4344
await departmentsService.create({
4445
name: 'HR',
4546
});
47+
await new Promise((resolve) => setTimeout(resolve, 10));
4648
await departmentsService.create({
4749
name: 'Engineering',
4850
});
4951
});
5052

5153
it('should retrieve all departments', async () => {
52-
const departments = await departmentsService.findAll();
54+
const departments = await departmentsService.findAll({});
5355
expect(departments.length).toEqual(2);
54-
expect(departments[0].name).toEqual('HR');
55-
expect(departments[1].name).toEqual('Engineering');
56+
expect(departments[0].name).toEqual('Engineering');
57+
expect(departments[1].name).toEqual('HR');
58+
});
59+
});
60+
61+
describe('sorting', () => {
62+
beforeEach(async () => {
63+
// Create departments with different names and timestamps to test sorting
64+
for (let i = 1; i <= 10; i++) {
65+
await departmentsService.create({
66+
name: `Department ${i}`,
67+
});
68+
69+
// Add small delay to ensure different createdAt timestamps
70+
await new Promise((resolve) => setTimeout(resolve, 100));
71+
}
72+
});
73+
74+
it('sorts by name in ascending order', async () => {
75+
const departments = await departmentsService.findAll({
76+
sortBy: SortOption.NAME_ASC,
77+
});
78+
79+
expect(departments).toHaveLength(10);
80+
expect(departments[0].name).toBe('Department 1');
81+
expect(departments[1].name).toBe('Department 10');
82+
});
83+
84+
it('sorts by name in descending order', async () => {
85+
const departments = await departmentsService.findAll({
86+
sortBy: SortOption.NAME_DESC,
87+
});
88+
89+
expect(departments).toHaveLength(10);
90+
expect(departments[0].name).toBe('Department 9');
91+
expect(departments[1].name).toBe('Department 8');
92+
});
93+
94+
it('sorts by creation date in ascending order', async () => {
95+
const departments = await departmentsService.findAll({
96+
sortBy: SortOption.CREATED_AT_ASC,
97+
});
98+
99+
expect(departments).toHaveLength(10);
100+
expect(departments[0].createdAt.getUTCSeconds()).toBeLessThanOrEqual(
101+
departments[1].createdAt.getUTCSeconds(),
102+
);
103+
expect(departments[1].createdAt.getUTCSeconds()).toBeLessThanOrEqual(
104+
departments[2].createdAt.getUTCSeconds(),
105+
);
106+
});
107+
108+
it('sorts by creation date in descending order', async () => {
109+
const departments = await departmentsService.findAll({
110+
sortBy: SortOption.CREATED_AT_DESC,
111+
});
112+
113+
expect(departments).toHaveLength(10);
114+
expect(departments[0].createdAt.getUTCSeconds()).toBeGreaterThanOrEqual(
115+
departments[1].createdAt.getUTCSeconds(),
116+
);
117+
expect(departments[1].createdAt.getUTCSeconds()).toBeGreaterThanOrEqual(
118+
departments[2].createdAt.getUTCSeconds(),
119+
);
120+
});
121+
122+
it('sorts by updated date in ascending order', async () => {
123+
const departments = await departmentsService.findAll({
124+
sortBy: SortOption.UPDATED_AT_ASC,
125+
});
126+
127+
expect(departments).toHaveLength(10);
128+
expect(departments[0].updatedAt.getUTCSeconds()).toBeLessThanOrEqual(
129+
departments[1].updatedAt.getUTCSeconds(),
130+
);
131+
expect(departments[1].updatedAt.getUTCSeconds()).toBeLessThanOrEqual(
132+
departments[2].updatedAt.getUTCSeconds(),
133+
);
134+
});
135+
136+
it('sorts by updated date in descending order', async () => {
137+
const departments = await departmentsService.findAll({
138+
sortBy: SortOption.UPDATED_AT_DESC,
139+
});
140+
141+
expect(departments).toHaveLength(10);
142+
expect(departments[0].updatedAt.getUTCSeconds()).toBeGreaterThanOrEqual(
143+
departments[1].updatedAt.getUTCSeconds(),
144+
);
145+
expect(departments[1].updatedAt.getUTCSeconds()).toBeGreaterThanOrEqual(
146+
departments[2].updatedAt.getUTCSeconds(),
147+
);
56148
});
57149
});
58150

apps/server/src/departments/departments.service.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
22
import { CreateDepartmentDto } from './dto/create-department.dto';
33
import { UpdateDepartmentDto } from './dto/update-department.dto';
44
import { PrismaService } from '../prisma/prisma.service';
5+
import { SortOption, orderBy } from '../utils';
56

67
@Injectable()
78
export class DepartmentsService {
@@ -24,11 +25,22 @@ export class DepartmentsService {
2425
/**
2526
* Retrieve all departments.
2627
* @param limit the number of departments we want to retrieve (optional)
28+
* @param sortBy optional sorting parameter
2729
* @returns all departments, hydrated
2830
*/
29-
async findAll(limit?: number) {
31+
async findAll({ limit, sortBy }: { limit?: number; sortBy?: SortOption }) {
3032
const departments = await this.prisma.department.findMany({
3133
take: limit,
34+
orderBy: orderBy(sortBy),
35+
include: {
36+
positions: {
37+
select: {
38+
id: true,
39+
name: true,
40+
departmentId: true,
41+
},
42+
},
43+
},
3244
});
3345
return departments;
3446
}

apps/server/src/departments/entities/department.entity.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ApiProperty } from '@nestjs/swagger';
22
import { Department } from '@prisma/client';
33
import { Exclude } from 'class-transformer';
4+
import { PositionEntity } from '../../positions/entities/position.entity';
45

56
export class DepartmentBaseEntity {
67
@ApiProperty()
@@ -31,3 +32,18 @@ export class DepartmentEntity implements Department {
3132
Object.assign(this, partial);
3233
}
3334
}
35+
36+
export class DepartmentEntityHydrated extends DepartmentEntity {
37+
@ApiProperty()
38+
positions: PositionEntity[];
39+
40+
constructor(partial: Partial<DepartmentEntityHydrated>) {
41+
super(partial);
42+
if (partial.positions) {
43+
partial.positions = partial.positions.map(
44+
(position) => new PositionEntity(position),
45+
);
46+
}
47+
Object.assign(this, partial);
48+
}
49+
}

apps/server/src/employees/dto/create-employee.dto.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ApiProperty } from '@nestjs/swagger';
22
import { EmployeeScope } from '@prisma/client';
3+
import { Transform } from 'class-transformer';
34
import {
45
IsEmail,
56
IsNotEmpty,
@@ -20,7 +21,11 @@ export class CreateEmployeeDto {
2021
lastName: string;
2122

2223
@IsUUID()
23-
@ApiProperty()
24+
@ApiProperty({ required: false, nullable: true })
25+
@Transform(({ value }) => {
26+
if (value === 'null') return null;
27+
return value;
28+
})
2429
positionId?: string | null;
2530

2631
@IsEmail()

0 commit comments

Comments
 (0)