170 lines
5.1 KiB
TypeScript
170 lines
5.1 KiB
TypeScript
import { Router, Request, Response, NextFunction } from 'express'
|
|
import { z } from 'zod'
|
|
import {
|
|
listEmployees,
|
|
inviteEmployee,
|
|
updateEmployeeRole,
|
|
deactivateEmployee,
|
|
reactivateEmployee,
|
|
removeEmployee,
|
|
} from '../services/teamService'
|
|
import { requireCompanyAuth } from '../middleware/requireCompanyAuth'
|
|
import { requireTenant } from '../middleware/requireTenant'
|
|
import { requireSubscription } from '../middleware/requireSubscription'
|
|
import { requireRole } from '../middleware/requireRole'
|
|
|
|
const router = Router()
|
|
|
|
// All team routes require a valid company employee session + active subscription
|
|
router.use(requireCompanyAuth, requireTenant, requireSubscription)
|
|
|
|
// ─── Validation schemas ───────────────────────────────────────
|
|
|
|
const inviteSchema = z.object({
|
|
firstName: z.string().min(1).max(64),
|
|
lastName: z.string().min(1).max(64),
|
|
email: z.string().email(),
|
|
role: z.enum(['MANAGER', 'AGENT']), // OWNER cannot be invited
|
|
})
|
|
|
|
const updateRoleSchema = z.object({
|
|
role: z.enum(['MANAGER', 'AGENT']),
|
|
})
|
|
|
|
// ─── GET /team ────────────────────────────────────────────────
|
|
// List all employees for this company.
|
|
// All roles can view the team list.
|
|
|
|
router.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const members = await listEmployees(req.companyId)
|
|
res.json({ data: members })
|
|
} catch (err) {
|
|
next(err)
|
|
}
|
|
})
|
|
|
|
// ─── GET /team/stats ──────────────────────────────────────────
|
|
// Quick counts for dashboard stat cards.
|
|
|
|
router.get('/stats', async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const members = await listEmployees(req.companyId)
|
|
const stats = {
|
|
total: members.length,
|
|
active: members.filter((m) => m.isActive && m.invitationStatus === 'accepted').length,
|
|
pending: members.filter((m) => m.invitationStatus === 'pending').length,
|
|
inactive: members.filter((m) => !m.isActive && m.invitationStatus === 'accepted').length,
|
|
}
|
|
res.json({ data: stats })
|
|
} catch (err) {
|
|
next(err)
|
|
}
|
|
})
|
|
|
|
// ─── POST /team/invite ────────────────────────────────────────
|
|
// Invite a new employee by email. OWNER only.
|
|
|
|
router.post(
|
|
'/invite',
|
|
requireRole('OWNER'),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const body = inviteSchema.parse(req.body)
|
|
const result = await inviteEmployee(
|
|
req.companyId,
|
|
req.employee.id,
|
|
body
|
|
)
|
|
res.status(201).json({ data: result })
|
|
} catch (err) {
|
|
next(err)
|
|
}
|
|
}
|
|
)
|
|
|
|
// ─── PATCH /team/:id/role ─────────────────────────────────────
|
|
// Change an employee's role. OWNER only.
|
|
|
|
router.patch(
|
|
'/:id/role',
|
|
requireRole('OWNER'),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const body = updateRoleSchema.parse(req.body)
|
|
const updated = await updateEmployeeRole(
|
|
req.companyId,
|
|
req.employee.id,
|
|
req.employee.role,
|
|
req.params.id,
|
|
body
|
|
)
|
|
res.json({ data: updated })
|
|
} catch (err) {
|
|
next(err)
|
|
}
|
|
}
|
|
)
|
|
|
|
// ─── POST /team/:id/deactivate ────────────────────────────────
|
|
// Suspend access without deleting the record. OWNER only.
|
|
|
|
router.post(
|
|
'/:id/deactivate',
|
|
requireRole('OWNER'),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const updated = await deactivateEmployee(
|
|
req.companyId,
|
|
req.employee.role,
|
|
req.params.id
|
|
)
|
|
res.json({ data: updated })
|
|
} catch (err) {
|
|
next(err)
|
|
}
|
|
}
|
|
)
|
|
|
|
// ─── POST /team/:id/reactivate ────────────────────────────────
|
|
// Restore a deactivated employee's access. OWNER only.
|
|
|
|
router.post(
|
|
'/:id/reactivate',
|
|
requireRole('OWNER'),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const updated = await reactivateEmployee(
|
|
req.companyId,
|
|
req.employee.role,
|
|
req.params.id
|
|
)
|
|
res.json({ data: updated })
|
|
} catch (err) {
|
|
next(err)
|
|
}
|
|
}
|
|
)
|
|
|
|
// ─── DELETE /team/:id ─────────────────────────────────────────
|
|
// Permanently remove an employee + revoke their Clerk session/invite. OWNER only.
|
|
|
|
router.delete(
|
|
'/:id',
|
|
requireRole('OWNER'),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const result = await removeEmployee(
|
|
req.companyId,
|
|
req.employee.role,
|
|
req.params.id
|
|
)
|
|
res.json({ data: result })
|
|
} catch (err) {
|
|
next(err)
|
|
}
|
|
}
|
|
)
|
|
|
|
export default router
|