Phase 10.5: Pre-Launch Critical Features
Email notifications, onboarding/offboarding workflows, alternate approvers
Phase 10.5: Pre-Launch Critical Features
Goal: Address critical workflow gaps identified before production launch.
| Attribute | Value |
|---|---|
| Steps | 01-10 |
| Estimated Time | 17-24 hours |
| Dependencies | Phase 10 complete (Platform Admin) |
| Completion Gate | Email notifications working, onboarding checklist functional, alternate approvers configured |
Phase Context (READ FIRST)
What This Phase Accomplishes
- Email Notifications: Automated emails for time-off requests and approvals
- Welcome Emails: Email sent to new employees when account is created
- Onboarding Checklists: HR can create templates, assign to new hires, track progress
- Offboarding Workflow: Checklist for terminating employees (access revocation, equipment return, exit interview)
- Alternate Approvers: Configure backup approvers when managers are unavailable
What This Phase Does NOT Include
- Push notifications (future Phase 11+)
- In-app notification center (future)
- Automated policy assignment (TO-09 remains in Phase 12)
- Complex escalation chains
- Email verification for registration domains
Target User Stories
| ID | Story | Priority |
|---|---|---|
| NTF-01 | Manager receives email when time-off request submitted | CRITICAL |
| NTF-02 | Employee receives email when time-off approved/rejected | CRITICAL |
| NTF-03 | New employee receives welcome email | HIGH |
| ONB-01 | HR creates onboarding checklist templates | HIGH |
| ONB-02 | New employee sees their onboarding checklist | HIGH |
| ONB-03 | HR tracks onboarding completion progress | HIGH |
| OFF-01 | HR initiates offboarding workflow | HIGH |
| OFF-02 | HR tracks offboarding checklist completion | HIGH |
| TO-07 | HR configures alternate approvers | HIGH |
Why This Phase Exists
Gap analysis identified these as critical missing workflows compared to industry HRMS standards (BambooHR, Workday, Personio):
- No notification system: Users have no way to know when requests are submitted/approved
- No onboarding structure: New hires have no guided experience
- No offboarding checklist: High risk of missed steps during termination
- Approval bottlenecks: Requests block when managers are on vacation
Database Schema Additions
Onboarding/Offboarding Models
model OnboardingChecklist {
id String @id @default(cuid())
tenantId String
name String
description String?
type String @default("ONBOARDING") // "ONBOARDING" | "OFFBOARDING"
isDefault Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id])
items ChecklistItem[]
assignments EmployeeChecklist[]
@@index([tenantId])
@@map("onboarding_checklists")
}
model ChecklistItem {
id String @id @default(cuid())
checklistId String
title String
description String?
dueInDays Int? // Days from start date (onboarding) or end date (offboarding)
assignedTo String? // "EMPLOYEE" | "MANAGER" | "HR"
order Int
checklist OnboardingChecklist @relation(fields: [checklistId], references: [id], onDelete: Cascade)
@@index([checklistId])
@@map("checklist_items")
}
model EmployeeChecklist {
id String @id @default(cuid())
employeeId String
checklistId String
startedAt DateTime @default(now())
completedAt DateTime?
employee Employee @relation(fields: [employeeId], references: [id])
checklist OnboardingChecklist @relation(fields: [checklistId], references: [id])
progress ChecklistProgress[]
@@index([employeeId])
@@index([checklistId])
@@map("employee_checklists")
}
model ChecklistProgress {
id String @id @default(cuid())
employeeChecklistId String
itemId String
completedAt DateTime?
completedBy String? // User ID who marked complete
notes String?
employeeChecklist EmployeeChecklist @relation(fields: [employeeChecklistId], references: [id], onDelete: Cascade)
@@index([employeeChecklistId])
@@map("checklist_progress")
}Alternate Approver Model
model AlternateApprover {
id String @id @default(cuid())
tenantId String
managerId String // Primary manager who delegates
alternateId String // Backup approver
startDate DateTime?
endDate DateTime?
isActive Boolean @default(true)
createdAt DateTime @default(now())
tenant Tenant @relation(fields: [tenantId], references: [id])
manager Employee @relation("PrimaryManager", fields: [managerId], references: [id])
alternate Employee @relation("AlternateApprover", fields: [alternateId], references: [id])
@@index([tenantId])
@@index([managerId])
@@map("alternate_approvers")
}Step 01: Setup Email Service (Resend)
Input
- Phase 10 complete
Constraints
- Use Resend (not SendGrid) - simpler API, better pricing for startups
- Configure via environment variables
- No hard-coded email addresses
Task
Install Resend and create email service:
// packages/emails/src/resend.ts
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
export interface SendEmailOptions {
to: string | string[];
subject: string;
html: string;
from?: string;
}
export async function sendEmail(options: SendEmailOptions) {
const from = options.from || process.env.EMAIL_FROM || 'noreply@hrms.app';
return resend.emails.send({
from,
to: options.to,
subject: options.subject,
html: options.html,
});
}Environment variables:
RESEND_API_KEY=re_xxxxx
EMAIL_FROM=noreply@yourdomain.comCompletion Gate
- Resend installed:
npm install resend - Email service module exists
- Test email sends successfully
Step 02: Create Email Templates
Input
- Step 01 complete
Constraints
- Use React Email for templates (type-safe, maintainable)
- Keep templates simple and clean
- Include company logo placeholder
Task
Create email templates:
// packages/emails/src/templates/time-off-request.tsx
import { Html, Head, Body, Container, Text, Button, Hr } from '@react-email/components';
interface TimeOffRequestEmailProps {
employeeName: string;
leaveType: string;
startDate: string;
endDate: string;
approveUrl: string;
rejectUrl: string;
}
export function TimeOffRequestEmail({
employeeName,
leaveType,
startDate,
endDate,
approveUrl,
rejectUrl,
}: TimeOffRequestEmailProps) {
return (
<Html>
<Head />
<Body style={main}>
<Container style={container}>
<Text style={heading}>New Time-Off Request</Text>
<Text style={paragraph}>
<strong>{employeeName}</strong> has requested time off:
</Text>
<Text style={details}>
Type: {leaveType}<br />
From: {startDate}<br />
To: {endDate}
</Text>
<Hr style={hr} />
<Button style={buttonApprove} href={approveUrl}>
Approve
</Button>
<Button style={buttonReject} href={rejectUrl}>
Reject
</Button>
</Container>
</Body>
</Html>
);
}// packages/emails/src/templates/time-off-decision.tsx
export function TimeOffDecisionEmail({
decision, // 'APPROVED' | 'REJECTED'
leaveType,
startDate,
endDate,
managerName,
comment,
}: TimeOffDecisionEmailProps) {
return (
<Html>
<Head />
<Body style={main}>
<Container style={container}>
<Text style={heading}>
Time-Off Request {decision === 'APPROVED' ? 'Approved' : 'Rejected'}
</Text>
<Text style={paragraph}>
Your {leaveType} request for {startDate} to {endDate} has been
<strong>{decision === 'APPROVED' ? ' approved' : ' rejected'}</strong>
{managerName && ` by ${managerName}`}.
</Text>
{comment && (
<Text style={details}>
Comment: {comment}
</Text>
)}
</Container>
</Body>
</Html>
);
}// packages/emails/src/templates/welcome.tsx
export function WelcomeEmail({
employeeName,
companyName,
loginUrl,
}: WelcomeEmailProps) {
return (
<Html>
<Head />
<Body style={main}>
<Container style={container}>
<Text style={heading}>Welcome to {companyName}!</Text>
<Text style={paragraph}>
Hi {employeeName},
</Text>
<Text style={paragraph}>
Your account has been created. You can now access the HRMS system
to view your profile, request time off, and more.
</Text>
<Button style={button} href={loginUrl}>
Log In
</Button>
</Container>
</Body>
</Html>
);
}Completion Gate
- 3 email templates created (time-off-request, time-off-decision, welcome)
- Templates use consistent styling
- Templates have all required props typed
Step 03: Time-Off Notification Triggers
Input
- Step 02 complete
Constraints
- Trigger emails asynchronously (don't block request/response)
- Handle email failures gracefully (log, don't throw)
- Respect user preferences if notification settings exist
Task
Add email triggers to time-off service:
// apps/api/src/modules/timeoff/timeoff.service.ts
async createRequest(dto: CreateTimeOffRequestDto, employeeId: string) {
const request = await this.prisma.timeOffRequest.create({
data: { ...dto, employeeId },
include: { employee: true },
});
// Send notification to manager (async, don't await)
this.notifyManager(request).catch((err) => {
console.error('Failed to send manager notification:', err);
});
return request;
}
private async notifyManager(request: TimeOffRequest & { employee: Employee }) {
const manager = await this.findApprover(request.employee.id);
if (!manager?.email) return;
const html = render(TimeOffRequestEmail({
employeeName: request.employee.name,
leaveType: request.leaveType,
startDate: format(request.startDate, 'PPP'),
endDate: format(request.endDate, 'PPP'),
approveUrl: `${process.env.APP_URL}/timeoff/approve/${request.id}`,
rejectUrl: `${process.env.APP_URL}/timeoff/reject/${request.id}`,
}));
await sendEmail({
to: manager.email,
subject: `Time-Off Request from ${request.employee.name}`,
html,
});
}
async approveRequest(id: string, approverId: string, comment?: string) {
const request = await this.prisma.timeOffRequest.update({
where: { id },
data: {
status: 'APPROVED',
approvedBy: approverId,
approverComment: comment,
},
include: { employee: { include: { user: true } } },
});
// Notify employee
this.notifyEmployee(request, 'APPROVED', comment).catch((err) => {
console.error('Failed to send employee notification:', err);
});
return request;
}
async rejectRequest(id: string, approverId: string, comment?: string) {
const request = await this.prisma.timeOffRequest.update({
where: { id },
data: {
status: 'REJECTED',
approvedBy: approverId,
approverComment: comment,
},
include: { employee: { include: { user: true } } },
});
// Notify employee
this.notifyEmployee(request, 'REJECTED', comment).catch((err) => {
console.error('Failed to send employee notification:', err);
});
return request;
}Completion Gate
- Email sent when request created (to manager)
- Email sent when request approved (to employee)
- Email sent when request rejected (to employee)
- Failures logged but don't break the workflow
Step 04: Onboarding Checklist Model Migration
Input
- Step 03 complete
Constraints
- Follow existing Prisma migration patterns
- Add indexes for query performance
Task
Run Prisma migration:
npx prisma migrate dev --name add_onboarding_checklistsVerify with seed data:
// packages/database/prisma/seed.ts
// Add default onboarding checklist
const defaultChecklist = await prisma.onboardingChecklist.create({
data: {
tenantId: tenant.id,
name: 'Standard Onboarding',
description: 'Default checklist for new employees',
type: 'ONBOARDING',
isDefault: true,
items: {
create: [
{ title: 'Complete personal information', assignedTo: 'EMPLOYEE', order: 1 },
{ title: 'Review company handbook', assignedTo: 'EMPLOYEE', order: 2 },
{ title: 'Set up workstation', assignedTo: 'HR', order: 3, dueInDays: 1 },
{ title: 'Meet with manager', assignedTo: 'MANAGER', order: 4, dueInDays: 3 },
{ title: 'Complete IT security training', assignedTo: 'EMPLOYEE', order: 5, dueInDays: 7 },
],
},
},
});Completion Gate
- Migration runs successfully
- All 4 new models exist (OnboardingChecklist, ChecklistItem, EmployeeChecklist, ChecklistProgress)
- Seed data creates default checklist
Step 05: Onboarding Checklist API
Input
- Step 04 complete
Constraints
- Tenant-scoped (all endpoints require X-Tenant-ID)
- HR_ADMIN role required for template CRUD
- Employees can view their own checklists
Task
Create checklist controller:
// apps/api/src/modules/onboarding/onboarding.controller.ts
@Controller('api/v1/checklists')
@UseGuards(TenantGuard)
export class OnboardingController {
constructor(private readonly service: OnboardingService) {}
// Template management (HR only)
@Post()
@Roles('HR_ADMIN')
async createTemplate(@Body() dto: CreateChecklistDto, @TenantId() tenantId: string) {
return this.service.createTemplate(tenantId, dto);
}
@Get()
async listTemplates(@TenantId() tenantId: string, @Query() query: ListChecklistsDto) {
return this.service.listTemplates(tenantId, query);
}
@Get(':id')
async getTemplate(@Param('id') id: string, @TenantId() tenantId: string) {
return this.service.getTemplate(id, tenantId);
}
@Patch(':id')
@Roles('HR_ADMIN')
async updateTemplate(
@Param('id') id: string,
@Body() dto: UpdateChecklistDto,
@TenantId() tenantId: string
) {
return this.service.updateTemplate(id, tenantId, dto);
}
@Delete(':id')
@Roles('HR_ADMIN')
async deleteTemplate(@Param('id') id: string, @TenantId() tenantId: string) {
return this.service.deleteTemplate(id, tenantId);
}
}// Employee-specific checklist endpoints
@Controller('api/v1/employees/:employeeId/onboarding')
@UseGuards(TenantGuard)
export class EmployeeOnboardingController {
// Assign checklist to employee (HR only)
@Post()
@Roles('HR_ADMIN')
async assignChecklist(
@Param('employeeId') employeeId: string,
@Body() dto: AssignChecklistDto
) {
return this.service.assignChecklist(employeeId, dto.checklistId);
}
// Get employee's checklist progress (self or HR)
@Get()
async getProgress(@Param('employeeId') employeeId: string, @CurrentUser() user: User) {
// Verify access (self or HR/Manager)
return this.service.getEmployeeProgress(employeeId);
}
// Mark item complete
@Patch(':itemId')
async markComplete(
@Param('employeeId') employeeId: string,
@Param('itemId') itemId: string,
@Body() dto: CompleteItemDto,
@CurrentUser() user: User
) {
return this.service.markItemComplete(employeeId, itemId, user.id, dto.notes);
}
}API Endpoints:
| Method | Path | Role | Description |
|---|---|---|---|
| POST | /api/v1/checklists | HR_ADMIN | Create checklist template |
| GET | /api/v1/checklists | Any | List templates |
| GET | /api/v1/checklists/:id | Any | Get template details |
| PATCH | /api/v1/checklists/:id | HR_ADMIN | Update template |
| DELETE | /api/v1/checklists/:id | HR_ADMIN | Delete template |
| POST | /api/v1/employees/:id/onboarding | HR_ADMIN | Assign checklist |
| GET | /api/v1/employees/:id/onboarding | Self/HR | Get progress |
| PATCH | /api/v1/employees/:id/onboarding/:itemId | Assignee | Mark item complete |
Completion Gate
- All 8 endpoints work
- Role-based access enforced
- Tenant isolation verified
Step 06: Onboarding UI Components
Input
- Step 05 complete
Constraints
- Follow design system (gradient cards, rounded-2xl, etc.)
- Use React Query for data fetching
- Mobile-responsive
Task
1. Checklist Template Manager (HR)
// apps/web/src/app/(dashboard)/settings/onboarding/page.tsx
export default function OnboardingSettingsPage() {
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">Onboarding Checklists</h1>
<p className="text-muted-foreground">
Create templates for new employee onboarding
</p>
</div>
<CreateChecklistDialog />
</div>
<ChecklistTemplateList />
</div>
);
}2. Employee Onboarding View
// apps/web/src/components/onboarding/EmployeeChecklist.tsx
export function EmployeeChecklist({ employeeId }: { employeeId: string }) {
const { data } = useQuery({
queryKey: ['employee', employeeId, 'onboarding'],
queryFn: () => getEmployeeOnboarding(employeeId),
});
if (!data) return <EmptyState title="No checklist assigned" />;
const completedCount = data.progress.filter(p => p.completedAt).length;
const totalCount = data.checklist.items.length;
const percentComplete = Math.round((completedCount / totalCount) * 100);
return (
<ContentCard>
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-lg font-semibold">{data.checklist.name}</h3>
<p className="text-sm text-muted-foreground">
{completedCount} of {totalCount} tasks complete
</p>
</div>
<div className="text-2xl font-bold text-primary">
{percentComplete}%
</div>
</div>
<Progress value={percentComplete} className="h-2 mb-6" />
<div className="space-y-3">
{data.checklist.items.map((item) => {
const progress = data.progress.find(p => p.itemId === item.id);
return (
<ChecklistItemRow
key={item.id}
item={item}
progress={progress}
onComplete={() => handleComplete(item.id)}
/>
);
})}
</div>
</ContentCard>
);
}3. HR Onboarding Dashboard
// apps/web/src/app/(dashboard)/hr/onboarding/page.tsx
export default function HROnboardingDashboard() {
const { data: inProgress } = useQuery({
queryKey: ['onboarding', 'in-progress'],
queryFn: getOnboardingInProgress,
});
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold">Onboarding Progress</h1>
<div className="grid grid-cols-3 gap-4">
<StatsCard
title="Active Onboardings"
value={inProgress?.active || 0}
variant="blue"
/>
<StatsCard
title="Completed This Month"
value={inProgress?.completedThisMonth || 0}
variant="emerald"
/>
<StatsCard
title="Overdue Tasks"
value={inProgress?.overdue || 0}
variant="amber"
/>
</div>
<ContentCard>
<h2 className="text-lg font-semibold mb-4">In Progress</h2>
<OnboardingTable data={inProgress?.employees || []} />
</ContentCard>
</div>
);
}Completion Gate
- Checklist template management works
- Employee can view and complete their checklist
- HR can track all onboarding progress
- Progress percentage calculated correctly
- Design system followed
Step 07: Offboarding Workflow
Input
- Step 06 complete
Constraints
- Reuse checklist infrastructure (type: "OFFBOARDING")
- Trigger when employee status changes to TERMINATED
- Include standard items: access revocation, equipment return, exit interview
Task
Create default offboarding template:
// Seed data
const offboardingChecklist = await prisma.onboardingChecklist.create({
data: {
tenantId: tenant.id,
name: 'Standard Offboarding',
description: 'Checklist for departing employees',
type: 'OFFBOARDING',
isDefault: true,
items: {
create: [
{ title: 'Revoke system access', assignedTo: 'HR', order: 1, dueInDays: 0 },
{ title: 'Collect company equipment', assignedTo: 'HR', order: 2, dueInDays: 1 },
{ title: 'Transfer knowledge/documents', assignedTo: 'EMPLOYEE', order: 3, dueInDays: -3 },
{ title: 'Conduct exit interview', assignedTo: 'HR', order: 4, dueInDays: -1 },
{ title: 'Process final paycheck', assignedTo: 'HR', order: 5, dueInDays: 7 },
{ title: 'Update org chart', assignedTo: 'HR', order: 6, dueInDays: 1 },
],
},
},
});Auto-assign offboarding when employee terminated:
// apps/api/src/modules/employee/employee.service.ts
async updateEmployee(id: string, dto: UpdateEmployeeDto, tenantId: string) {
const employee = await this.prisma.employee.findUnique({ where: { id } });
// Check if status is changing to TERMINATED
if (dto.status === 'TERMINATED' && employee.status !== 'TERMINATED') {
// Auto-assign default offboarding checklist
await this.onboardingService.assignDefaultOffboarding(id, tenantId);
}
return this.prisma.employee.update({
where: { id },
data: dto,
});
}Completion Gate
- Default offboarding template exists
- Offboarding auto-assigned when employee terminated
- HR can view offboarding progress
- Offboarding items have negative dueInDays (before end date)
Step 08: Alternate Approvers Migration
Input
- Step 07 complete
Task
Run migration for AlternateApprover model:
npx prisma migrate dev --name add_alternate_approversCompletion Gate
- AlternateApprover table exists
- Indexes created for performance
Step 09: Alternate Approvers Implementation
Input
- Step 08 complete
Constraints
- HR_ADMIN can configure alternate approvers
- Time-based delegation (startDate/endDate optional)
- Approval logic checks for alternates when manager unavailable
Task
1. API Endpoints:
@Controller('api/v1/alternate-approvers')
@UseGuards(TenantGuard)
@Roles('HR_ADMIN')
export class AlternateApproverController {
@Post()
async create(@Body() dto: CreateAlternateDto, @TenantId() tenantId: string) {
return this.service.create(tenantId, dto);
}
@Get()
async list(@TenantId() tenantId: string) {
return this.service.list(tenantId);
}
@Delete(':id')
async remove(@Param('id') id: string, @TenantId() tenantId: string) {
return this.service.remove(id, tenantId);
}
}2. Update Time-Off Approval Logic:
// apps/api/src/modules/timeoff/timeoff.service.ts
async findApprover(employeeId: string): Promise<Employee | null> {
const employee = await this.prisma.employee.findUnique({
where: { id: employeeId },
include: { primaryManager: true },
});
if (!employee?.primaryManager) return null;
const manager = employee.primaryManager;
// Check if manager is on leave
const managerOnLeave = await this.isEmployeeOnLeave(manager.id);
if (managerOnLeave) {
// Find alternate approver
const alternate = await this.prisma.alternateApprover.findFirst({
where: {
managerId: manager.id,
isActive: true,
OR: [
{ startDate: null, endDate: null },
{
startDate: { lte: new Date() },
endDate: { gte: new Date() },
},
],
},
include: { alternate: true },
});
if (alternate) {
return alternate.alternate;
}
}
return manager;
}
private async isEmployeeOnLeave(employeeId: string): Promise<boolean> {
const today = new Date();
const activeLeave = await this.prisma.timeOffRequest.findFirst({
where: {
employeeId,
status: 'APPROVED',
startDate: { lte: today },
endDate: { gte: today },
},
});
return !!activeLeave;
}3. UI for Managing Alternates:
// apps/web/src/app/(dashboard)/settings/approvers/page.tsx
export default function AlternateApproversPage() {
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">Alternate Approvers</h1>
<p className="text-muted-foreground">
Configure backup approvers for when managers are unavailable
</p>
</div>
<CreateAlternateDialog />
</div>
<AlternateApproversList />
</div>
);
}Completion Gate
- HR can create/delete alternate approver assignments
- Time-off requests route to alternate when manager on leave
- Date-based delegation works (startDate/endDate)
- UI shows current alternate approver assignments
Step 10: Integration Testing
Input
- All previous steps complete
Task
Create integration tests for new features:
// Test: Email notifications
describe('Time-Off Email Notifications', () => {
it('should send email to manager on request creation', async () => {
// Mock Resend
const sendSpy = jest.spyOn(resend.emails, 'send');
await createTimeOffRequest(employeeId, requestDto);
expect(sendSpy).toHaveBeenCalledWith(
expect.objectContaining({
to: manager.email,
subject: expect.stringContaining('Time-Off Request'),
})
);
});
it('should send email to employee on approval', async () => {
const sendSpy = jest.spyOn(resend.emails, 'send');
await approveTimeOffRequest(requestId, managerId);
expect(sendSpy).toHaveBeenCalledWith(
expect.objectContaining({
to: employee.email,
subject: expect.stringContaining('Approved'),
})
);
});
});
// Test: Alternate approvers
describe('Alternate Approvers', () => {
it('should route to alternate when manager is on leave', async () => {
// Setup: Manager on approved leave today
await createApprovedLeave(managerId, today, today);
// Setup: Alternate approver configured
await createAlternateApprover(managerId, alternateId);
// Create time-off request
await createTimeOffRequest(employeeId, requestDto);
// Verify notification sent to alternate
expect(sendSpy).toHaveBeenCalledWith(
expect.objectContaining({
to: alternate.email,
})
);
});
});
// Test: Onboarding checklist
describe('Onboarding Checklist', () => {
it('should track completion progress', async () => {
// Assign checklist
await assignChecklist(employeeId, checklistId);
// Mark first item complete
await markItemComplete(employeeId, item1Id);
// Check progress
const progress = await getEmployeeProgress(employeeId);
expect(progress.completedCount).toBe(1);
expect(progress.percentComplete).toBe(20); // 1/5 items
});
});Completion Gate
- Email notification tests pass
- Onboarding checklist tests pass
- Alternate approver tests pass
- Integration test suite runs successfully
Quick Reference
New API Endpoints
| Method | Path | Guard | Description |
|---|---|---|---|
| POST | /api/v1/checklists | TenantGuard, HR_ADMIN | Create checklist template |
| GET | /api/v1/checklists | TenantGuard | List templates |
| GET | /api/v1/checklists/:id | TenantGuard | Get template |
| PATCH | /api/v1/checklists/:id | TenantGuard, HR_ADMIN | Update template |
| DELETE | /api/v1/checklists/:id | TenantGuard, HR_ADMIN | Delete template |
| POST | /api/v1/employees/:id/onboarding | TenantGuard, HR_ADMIN | Assign checklist |
| GET | /api/v1/employees/:id/onboarding | TenantGuard | Get progress |
| PATCH | /api/v1/employees/:id/onboarding/:itemId | TenantGuard | Mark complete |
| POST | /api/v1/alternate-approvers | TenantGuard, HR_ADMIN | Create alternate |
| GET | /api/v1/alternate-approvers | TenantGuard, HR_ADMIN | List alternates |
| DELETE | /api/v1/alternate-approvers/:id | TenantGuard, HR_ADMIN | Remove alternate |
New Pages
| Path | Description |
|---|---|
| /settings/onboarding | Checklist template management |
| /hr/onboarding | HR onboarding dashboard |
| /settings/approvers | Alternate approver configuration |
| /my/onboarding | Employee's own checklist |
File Structure
apps/
├── api/src/modules/
│ ├── onboarding/
│ │ ├── onboarding.module.ts
│ │ ├── onboarding.controller.ts
│ │ └── onboarding.service.ts
│ └── alternate-approvers/
│ ├── alternate-approvers.module.ts
│ ├── alternate-approvers.controller.ts
│ └── alternate-approvers.service.ts
├── web/src/
│ ├── app/(dashboard)/
│ │ ├── settings/onboarding/
│ │ ├── settings/approvers/
│ │ └── hr/onboarding/
│ └── components/onboarding/
│ ├── ChecklistTemplateList.tsx
│ ├── EmployeeChecklist.tsx
│ └── CreateChecklistDialog.tsx
└── packages/emails/
├── src/resend.ts
└── src/templates/
├── time-off-request.tsx
├── time-off-decision.tsx
└── welcome.tsxSuccess Criteria
After Phase 10.5 completion:
| Feature | Verification |
|---|---|
| Email: Time-off request | Manager receives email when employee submits request |
| Email: Time-off decision | Employee receives email when request approved/rejected |
| Email: Welcome | New employee receives welcome email on account creation |
| Onboarding: Templates | HR can create/edit onboarding checklist templates |
| Onboarding: Assignment | HR can assign checklist to new employee |
| Onboarding: Progress | Employee can view and complete checklist items |
| Onboarding: Tracking | HR can see all employees' onboarding progress |
| Offboarding: Auto-assign | Offboarding checklist auto-assigned when employee terminated |
| Offboarding: Tracking | HR can track offboarding completion |
| Alternate: Configuration | HR can configure backup approvers |
| Alternate: Routing | Requests route to alternate when manager on leave |
Next Phase
After Phase 10.5, proceed to:
- Phase 11: Production Deployment - Deploy to Google Cloud with Cloud SQL, MongoDB Atlas, Secret Manager, Redis, and multi-service CI/CD
Phase Completion Checklist (MANDATORY)
BEFORE LAUNCHING
Complete ALL items before proceeding to production. Do NOT skip any step.
1. Gate Verification
- All step gates passed
- Email notifications working
- Onboarding checklists functional
- Alternate approvers working
- All core features tested
2. Update PROJECT_STATE.md
- Mark Phase 10.5 as COMPLETED with timestamp
- Update "Current Phase" to PRODUCTION
- Add session log entry
- Document final locked files3. Update WHAT_EXISTS.md
## Complete Inventory
- All database models documented
- All API endpoints documented
- All frontend routes documented
- All patterns documented4. Git Tag & Commit
git add PROJECT_STATE.md WHAT_EXISTS.md
git commit -m "chore: complete Phase 10.5 - Pre-Launch"
git tag phase-10.5-pre-launch
git tag v1.0.0-mvpMVP Code Complete
All code phases completed. System is ready for production deployment.
Next Step: Phase 11: Production Deployment
See also: Deployment Guide for detailed infrastructure documentation.
Last Updated: 2025-11-30