Files
Reactive-Resume/packages/schema/src/applications/data.ts
T
FalconSpyClaude Opus 5.5Amruth Pillaiautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1b78e546e2 feat(applications): schedule interviews and view them on a calendar (#3539)
* feat(applications): schedule interviews and view them on a calendar

Interviews (screening, technical, behavioral, onsite, other) are stored as
"interview" entries on an application's activity timeline, so an application
can have any number of them and they show in its timeline without a
migration. Each interview has a start date-time (timezone-aware), duration,
and optional location and notes.

- schema: interview timeline entry type, interviewDetailsSchema, INTERVIEW_KINDS
- api: addInterview / updateInterview procedures (delete via timeline entry);
  generic timeline updates now only edit text on note entries
- web: Calendar view on the Applications page (month grid, type legend,
  upcoming list grouped by day, schedule button with application picker,
  per-day "+" and "+N more" popover), Interviews section and interview
  dialog in the application detail panel, interview rows in the timeline
  and CSV export
- mcp: add_application_interview / update_application_interview tools
- i18n: extract new strings into all locale catalogs (English fallback)
- docs: MCP tool table, scheduling guide section, resume-builder skill

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(applications): keep interview dates in local time and guard generic timeline edits

- Format the timeline date chip for interview entries in the viewer's local
  timezone so it matches the interview's date-time label; stage and note
  entries still render in UTC.
- Reject interview entries in the generic updateTimelineEntry path. A
  day-granular date edit kept the UTC time of day and could move an interview
  to the wrong local day; callers are pointed to updateInterview
  (update_application_interview). The MCP tool description now says so, and
  a service test covers the rejection.
- Associate each interview dialog label with its control via useId/htmlFor.
- Drop the duplicate onInput handler on the date-time input; onChange covers
  controlled inputs.
- Give the calendar's per-day schedule button an accessible name that
  includes the date, and update the extracted locale catalogs for the new
  message.

* [autofix.ci] apply automated fixes

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-09-28 08:57:25 +02:00

91 lines
3.6 KiB
TypeScript

import z from "zod";
// Pipeline stages are a fixed enum for v1. If per-user custom stages are ever needed,
// promote this to a table. `rejected` is a terminal stage; `archived` (a boolean on the
// row) hides an application from the board without deleting it.
export const applicationStatusSchema = z.enum(["saved", "applied", "screening", "interview", "offer", "rejected"]);
export type ApplicationStatus = z.infer<typeof applicationStatusSchema>;
// Ordered stage metadata shared by the API (validation) and the web board (columns/colors).
export const STAGES = [
{ value: "saved", label: "Saved", color: "oklch(0.62 0 0)" },
{ value: "applied", label: "Applied", color: "oklch(0.52 0.19 285)" },
{ value: "screening", label: "Screening", color: "oklch(0.45 0.08 195)" },
{ value: "interview", label: "Interview", color: "oklch(0.5 0.1 70)" },
{ value: "offer", label: "Offer", color: "oklch(0.55 0.15 152)" },
{ value: "rejected", label: "Rejected", color: "oklch(0.63 0.12 22)" },
] as const satisfies ReadonlyArray<{ value: ApplicationStatus; label: string; color: string }>;
export const contactSchema = z.object({
name: z.string().trim().min(1),
role: z.string().trim().default(""),
// Free-form label shown as a pill: "Recruiter", "Referral", "Hiring Manager"…
type: z.string().trim().default(""),
email: z
.string()
.trim()
.refine((value) => value === "" || z.email().safeParse(value).success, "Invalid email address.")
.default(""),
phone: z.string().trim().default(""),
});
export type Contact = z.infer<typeof contactSchema>;
const timelineBaseSchema = z.object({
id: z.string().min(1),
at: z.coerce.date(),
});
export const interviewKindSchema = z.enum(["screening", "technical", "behavioral", "onsite", "other"]);
export type InterviewKind = z.infer<typeof interviewKindSchema>;
export const INTERVIEW_KINDS = [
{ value: "screening", label: "Screening", color: "oklch(0.45 0.08 195)" },
{ value: "technical", label: "Technical", color: "oklch(0.52 0.19 285)" },
{ value: "behavioral", label: "Behavioral", color: "oklch(0.5 0.1 70)" },
{ value: "onsite", label: "Onsite", color: "oklch(0.55 0.15 152)" },
{ value: "other", label: "Other", color: "oklch(0.62 0 0)" },
] as const satisfies ReadonlyArray<{ value: InterviewKind; label: string; color: string }>;
// Interview details editable by the user. `at` on the timeline entry is the scheduled start
// (full timestamp, unlike stage/note entries which are day-granular).
export const interviewDetailsSchema = z.object({
kind: interviewKindSchema,
durationMinutes: z
.number()
.int()
.min(5)
.max(24 * 60)
.default(60),
location: z.string().trim().max(500).default(""),
notes: z.string().trim().max(5000).default(""),
});
export type InterviewDetails = z.infer<typeof interviewDetailsSchema>;
export const applicationTimelineEntrySchema = z.discriminatedUnion("type", [
timelineBaseSchema.extend({
type: z.literal("stage"),
stage: applicationStatusSchema,
}),
timelineBaseSchema.extend({
type: z.literal("note"),
text: z.string().trim().min(1),
}),
timelineBaseSchema.extend({
type: z.literal("interview"),
...interviewDetailsSchema.shape,
}),
]);
export type ApplicationTimelineEntry = z.infer<typeof applicationTimelineEntrySchema>;
export type InterviewTimelineEntry = Extract<ApplicationTimelineEntry, { type: "interview" }>;
// Reserved for AI enrichment output (autofill / match-score). Free-form so the shape can
// evolve without a migration. See the AI roadmap in the applications feature.
export const aiMetadataSchema = z.record(z.string(), z.unknown());
export type AiMetadata = z.infer<typeof aiMetadataSchema>;