Skip to content

Commit b6a8094

Browse files
committed
feat: add airtable submit
1 parent b264092 commit b6a8094

File tree

9 files changed

+1026
-6
lines changed

9 files changed

+1026
-6
lines changed

LOGIN_PAGE_API_DOCUMENTATION.md

Lines changed: 589 additions & 2 deletions
Large diffs are not rendered by default.

owl-api/src/admin/admin.controller.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ export class AdminController {
1919
return this.adminService.getAllSubmissions();
2020
}
2121

22+
@Get('edit-requests')
23+
@UseGuards(RolesGuard)
24+
@Roles(Role.Admin)
25+
async getAllEditRequests() {
26+
return this.adminService.getAllEditRequests();
27+
}
28+
2229
@Put('submissions/:id')
2330
@UseGuards(RolesGuard)
2431
@Roles(Role.Admin)

owl-api/src/admin/admin.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import { Module } from '@nestjs/common';
22
import { AdminController } from './admin.controller';
33
import { AdminService } from './admin.service';
44
import { PrismaService } from '../prisma.service';
5+
import { AirtableModule } from '../airtable/airtable.module';
56

67
@Module({
8+
imports: [AirtableModule],
79
controllers: [AdminController],
810
providers: [AdminService, PrismaService],
911
})

owl-api/src/admin/admin.service.ts

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common';
22
import { PrismaService } from '../prisma.service';
33
import { UpdateSubmissionDto } from './dto/update-submission.dto';
4+
import { AirtableService } from '../airtable/airtable.service';
45

56
@Injectable()
67
export class AdminService {
7-
constructor(private prisma: PrismaService) {}
8+
constructor(
9+
private prisma: PrismaService,
10+
private airtableService: AirtableService,
11+
) {}
812

913
async getAllSubmissions() {
1014
const submissions = await this.prisma.submission.findMany({
@@ -17,6 +21,16 @@ export class AdminService {
1721
firstName: true,
1822
lastName: true,
1923
email: true,
24+
birthday: true,
25+
addressLine1: true,
26+
addressLine2: true,
27+
city: true,
28+
state: true,
29+
country: true,
30+
zipCode: true,
31+
airtableRecId: true,
32+
createdAt: true,
33+
updatedAt: true,
2034
},
2135
},
2236
},
@@ -77,9 +91,122 @@ export class AdminService {
7791
},
7892
});
7993

94+
// If submission is approved, create Airtable record
95+
if (updateSubmissionDto.approvalStatus === 'approved' && !submission.project.airtableRecId) {
96+
try {
97+
const airtableData = {
98+
user: {
99+
firstName: submission.project.user.firstName,
100+
lastName: submission.project.user.lastName,
101+
email: submission.project.user.email,
102+
birthday: submission.project.user.birthday,
103+
addressLine1: submission.project.user.addressLine1,
104+
addressLine2: submission.project.user.addressLine2,
105+
city: submission.project.user.city,
106+
state: submission.project.user.state,
107+
country: submission.project.user.country,
108+
zipCode: submission.project.user.zipCode,
109+
},
110+
project: {
111+
projectTitle: submission.project.projectTitle,
112+
description: submission.project.description,
113+
playableUrl: submission.project.playableUrl,
114+
repoUrl: submission.project.repoUrl,
115+
screenshotUrl: submission.project.screenshotUrl,
116+
nowHackatimeHours: submission.project.nowHackatimeHours,
117+
},
118+
submission: {
119+
description: submission.description,
120+
playableUrl: submission.playableUrl,
121+
repoUrl: submission.repoUrl,
122+
screenshotUrl: submission.screenshotUrl,
123+
},
124+
};
125+
126+
const airtableResult = await this.airtableService.createYSWSSubmission(airtableData);
127+
128+
// Update project with Airtable record ID
129+
await this.prisma.project.update({
130+
where: { projectId: submission.projectId },
131+
data: { airtableRecId: airtableResult.recordId },
132+
});
133+
134+
// Update user with Airtable record ID if not already set
135+
if (!submission.project.user.airtableRecId) {
136+
await this.prisma.user.update({
137+
where: { userId: submission.project.userId },
138+
data: { airtableRecId: airtableResult.recordId },
139+
});
140+
}
141+
142+
// Update Airtable record with approved hours if provided
143+
if (updateSubmissionDto.approvedHours !== undefined) {
144+
await this.airtableService.updateYSWSSubmission(airtableResult.recordId, {
145+
approvedHours: updateSubmissionDto.approvedHours,
146+
hoursJustification: updateSubmissionDto.hoursJustification,
147+
});
148+
}
149+
} catch (error) {
150+
console.error('Error creating Airtable record:', error);
151+
// Don't throw error here to avoid breaking the submission update
152+
}
153+
}
154+
80155
return updatedSubmission;
81156
}
82157

