Skip to content

Commit 7754cff

Browse files
committed
feat: add user feedback field
1 parent 127e03c commit 7754cff

File tree

5 files changed

+68
-31
lines changed

5 files changed

+68
-31
lines changed

lark-ui/src/routes/admin/+page.svelte

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
submissionId: number;
2323
approvalStatus: string;
2424
approvedHours: number | null;
25-
hoursJustification: string | null;
25+
hoursJustification: string | null; // User feedback - what user submits with their submission
2626
description: string | null;
2727
playableUrl: string | null;
2828
repoUrl: string | null;
@@ -40,6 +40,7 @@
4040
nowHackatimeHours: number | null;
4141
nowHackatimeProjects: string[] | null;
4242
approvedHours: number | null;
43+
hoursJustification: string | null; // Admin's justification - synced to Airtable
4344
user: AdminLightUser;
4445
};
4546
};
@@ -55,6 +56,7 @@
5556
repoUrl: string | null;
5657
screenshotUrl: string | null;
5758
approvedHours: number | null;
59+
hoursJustification: string | null; // Admin's justification - synced to Airtable
5860
isLocked: boolean;
5961
createdAt: string;
6062
updatedAt: string;
@@ -96,12 +98,15 @@ const toSubmissionDraft = (submission: AdminSubmission) => ({
9698
approvedHours: submission.project.approvedHours !== null
9799
? submission.project.approvedHours.toString()
98100
: (submission.project.nowHackatimeHours !== null ? submission.project.nowHackatimeHours.toFixed(1) : ''),
99-
hoursJustification: submission.hoursJustification ?? '',
101+
// User feedback - what admin sends to user via email
102+
userFeedback: submission.hoursJustification ?? '',
103+
// Hours justification - admin's internal notes for Airtable
104+
hoursJustification: submission.project.hoursJustification ?? '',
100105
sendEmailNotification: false
101106
});
102107
103108
const buildSubmissionDrafts = (list: AdminSubmission[]) => {
104-
const drafts: Record<number, { approvalStatus: string; approvedHours: string; hoursJustification: string; sendEmailNotification: boolean }> = {};
109+
const drafts: Record<number, { approvalStatus: string; approvedHours: string; userFeedback: string; hoursJustification: string; sendEmailNotification: boolean }> = {};
105110
for (const submission of list) {
106111
drafts[submission.submissionId] = toSubmissionDraft(submission);
107112
}
@@ -181,6 +186,7 @@ function generateBillyLink(hackatimeAccount: string | null): string | null {
181186
182187
const statusIdFor = (submissionId: number) => `submission-${submissionId}-status`;
183188
const hoursIdFor = (submissionId: number) => `submission-${submissionId}-hours`;
189+
const userFeedbackIdFor = (submissionId: number) => `submission-${submissionId}-user-feedback`;
184190
const justificationIdFor = (submissionId: number) => `submission-${submissionId}-justification`;
185191
186192
const apiUrl = data.apiUrl;
@@ -190,7 +196,7 @@ let projectsLoading = $state(false);
190196
let usersLoading = $state(false);
191197
let metricsLoading = $state(false);
192198
193-
let submissionDrafts = $state<Record<number, { approvalStatus: string; approvedHours: string; hoursJustification: string; sendEmailNotification: boolean }>>(
199+
let submissionDrafts = $state<Record<number, { approvalStatus: string; approvedHours: string; userFeedback: string; hoursJustification: string; sendEmailNotification: boolean }>>(
194200
buildSubmissionDrafts(data.submissions ?? [])
195201
);
196202
let submissionSaving = $state<Record<number, boolean>>({});
@@ -371,6 +377,7 @@ async function recalculateAllProjectsHours() {
371377
const payload = {
372378
approvalStatus: draft.approvalStatus,
373379
approvedHours: draft.approvedHours === '' ? null : parseFloat(draft.approvedHours),
380+
userFeedback: draft.userFeedback === '' ? null : draft.userFeedback,
374381
hoursJustification: draft.hoursJustification === '' ? null : draft.hoursJustification,
375382
sendEmail: shouldSendEmail,
376383
};
@@ -409,14 +416,15 @@ async function recalculateAllProjectsHours() {
409416
submissionSuccess = { ...submissionSuccess, [submission.submissionId]: '' };
410417
411418
const draft = submissionDrafts[submission.submissionId];
419+
const userFeedback = draft?.userFeedback || '';
412420
const hoursJustification = draft?.hoursJustification || '';
413421
414422
try {
415423
const response = await fetch(`${apiUrl}/api/admin/submissions/${submission.submissionId}/quick-approve`, {
416424
method: 'POST',
417425
headers: { 'Content-Type': 'application/json' },
418426
credentials: 'include',
419-
body: JSON.stringify({ hoursJustification }),
427+
body: JSON.stringify({ userFeedback, hoursJustification }),
420428
});
421429
422430
if (!response.ok) {
@@ -1177,11 +1185,18 @@ function normalizeUrl(url: string | null): string | null {
11771185

11781186
{#if selectedSubmission.hoursJustification}
11791187
<div class="space-y-2 bg-blue-950/30 border border-blue-800 rounded-lg p-4">
1180-
<h4 class="text-sm font-semibold uppercase tracking-wide text-blue-300">Hours Justification</h4>
1188+
<h4 class="text-sm font-semibold uppercase tracking-wide text-blue-300">User Feedback</h4>
11811189
<p class="text-sm text-gray-300">{selectedSubmission.hoursJustification}</p>
11821190
</div>
11831191
{/if}
11841192

1193+
{#if selectedSubmission.project.hoursJustification}
1194+
<div class="space-y-2 bg-purple-950/30 border border-purple-800 rounded-lg p-4">
1195+
<h4 class="text-sm font-semibold uppercase tracking-wide text-purple-300">Hours Justification (Admin Only)</h4>
1196+
<p class="text-sm text-gray-300">{selectedSubmission.project.hoursJustification}</p>
1197+
</div>
1198+
{/if}
1199+
11851200
<div class="flex flex-wrap gap-2">
11861201
<h4 class="text-sm font-semibold uppercase tracking-wide text-gray-400 w-full">Quick Actions</h4>
11871202
{#if selectedSubmission.playableUrl || selectedSubmission.project.playableUrl}
@@ -1267,12 +1282,21 @@ function normalizeUrl(url: string | null): string | null {
12671282
/>
12681283
</div>
12691284
<div class="space-y-2">
1270-
<label class="text-sm font-medium text-gray-300" for={justificationIdFor(selectedSubmission.submissionId)}>Hours Justification</label>
1285+
<label class="text-sm font-medium text-gray-300" for={userFeedbackIdFor(selectedSubmission.submissionId)}>User Feedback (sent via email)</label>
1286+
<textarea
1287+
id={userFeedbackIdFor(selectedSubmission.submissionId)}
1288+
class="w-full rounded-lg border border-blue-600 bg-gray-800 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
1289+
rows="2"
1290+
placeholder="Feedback to send to the user..."
1291+
bind:value={submissionDrafts[selectedSubmission.submissionId].userFeedback}></textarea>
1292+
</div>
1293+
<div class="space-y-2">
1294+
<label class="text-sm font-medium text-gray-300" for={justificationIdFor(selectedSubmission.submissionId)}>Hours Justification (admin only, synced to Airtable)</label>
12711295
<textarea
12721296
id={justificationIdFor(selectedSubmission.submissionId)}
1273-
class="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500"
1297+
class="w-full rounded-lg border border-purple-600 bg-gray-800 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500"
12741298
rows="2"
1275-
placeholder="Explain the approved hours..."
1299+
placeholder="Internal justification for Airtable..."
12761300
bind:value={submissionDrafts[selectedSubmission.submissionId].hoursJustification}></textarea>
12771301
</div>
12781302
</div>

lark-ui/src/routes/app/projects/[id]/+page.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,10 @@
129129
</div>
130130
</div>
131131

132-
{#if status === 'rejected'}
132+
{#if status === 'rejected' || status === 'approved'}
133133
<div class="tracking-feedback">
134134
<div class="feedback-body">
135-
<div class="feedback-label">Reviewer Feedback</div>
135+
<div class="feedback-label">{status === 'approved' ? 'Feedback' : 'Reviewer Feedback'}</div>
136136
<div class="feedback-text">{submission.hoursJustification || 'No feedback provided'}</div>
137137
</div>
138138
</div>

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,10 @@ export class AdminController {
4242
@Roles(Role.Admin)
4343
async quickApproveSubmission(
4444
@Param('id', ParseIntPipe) id: number,
45-
@Body() body: { hoursJustification?: string },
45+
@Body() body: { userFeedback?: string; hoursJustification?: string },
4646
@Req() req: Request,
4747
) {
48-
return this.adminService.quickApproveSubmission(id, req.user.userId, body.hoursJustification);
48+
return this.adminService.quickApproveSubmission(id, req.user.userId, body.hoursJustification, body.userFeedback);
4949
}
5050

5151
@Put('projects/:id/unlock')

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

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ export class AdminService {
9090
if (updateSubmissionDto.approvedHours !== undefined) {
9191
updateData.approvedHours = updateSubmissionDto.approvedHours;
9292
}
93-
if (updateSubmissionDto.hoursJustification !== undefined) {
94-
updateData.hoursJustification = updateSubmissionDto.hoursJustification;
93+
if (updateSubmissionDto.userFeedback !== undefined) {
94+
updateData.hoursJustification = updateSubmissionDto.userFeedback;
9595
}
9696
if (updateSubmissionDto.approvalStatus !== undefined) {
9797
updateData.approvalStatus = updateSubmissionDto.approvalStatus;
@@ -123,6 +123,11 @@ export class AdminService {
123123
if (updateSubmissionDto.approvedHours !== undefined) {
124124
projectUpdateData.approvedHours = updateSubmissionDto.approvedHours;
125125
}
126+
// hoursJustification is now admin-only for the project (stored in the updateSubmissionDto but used for airtable)
127+
// We store it in the project table for admin reference and Airtable sync only
128+
if (updateSubmissionDto.hoursJustification !== undefined) {
129+
projectUpdateData.hoursJustification = updateSubmissionDto.hoursJustification;
130+
}
126131
if (updateSubmissionDto.approvalStatus === 'approved') {
127132
// When approving, update project with submission data
128133
projectUpdateData.playableUrl = submission.playableUrl;
@@ -145,6 +150,11 @@ export class AdminService {
145150
if (!isResubmission) {
146151
// First submission - create Airtable record
147152
try {
153+
// Get the project record to access the admin's hoursJustification
154+
const projectRecord = await this.prisma.project.findUnique({
155+
where: { projectId: submission.projectId },
156+
});
157+
148158
const approvedProjectData = {
149159
user: {
150160
firstName: submission.project.user.firstName,
@@ -163,7 +173,7 @@ export class AdminService {
163173
repoUrl: submission.repoUrl || submission.project.repoUrl || '',
164174
screenshotUrl: submission.screenshotUrl || submission.project.screenshotUrl || '',
165175
approvedHours: updateSubmissionDto.approvedHours || 0,
166-
hoursJustification: updateSubmissionDto.hoursJustification || '',
176+
hoursJustification: projectRecord?.hoursJustification || updateSubmissionDto.hoursJustification || '',
167177
description: submission.description || submission.project.description || undefined,
168178
},
169179
};
@@ -189,13 +199,17 @@ export class AdminService {
189199
} else {
190200
// Resubmission - update existing Airtable record
191201
try {
202+
const projectRecord = await this.prisma.project.findUnique({
203+
where: { projectId: submission.projectId },
204+
});
205+
192206
await this.airtableService.updateApprovedProject(submission.project.airtableRecId, {
193207
playableUrl: submission.playableUrl || undefined,
194208
repoUrl: submission.repoUrl || undefined,
195209
screenshotUrl: submission.screenshotUrl || undefined,
196210
description: submission.description || undefined,
197211
approvedHours: updateSubmissionDto.approvedHours,
198-
hoursJustification: updateSubmissionDto.hoursJustification,
212+
hoursJustification: projectRecord?.hoursJustification || updateSubmissionDto.hoursJustification,
199213
});
200214
} catch (error) {
201215
console.error('Error updating Approved Projects record in Airtable:', error);
@@ -225,7 +239,7 @@ export class AdminService {
225239
projectId: updatedSubmission.project.projectId,
226240
approved: updateSubmissionDto.approvalStatus === 'approved',
227241
approvedHours: updateSubmissionDto.approvedHours,
228-
feedback: updateSubmissionDto.hoursJustification,
242+
feedback: updatedSubmission.hoursJustification,
229243
},
230244
);
231245
console.log(`Email sent for submission ${submissionId} because sendEmail was explicitly true`);
@@ -296,7 +310,7 @@ export class AdminService {
296310
return updatedSubmission;
297311
}
298312

299-
async quickApproveSubmission(submissionId: number, adminUserId: number, providedJustification?: string) {
313+
async quickApproveSubmission(submissionId: number, adminUserId: number, providedJustification?: string, userFeedback?: string) {
300314
const submission = await this.prisma.submission.findUnique({
301315
where: { submissionId },
302316
include: {
@@ -314,12 +328,12 @@ export class AdminService {
314328

315329
const hackatimeHours = submission.project.nowHackatimeHours || 0;
316330
const autoJustification = `Quick approved with ${hackatimeHours.toFixed(1)} Hackatime hours tracked on Midnight project.`;
317-
const hoursJustification = providedJustification || submission.hoursJustification || autoJustification;
331+
const adminHoursJustification = providedJustification || autoJustification;
318332

319333
const updateData: any = {
320334
approvalStatus: 'approved',
321335
approvedHours: hackatimeHours,
322-
hoursJustification: hoursJustification,
336+
hoursJustification: userFeedback || '',
323337
reviewedBy: adminUserId.toString(),
324338
reviewedAt: new Date(),
325339
};
@@ -343,20 +357,18 @@ export class AdminService {
343357
},
344358
});
345359

346-
// Update project with new data from submission
347360
await this.prisma.project.update({
348361
where: { projectId: submission.projectId },
349362
data: {
350363
approvedHours: hackatimeHours,
351-
hoursJustification: hoursJustification,
364+
hoursJustification: adminHoursJustification,
352365
playableUrl: submission.playableUrl,
353366
repoUrl: submission.repoUrl,
354367
screenshotUrl: submission.screenshotUrl,
355368
description: submission.description,
356369
},
357370
});
358371

359-
// Check if this is a resubmission (project already has Airtable record)
360372
const isResubmission = !!submission.project.airtableRecId;
361373

362374
if (!isResubmission) {
@@ -388,7 +400,7 @@ export class AdminService {
388400
repoUrl: repoUrl,
389401
screenshotUrl: submission.screenshotUrl || submission.project.screenshotUrl || '',
390402
approvedHours: hackatimeHours,
391-
hoursJustification: hoursJustification,
403+
hoursJustification: adminHoursJustification,
392404
description: submission.description || submission.project.description || undefined,
393405
},
394406
};
@@ -420,17 +432,13 @@ export class AdminService {
420432
screenshotUrl: submission.screenshotUrl || undefined,
421433
description: submission.description || undefined,
422434
approvedHours: hackatimeHours,
423-
hoursJustification: hoursJustification,
435+
hoursJustification: adminHoursJustification,
424436
});
425437
} catch (error) {
426438
console.error('Error updating Approved Projects record in Airtable:', error);
427439
}
428440
}
429441

430-
// Note: quickApproveSubmission doesn't send email by default
431-
// Email should be sent via the regular updateSubmission endpoint with sendEmail flag
432-
433-
// Auto-approve pending edit requests when submission is approved
434442
try {
435443
const pendingEditRequests = await this.prisma.editRequest.findMany({
436444
where: {

owl-api/src/admin/dto/update-submission.dto.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ export class UpdateSubmissionDto {
88
@IsString()
99
@IsOptional()
1010
@MaxLength(500)
11-
hoursJustification?: string;
11+
userFeedback?: string; // Feedback for the user, sent via email, stored in submission table
12+
13+
@IsString()
14+
@IsOptional()
15+
@MaxLength(500)
16+
hoursJustification?: string; // Admin's internal justification, synced to Airtable, stored in project table
1217

1318
@IsEnum(['pending', 'approved', 'rejected'])
1419
@IsOptional()

0 commit comments

Comments
 (0)