feat: complete offline interview review workflow - #137
Conversation
There was a problem hiding this comment.
Pull request overview
This PR completes the offline interview review workflow end-to-end: it removes the unused AI drafting features, expands Feishu interview scheduling to track meeting end/minutes events for archival, and updates evaluation + approval rules/UI to require lecturer evaluation content + recommendation and keep archived decisions immutable. It also introduces a Docker-backed local dev workflow and standardizes several dashboard surfaces (including email record filters) with shadcn/ui styling.
Changes:
- Add local dev workflow (
dev:local) and enableinngest-clibuilds; remove server-side OpenAI/AI draft features across UI/actions/docs/env. - Complete Feishu offline interview scheduling lifecycle: store meeting unique ID, handle meeting-ended + minutes-generated events, and propagate “ended” state into the workflow/UI.
- Tighten evaluation/approval flow: require lecturer content + recommendation, prevent archived decision mutation, restrict phone visibility to admins while allowing lecturers to view QQ; refresh related UI/tests.
Reviewed changes
Copilot reviewed 62 out of 62 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/dev-local.sh | New local dev script orchestrating DB + Inngest + Next dev with cleanup. |
| README.md | Remove AI draft feature references and OpenAI env docs. |
| queue/interviewScheduleReminder.ts | Reminder cards now use QQ and offline interview wording. |
| pnpm-workspace.yaml | Allow inngest-cli to build/install. |
| package.json | Add dev:local; ensure INNGEST_DEV=1 for dev:full Next process. |
| migrations/meta/_journal.json | Register new migrations 0027–0029. |
| migrations/0027_interview_schedule_event_state.sql | Add meeting state fields + active-schedule uniqueness index. |
| migrations/0028_offline_interview_email_copy.sql | Update interview schedule email copy to offline wording. |
| migrations/0029_interview_evaluation_recommendation.sql | Add lecturer recommendation enum + column. |
| lib/feishu/interview-schedule.ts | Surface Feishu meeting unique ID from calendar events. |
| lib/feishu/interview-message.ts | Schedule cards updated for offline “recording/minutes” semantics and QQ display. |
| lib/feishu/interview-event.ts | New utilities to normalize Feishu meeting/minutes event payloads. |
| lib/feishu/interview-event.test.ts | Unit tests for Feishu event payload normalization. |
| lib/evaluation-state.ts | Remove obsolete state helpers; simplify action→step mapping. |
| lib/evaluation-state.test.ts | Update tests to reflect removed helpers/actions. |
| lib/email/interview-template-settings.ts | Update defaults and remove meeting/schedule link variables for candidate emails. |
| lib/email/interview-schedule.tsx | Stop providing meeting/schedule link variables; keep offline email rendering. |
| lib/email-center/render.test.ts | Update render test inputs to match new interview template variables. |
| lib/email-center/registry.ts | Update template descriptions/variables for offline schedule emails. |
| lib/ai/openai.ts | Remove OpenAI client helper (AI drafts removed). |
| hooks/useUserList.ts | Split visibility flags to expose QQ to lecturers while keeping phone admin-only. |
| hooks/useUserInfoById.ts | Same visibility split for user detail; QQ for lecturers, phone for admins only. |
| emails/interview-schedule.tsx | Candidate email updated for offline interview (no meeting/schedule links). |
| docs/FEISHU_INTERVIEW_SCHEDULING_PLAN.md | Update plan/spec for offline-only emails and meeting/minutes archival workflow. |
| docker-compose.dev.yml | Use fully-qualified postgres image reference. |
| db/schema.ts | Add recommendation enum/column; add schedule meeting state fields + partial unique index. |
| components/userInfo/basic.tsx | Simplify college field UI; remove unused college list Select. |
| components/userInfo/basic.test.tsx | Update tests for disabled college input (no combobox). |
| components/ui/card.tsx | Adjust Card radius/shadow styling. |
| components/review/selectProblem.tsx | Review UI surface styling adjustments. |
| components/review/selectedRangeDisplay.tsx | Factor storage reader into lib helper; add “missing range” notice component. |
| components/review/selectedRangeDisplay.test.tsx | Add coverage for new notice behavior. |
| components/review/qrcodeScanner.tsx | Review scanner styling adjustments. |
| components/review/probCheckBox.tsx | Review selection styling adjustments. |
| components/review/markProblemTable.tsx | Restructure markup from Card to semantic sections; styling tweaks. |
| components/review/mannualInput.tsx | Remove inline “missing range” warning (handled elsewhere). |
| components/recruitment/table.tsx | Recruitment table surface styling tweaks. |
| components/recruitment/recruitmentContent.tsx | Recruitment header section styling tweaks. |
| components/recruitment/evaluationTable.tsx | Offline interview workflow UI: QQ display, schedule-ended confirmation, required content + recommendation, remove AI drafting/reopen/reject paths. |
| components/recruitment/evaluationTable.test.tsx | Add test for required evaluation content; update candidate fields for QQ/recommendation/schedule state. |
| components/manage/viewUserInfoSheet.tsx | Remove AI summary; show QQ to lecturers; keep phone admin-only. |
| components/manage/manageTable.tsx | Show QQ column/details to lecturers; keep phone gated to admins. |
| components/manage/manageTable.test.tsx | Add test to ensure lecturers see QQ but not phone. |
| components/manage/interviewEvaluation.tsx | Require content; add lecturer recommendation selection; update copy for “不通过”. |
| components/manage/approvalsContent.tsx | Make archives immutable; add archive filters/search/pagination and show lecturer recommendation badge; remove reopen/unapprove actions. |
| components/manage/approvalsContent.test.tsx | Add coverage for archive search + recommendation labels. |
| components/flow/table.tsx | Flow table styling tweaks. |
| components/email/EmailTemplateManagementSection.tsx | Update interview template help text; styling tweaks. |
| components/email/EmailSendingTasksSection.tsx | Remove shadow styling. |
| components/email/EmailRecordsSection.tsx | Replace native selects with shadcn Select + hidden inputs for filter submission. |
| components/email/EmailOverviewSection.tsx | Remove extra borders/shadows in overview surfaces. |
| components/email/EmailBatchTasksSection.tsx | Remove shadow styling. |
| CLAUDE.md | Remove optional AI draft mention from stack summary. |
| app/dashboard/review/page.tsx | Update layout; integrate ReviewRangeNotice. |
| app/api/feishu/events/route.ts | Expand Feishu event handling for meeting-ended + minutes-generated; update schedule/evaluation linkage; require verification token config. |
| app/api/auth/feishu/route.ts | Harden OAuth state validation and restrict binding to lecturer+ roles. |
| action/user-flow/interviewSchedule.ts | Offline schedule creation/reschedule/cancel rules; store meeting unique ID; add manual meeting-ended confirmation action; update card/email copy. |
| action/user-flow/evaluation.ts | Add lecturer recommendation; prevent archived rewrites; link evaluations to schedules; improve admin listings with minutes fallback. |
| action/user-flow/edit.ts | Prevent non-written flows from bypassing approval via direct outcome edits. |
| action/email/test-send.ts | Update test-send payload for interview schedule email variables. |
| action/ai/candidate.ts | Remove AI server actions (candidate summary + evaluation draft). |
| .env.example | Remove OpenAI envs; add local INNGEST_DEV=1 note. |
Suppressed comments (1)
action/user-flow/interviewSchedule.ts:494
- The initial
idempotencyKeyonly includesstartsAt. If a first attempt partially succeeds and a retry changesendsAtwhile keeping the same start time, Feishu may return the original event due to idempotency, producing inconsistent meeting metadata.
Include endsAt in the key so retries with a different end time won’t reuse the earlier event.
timezone: DEFAULT_TIMEZONE,
idempotencyKey: `people-interview-${input.userFlowId}-${startsAt.getTime()}`,
});
| timezone: DEFAULT_TIMEZONE, | ||
| idempotencyKey: `people-interview-${input.userFlowId}-${startsAt.getTime()}-recreate-${Date.now()}`, | ||
| idempotencyKey: `people-interview-${input.userFlowId}-${startsAt.getTime()}-recreate`, | ||
| }); |
| const filters = deliveryPage.filters; | ||
| const [status, setStatus] = useState(filters.status || "all"); | ||
| const [category, setCategory] = useState(filters.category || "all"); | ||
| const [flowId, setFlowId] = useState(filters.flowId || "all"); |
| const result = await db.transaction(async (tx) => { | ||
| const [currentFlow] = await tx | ||
| .select({ progressStatus: userFlow.progressStatus }) | ||
| .from(userFlow) | ||
| .where(eq(userFlow.id, userFlowId)) | ||
| .limit(1); | ||
|
|
||
| if (!currentFlow) { | ||
| return { | ||
| success: false as const, | ||
| error: { message: "报名流程不存在" }, | ||
| }; | ||
| } | ||
| if (currentFlow.progressStatus === "passed") { | ||
| return { | ||
| success: false as const, | ||
| error: { | ||
| message: "该候选人流程已结束;如需调整成员权限,请在成员管理中操作", | ||
| }, | ||
| }; | ||
| } | ||
| if (currentFlow.progressStatus === "failed") { | ||
| return { | ||
| success: false as const, | ||
| error: { | ||
| message: "该候选人流程已结束;如需重新评估,请重新报名并完整走流程", | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| const active = await findActiveEvaluationInTx(tx, userFlowId); |
Co-authored-by: zzemy <86453687+zzemy@users.noreply.github.com>
Scope
Database migrations
Apply migrations
0027_interview_schedule_event_state.sql,0028_offline_interview_email_copy.sql, and0029_interview_evaluation_recommendation.sqlbefore deploying the application.Validation
pnpm lintpnpm exec tsc --noEmitpnpm test(61 suites, 149 tests)