158+
async getAllEditRequests() {
159+
const editRequests = await this.prisma.editRequest.findMany({
160+
include: {
161+
user: {
162+
select: {
163+
userId: true,
164+
firstName: true,
165+
lastName: true,
166+
email: true,
167+
birthday: true,
168+
addressLine1: true,
169+
addressLine2: true,
170+
city: true,
171+
state: true,
172+
country: true,
173+
zipCode: true,
174+
airtableRecId: true,
175+
createdAt: true,
176+
updatedAt: true,
177+
},
178+
},
179+
project: {
180+
select: {
181+
projectId: true,
182+
projectTitle: true,
183+
projectType: true,
184+
description: true,
185+
playableUrl: true,
186+
repoUrl: true,
187+
screenshotUrl: true,
188+
nowHackatimeHours: true,
189+
airtableRecId: true,
190+
isLocked: true,
191+
createdAt: true,
192+
updatedAt: true,
193+
},
194+
},
195+
reviewer: {
196+
select: {
197+
userId: true,
198+
firstName: true,
199+
lastName: true,
200+
email: true,
201+
},
202+
},
203+
},
204+
orderBy: { createdAt: 'desc' },
205+
});
206+
207+
return editRequests;
208+
}
209+
83210
async unlockProject(projectId: number, adminUserId: number) {
84211
const project = await this.prisma.project.findUnique({
85212
where: { projectId },
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Module } from '@nestjs/common';
2+
import { AirtableService } from './airtable.service';
3+
4+
@Module({
5+
providers: [AirtableService],
6+
exports: [AirtableService],
7+
})
8+
export class AirtableModule {}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
2+
3+
@Injectable()
4+
export class AirtableService {
5+
private readonly BASE_ID = 'appsibjo37dhUSTQp';
6+
private readonly YSWS_TABLE_ID = 'tblZEEoz2V2kFJHIk';
7+
private readonly AIRTABLE_API_KEY = process.env.USER_SERVICE_AIRTABLE_API_KEY;
8+
9+
async createYSWSSubmission(data: {
10+
user: {
11+
firstName: string;
12+
lastName: string;
13+
email: string;
14+
birthday: Date;
15+
addressLine1: string;
16+
addressLine2?: string;
17+
city: string;
18+
state: string;
19+
country: string;
20+
zipCode: string;
21+
};
22+
project: {
23+
projectTitle: string;
24+
description: string;
25+
playableUrl: string;
26+
repoUrl: string;
27+
screenshotUrl: string;
28+
nowHackatimeHours: number;
29+
};
30+
submission: {
31+
description: string;
32+
playableUrl: string;
33+
repoUrl: string;
34+
screenshotUrl: string;
35+
};
36+
}): Promise<{ recordId: string }> {
37+
if (!this.AIRTABLE_API_KEY) {
38+
throw new HttpException(
39+
'Airtable API key not configured',
40+
HttpStatus.INTERNAL_SERVER_ERROR,
41+
);
42+
}
43+
44+
try {
45+
const fields = {
46+
'First Name': data.user.firstName,
47+
'Last Name': data.user.lastName,
48+
'Email': data.user.email,
49+
'Birthday': data.user.birthday.toISOString().split('T')[0],
50+
'Address (Line 1)': data.user.addressLine1,
51+
'Address (Line 2)': data.user.addressLine2 || '',
52+
'City': data.user.city,
53+
'State / Province': data.user.state,
54+
'Country': data.user.country,
55+
'ZIP / Postal Code': data.user.zipCode,
56+
'Code URL': data.project.repoUrl,
57+
'Playable URL': data.project.playableUrl,
58+
'Description': data.project.description,
59+
'Screenshot': [
60+
{
61+
url: data.project.screenshotUrl,
62+
filename: `screenshot-${Date.now()}.png`
63+
}
64+
],
65+
'Optional - Override Hours Spent': data.project.nowHackatimeHours,
66+
'Automation - First Submitted At': new Date().toISOString(),
67+
'Automation - Submit to Unified YSWS': true,
68+
};
69+
70+
const response = await fetch(
71+
`https://api.airtable.com/v0/${this.BASE_ID}/${this.YSWS_TABLE_ID}`,
72+
{
73+
method: 'POST',
74+
headers: {
75+
Authorization: `Bearer ${this.AIRTABLE_API_KEY}`,
76+
'Content-Type': 'application/json',
77+
},
78+
body: JSON.stringify({
79+
records: [
80+
{
81+
fields,
82+
},
83+
],
84+
}),
85+
},
86+
);
87+
88+
if (!response.ok) {
89+
const errorData = await response.json();
90+
console.error('Airtable API error:', errorData);
91+
throw new HttpException(
92+
'Failed to create YSWS submission record',
93+
response.status || HttpStatus.BAD_REQUEST,
94+
);
95+
}
96+
97+
const result = await response.json();
98+
return { recordId: result.records[0].id };
99+
} catch (error) {
100+
console.error('Error creating YSWS submission:', error);
101+
if (error instanceof HttpException) {
102+
throw error;
103+
}
104+
throw new HttpException(
105+
'Internal server error',
106+
HttpStatus.INTERNAL_SERVER_ERROR,
107+
);
108+
}
109+
}
110+
111+
async updateYSWSSubmission(recordId: string, data: {
112+
approvedHours?: number;
113+
hoursJustification?: string;
114+
status?: string;
115+
}): Promise<void> {
116+
if (!this.AIRTABLE_API_KEY) {
117+
throw new HttpException(
118+
'Airtable API key not configured',
119+
HttpStatus.INTERNAL_SERVER_ERROR,
120+
);
121+
}
122+
123+
try {
124+
const fields: any = {};
125+
126+
if (data.approvedHours !== undefined) {
127+
fields['Optional - Override Hours Spent'] = data.approvedHours;
128+
}
129+
130+
if (data.hoursJustification !== undefined) {
131+
fields['Optional - Override Hours Spent Justification'] = data.hoursJustification;
132+
}
133+
134+
const response = await fetch(
135+
`https://api.airtable.com/v0/${this.BASE_ID}/${this.YSWS_TABLE_ID}/${recordId}`,
136+
{
137+
method: 'PATCH',
138+
headers: {
139+
Authorization: `Bearer ${this.AIRTABLE_API_KEY}`,
140+
'Content-Type': 'application/json',
141+
},
142+
body: JSON.stringify({
143+
fields,
144+
}),
145+
},
146+
);
147+
148+
if (!response.ok) {
149+
const errorData = await response.json();
150+
console.error('Airtable update error:', errorData);
151+
throw new HttpException(
152+
'Failed to update YSWS submission record',
153+
response.status || HttpStatus.BAD_REQUEST,
154+
);
155+
}
156+
} catch (error) {
157+
console.error('Error updating YSWS submission:', error);
158+
if (error instanceof HttpException) {
159+
throw error;
160+
}
161+
throw new HttpException(
162+
'Internal server error',
163+
HttpStatus.INTERNAL_SERVER_ERROR,
164+
);
165+
}
166+
}
167+
}

owl-api/src/app.module.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { ProjectsModule } from "./projects/projects.module";
66
import { AdminModule } from "./admin/admin.module";
77
import { EditRequestsModule } from "./edit-requests/edit-requests.module";
88
import { HealthModule } from "./health/health.module";
9-
import { ConfigModule } from "@nestjs/config";
109

1110
//todo: dynamically enable & disable modules based on env. this will allow separate modules to run as separate services
1211
@Module({

owl-api/src/edit-requests/edit-requests.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import { Module } from '@nestjs/common';
22
import { EditRequestsController } from './edit-requests.controller';
33
import { EditRequestsService } from './edit-requests.service';
44
import { PrismaService } from '../prisma.service';
5+
import { AirtableModule } from '../airtable/airtable.module';
56

67
@Module({
8+
imports: [AirtableModule],
79
controllers: [EditRequestsController],
810
providers: [EditRequestsService, PrismaService],
911
})

0 commit comments

Comments
 (0)