Skip to main content

Leave Balance

This guide covers how to check your leave balance, understand entitlements, and view how leave is calculated.


Overview

What is Leave Balance?

Your leave balance shows your remaining time off entitlement:

  1. Annual Entitlement - Total days you're entitled to per year
  2. Used Leave - Days already taken or approved
  3. Remaining Balance - Days still available
  4. Pending Requests - Days awaiting approval

Accessing Leave Balance

  1. Navigate to Staff PortalLeave from the sidebar
  2. Your current balance is shown at the top of the page
  3. Click on the balance card for detailed breakdown

Leave Balance API

Fetch Your Balance

Endpoint: GET /api/staff/leave-balance

// Source: src/app/api/staff/leave-balance/route.ts:25-65
const leaveBalanceQuerySchema = z.object({
year: z.coerce.number().min(2020).max(2099).optional(),
include_breakdown: z.enum(['true', 'false']).optional().default('true'),
include_history: z.enum(['true', 'false']).optional().default('false')
})

Query Parameters

ParameterTypeRequiredDescription
yearnumberNoLeave year (default: current year)
include_breakdownbooleanNoInclude detailed breakdown (default: true)
include_historybooleanNoInclude usage history (default: false)

Example Request

// Fetch current year's balance
const getLeaveBalance = async () => {
const response = await fetch('/api/staff/leave-balance')
return response.json()
}

// Fetch specific year with history
const getBalanceWithHistory = async (year: number) => {
const response = await fetch(
`/api/staff/leave-balance?year=${year}&include_history=true`
)
return response.json()
}

Response Format

// Leave balance response
{
success: true,
data: {
year: 2025,
staff_id: "staff-uuid",

// Summary
entitlement: 28.0,
used: 10.0,
pending: 5.0,
remaining: 13.0,

// Breakdown by type
breakdown: {
annual: {
entitlement: 28.0,
used: 10.0,
pending: 5.0,
remaining: 13.0
},
toil: {
accrued: 3.5,
used: 1.0,
remaining: 2.5
}
},

// Policy details
policy: {
carry_over_allowed: true,
carry_over_max: 5.0,
carry_over_expiry: "2025-03-31",
pro_rata_applied: false
},

// Dates
leave_year_start: "2025-01-01",
leave_year_end: "2025-12-31",
calculated_at: "2025-01-14T10:00:00Z"
}
}

Balance Display

Balance Overview Component

// Leave balance display
const LeaveBalanceOverview = () => {
const { data: balance, isLoading } = useLeaveBalance()

if (isLoading) return <BalanceSkeleton />

return (
<div className="card-glass p-6">
{/* Main balance */}
<div className="text-center">
<div className="text-4xl font-bold text-coral-gradient">
{balance.remaining}
</div>
<div className="text-secondary-text mt-1">days remaining</div>
</div>

{/* Progress bar */}
<div className="mt-6">
<div className="flex justify-between text-sm mb-2">
<span>Used: {balance.used} days</span>
<span>Total: {balance.entitlement} days</span>
</div>
<div className="h-3 bg-white/10 rounded-full overflow-hidden">
<div
className="h-full bg-coral-gradient rounded-full transition-all"
style={{ width: `${(balance.used / balance.entitlement) * 100}%` }}
/>
</div>
</div>

{/* Pending indicator */}
{balance.pending > 0 && (
<div className="mt-4 p-3 bg-yellow-500/10 rounded-lg text-sm">
<span className="text-yellow-400 font-medium">
{balance.pending} days pending approval
</span>
</div>
)}
</div>
)
}

Balance Breakdown

Detailed Breakdown

// Balance breakdown display
const LeaveBalanceBreakdown = ({ breakdown }) => (
<div className="card-glass p-6 space-y-6">
<h3 className="font-semibold">Leave Breakdown</h3>

{/* Annual Leave */}
<div className="space-y-2">
<div className="flex justify-between">
<span>Annual Leave</span>
<span className="font-medium">{breakdown.annual.remaining} days</span>
</div>
<div className="grid grid-cols-3 gap-4 text-sm">
<div>
<span className="text-secondary-text">Entitlement</span>
<div>{breakdown.annual.entitlement} days</div>
</div>
<div>
<span className="text-secondary-text">Used</span>
<div>{breakdown.annual.used} days</div>
</div>
<div>
<span className="text-secondary-text">Pending</span>
<div>{breakdown.annual.pending} days</div>
</div>
</div>
</div>

{/* TOIL */}
{breakdown.toil && (
<div className="space-y-2 pt-4 border-t border-white/10">
<div className="flex justify-between">
<span>Time Off In Lieu (TOIL)</span>
<span className="font-medium">{breakdown.toil.remaining} days</span>
</div>
<div className="grid grid-cols-3 gap-4 text-sm">
<div>
<span className="text-secondary-text">Accrued</span>
<div>{breakdown.toil.accrued} days</div>
</div>
<div>
<span className="text-secondary-text">Used</span>
<div>{breakdown.toil.used} days</div>
</div>
<div>
<span className="text-secondary-text">Remaining</span>
<div>{breakdown.toil.remaining} days</div>
</div>
</div>
</div>
)}
</div>
)

Entitlement Calculation

Annual Entitlement

Your annual leave entitlement is based on:

FactorDescription
Base EntitlementStandard company allowance
Service YearsAdditional days for long service
Contract TypeFull-time vs part-time ratio
Pro-RataAdjusted for start date if mid-year

Calculation Formula

// Source: src/lib/utils/leave.ts:25-80
// Calculate annual leave entitlement
const calculateEntitlement = (staff: Staff): number => {
const { company, contract_type, employment_start_date, fte_ratio } = staff

// Base entitlement from company policy
let entitlement = company.leave_policy.base_annual_days

// Add service-based additional days
const yearsOfService = calculateYearsOfService(employment_start_date)
const additionalDays = getServiceBonus(yearsOfService, company.leave_policy)
entitlement += additionalDays

// Apply FTE ratio for part-time staff
if (contract_type !== 'full_time' && fte_ratio) {
entitlement = entitlement * fte_ratio
}

// Apply pro-rata for mid-year starters
if (isCurrentYearStarter(employment_start_date)) {
const monthsRemaining = getMonthsRemainingInYear(employment_start_date)
entitlement = (entitlement / 12) * monthsRemaining
}

return Math.round(entitlement * 2) / 2 // Round to nearest 0.5
}

Service Bonus Example

Years of ServiceAdditional Days
0-2 years0 days
3-5 years1 day
6-10 years2 days
10+ years3 days

Part-Time Calculation

FTE (Full-Time Equivalent)

Part-time staff receive pro-rata entitlement:

// FTE calculation examples
const fteExamples = [
{ hours: 37.5, fte: 1.0, entitlement: 28 }, // Full-time
{ hours: 30, fte: 0.8, entitlement: 22.4 }, // 4 days/week
{ hours: 22.5, fte: 0.6, entitlement: 16.8 }, // 3 days/week
{ hours: 15, fte: 0.4, entitlement: 11.2 }, // 2 days/week
]

Part-Time Display

// Part-time balance info
const PartTimeBalanceInfo = ({ staff, balance }) => {
if (staff.fte_ratio === 1.0) return null

return (
<div className="p-4 bg-blue-500/10 rounded-lg text-sm">
<div className="font-medium text-blue-400">Part-Time Calculation</div>
<p className="mt-1 text-secondary-text">
Your entitlement is pro-rated based on your contracted hours
({staff.contracted_hours} hours/week = {staff.fte_ratio * 100}% FTE)
</p>
<div className="mt-2 grid grid-cols-2 gap-4">
<div>
<span className="text-secondary-text">Full-time equivalent</span>
<div>{balance.full_time_equivalent} days</div>
</div>
<div>
<span className="text-secondary-text">Your entitlement</span>
<div>{balance.entitlement} days</div>
</div>
</div>
</div>
)
}

Carry Over

Carry Over Policy

Unused leave may carry forward:

PolicyDescription
AllowedCan carry over unused days
MaximumMax days that can carry over
ExpiryDate by which carried leave must be used

Carry Over Display

// Carry over information
const CarryOverInfo = ({ policy, balance }) => {
if (!policy.carry_over_allowed) {
return (
<div className="p-4 bg-orange-500/10 rounded-lg">
<span className="text-orange-400 font-medium">No Carry Over</span>
<p className="text-sm text-secondary-text mt-1">
Unused leave cannot be carried to next year. Use it or lose it!
</p>
</div>
)
}

return (
<div className="p-4 bg-white/5 rounded-lg space-y-3">
<div className="font-medium">Carry Over Policy</div>

<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-secondary-text">Maximum carry over</span>
<div>{policy.carry_over_max} days</div>
</div>
<div>
<span className="text-secondary-text">Must be used by</span>
<div>{moment(policy.carry_over_expiry).format('D MMMM YYYY')}</div>
</div>
</div>

{balance.carried_over > 0 && (
<div className="pt-3 border-t border-white/10">
<div className="flex justify-between">
<span>Carried from last year</span>
<span className="font-medium">{balance.carried_over} days</span>
</div>
{moment().isAfter(policy.carry_over_expiry) && (
<p className="text-sm text-red-400 mt-1">
Carry over has expired - these days are no longer available
</p>
)}
</div>
)}
</div>
)
}

Leave History

Usage History

// Fetch balance with history
const { data } = await fetch('/api/staff/leave-balance?include_history=true')

// History response
{
// ... balance data ...
history: [
{
id: "request-uuid-1",
leave_type: "annual",
start_date: "2025-01-06",
end_date: "2025-01-10",
total_days: 5.0,
status: "approved",
reason: "Holiday"
},
{
id: "request-uuid-2",
leave_type: "annual",
start_date: "2024-12-23",
end_date: "2024-12-27",
total_days: 3.0,
status: "approved",
reason: "Christmas break"
}
]
}

History Display

// Leave history list
const LeaveHistoryList = ({ history }) => (
<div className="card-glass p-6">
<h3 className="font-semibold mb-4">Leave History</h3>

<div className="space-y-3">
{history.map(entry => (
<div key={entry.id} className="flex justify-between items-center p-3 bg-white/5 rounded-lg">
<div>
<div className="font-medium">
{moment(entry.start_date).format('D MMM')} -
{moment(entry.end_date).format('D MMM YYYY')}
</div>
<div className="text-sm text-secondary-text">{entry.reason}</div>
</div>
<div className="text-right">
<div className="font-medium">
{entry.total_days} {entry.total_days === 1 ? 'day' : 'days'}
</div>
<LeaveStatusBadge status={entry.status} size="sm" />
</div>
</div>
))}
</div>
</div>
)

TOIL (Time Off In Lieu)

What is TOIL?

Time Off In Lieu is compensatory time off for extra hours worked:

AspectDescription
AccrualEarned by working overtime
UsageUsed like annual leave
ExpiryMay expire after set period
ApprovalManager approves accrual and usage

TOIL Balance

// TOIL balance display
const TOILBalance = ({ toil }) => {
if (!toil || toil.accrued === 0) {
return (
<div className="p-4 bg-white/5 rounded-lg text-center text-secondary-text">
No TOIL accrued
</div>
)
}

return (
<div className="card-glass p-4 space-y-4">
<div className="flex justify-between items-center">
<h4 className="font-medium">Time Off In Lieu</h4>
<span className="text-2xl font-bold">{toil.remaining} days</span>
</div>

<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-secondary-text">Total Accrued</span>
<div>{toil.accrued} days</div>
</div>
<div>
<span className="text-secondary-text">Used</span>
<div>{toil.used} days</div>
</div>
</div>

{toil.expiry_date && (
<div className="pt-3 border-t border-white/10 text-sm">
<span className="text-secondary-text">Use by: </span>
<span>{moment(toil.expiry_date).format('D MMMM YYYY')}</span>
</div>
)}
</div>
)
}

Year Comparison

Compare Years

// Year comparison component
const YearComparison = () => {
const currentYear = new Date().getFullYear()
const [selectedYear, setSelectedYear] = useState(currentYear)

const { data: balance } = useLeaveBalance({ year: selectedYear })

return (
<div className="space-y-4">
{/* Year selector */}
<div className="flex gap-2">
{[currentYear - 1, currentYear, currentYear + 1].map(year => (
<button
key={year}
onClick={() => setSelectedYear(year)}
className={`px-4 py-2 rounded-lg ${
selectedYear === year
? 'bg-coral-gradient text-white'
: 'bg-white/5 text-secondary-text'
}`}
>
{year}
</button>
))}
</div>

{/* Balance for selected year */}
<LeaveBalanceBreakdown breakdown={balance?.breakdown} />
</div>
)
}

Calendar View

Balance Calendar

See leave distribution across the year:

// Leave calendar display
const LeaveCalendar = ({ history }) => {
const months = useMemo(() => {
// Group leave by month
const grouped = {}
history.forEach(entry => {
const month = moment(entry.start_date).format('YYYY-MM')
if (!grouped[month]) grouped[month] = []
grouped[month].push(entry)
})
return grouped
}, [history])

return (
<div className="card-glass p-6">
<h3 className="font-semibold mb-4">Leave Calendar</h3>

<div className="grid grid-cols-4 gap-2">
{['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'].map((month, index) => {
const key = moment().year() + '-' + String(index + 1).padStart(2, '0')
const leaves = months[key] || []
const totalDays = leaves.reduce((sum, l) => sum + l.total_days, 0)

return (
<div
key={month}
className={`p-2 rounded text-center text-sm ${
totalDays > 0 ? 'bg-coral-500/20' : 'bg-white/5'
}`}
>
<div className="font-medium">{month}</div>
{totalDays > 0 && (
<div className="text-xs text-secondary-text">{totalDays}d</div>
)}
</div>
)
})}
</div>
</div>
)
}

Alerts and Reminders

Balance Alerts

// Balance alert messages
const BalanceAlerts = ({ balance, policy }) => {
const alerts = []

// Low balance warning
if (balance.remaining <= 5 && balance.remaining > 0) {
alerts.push({
type: 'warning',
message: `Only ${balance.remaining} days remaining this year`
})
}

// No balance left
if (balance.remaining <= 0) {
alerts.push({
type: 'error',
message: 'No annual leave remaining for this year'
})
}

// Carry over expiring soon
if (balance.carried_over > 0 && policy.carry_over_expiry) {
const daysUntilExpiry = moment(policy.carry_over_expiry).diff(moment(), 'days')
if (daysUntilExpiry <= 30 && daysUntilExpiry > 0) {
alerts.push({
type: 'warning',
message: `${balance.carried_over} carried-over days expire in ${daysUntilExpiry} days`
})
}
}

// Lots of unused leave near year end
const monthsLeft = 12 - moment().month()
if (monthsLeft <= 3 && balance.remaining > 10) {
alerts.push({
type: 'info',
message: `${balance.remaining} days remaining - consider booking time off`
})
}

if (alerts.length === 0) return null

return (
<div className="space-y-2">
{alerts.map((alert, index) => (
<div
key={index}
className={`p-3 rounded-lg ${
alert.type === 'error' ? 'bg-red-500/10 text-red-400' :
alert.type === 'warning' ? 'bg-yellow-500/10 text-yellow-400' :
'bg-blue-500/10 text-blue-400'
}`}
>
{alert.message}
</div>
))}
</div>
)
}

Mobile View

Mobile Balance Display

// Mobile balance view
const MobileLeaveBalance = () => {
const { data: balance, isLoading } = useLeaveBalance()

if (isLoading) return <MobileBalanceSkeleton />

return (
<div className="p-4 space-y-4">
{/* Main balance card */}
<div className="card-glass p-6 text-center">
<div className="text-5xl font-bold text-coral-gradient">
{balance.remaining}
</div>
<div className="text-secondary-text mt-1">days remaining</div>

{/* Quick stats */}
<div className="grid grid-cols-3 gap-4 mt-6 text-sm">
<div>
<div className="font-medium">{balance.entitlement}</div>
<div className="text-secondary-text">Total</div>
</div>
<div>
<div className="font-medium">{balance.used}</div>
<div className="text-secondary-text">Used</div>
</div>
<div>
<div className="font-medium">{balance.pending}</div>
<div className="text-secondary-text">Pending</div>
</div>
</div>
</div>

{/* Alerts */}
<BalanceAlerts balance={balance} policy={balance.policy} />

{/* Quick action */}
<button
onClick={() => router.push('/staff/leave/request')}
className="form-button w-full h-12"
>
Request Leave
</button>
</div>
)
}

Understanding Your Balance

Key Terms

TermMeaning
EntitlementTotal days you can take this year
UsedDays already taken (approved)
PendingDays in requests awaiting approval
RemainingEntitlement minus used minus pending
Carried OverDays brought from previous year
TOILExtra days earned from overtime

Balance Formula

Remaining = Entitlement + Carried Over + TOIL - Used - Pending

How Bank Holidays Affect Your Balance

Holiday-Aware Calculations

When you request leave, Shyfts automatically excludes bank holidays and company closures from the day count — just like weekends. This means you use fewer leave days when holidays fall within your requested dates.

Example

Requesting leave from Monday 21 December to Friday 25 December (Christmas week):

DateDayTypeCounted?
21 DecMondayWorking dayYes (1 day)
22 DecTuesdayWorking dayYes (1 day)
23 DecWednesdayWorking dayYes (1 day)
24 DecThursdayCompany ClosureNo
25 DecFridayChristmas Day (Bank Holiday)No

Result: Only 3 days deducted from your balance instead of 5.

What Counts as a Holiday?

Your company's holiday calendar includes:

TypeDescription
Bank HolidaysOfficial UK public holidays (e.g., Christmas Day, Easter Monday)
Company ClosuresDays your company is closed (e.g., annual shutdown)
Custom HolidaysOther company-specific dates (e.g., training days)

Viewing Your Company's Holidays

Holidays are visible on the scheduling calendar:

  • Day view: An amber badge shows the holiday name
  • Week view: Holiday names appear under each date with an amber tint

Impact on Your Balance

AspectHow Holidays Apply
Leave requestsHolidays within your dates are automatically excluded
Balance display"Days requested" reflects the holiday-adjusted count
Pending requestsHoliday-aware calculation applied at submission time
tip

When planning leave around Christmas or Easter, check the calendar for bank holidays first. You might need fewer leave days than you think!


Best Practices

Managing Your Balance

  1. Plan Ahead - Book leave early in the year
  2. Check Regularly - Monitor your balance
  3. Use Carry Over - Don't let carried days expire
  4. Spread Leave - Take breaks throughout the year
  5. Consider Team - Coordinate with colleagues

Balance Management Tips

DoDon't
Plan annual leave earlyLeave it all to December
Use carry-over firstLet carry-over expire
Take regular breaksSave all leave
Check balance before requestingAssume you have days left

Troubleshooting

Common Issues

IssueCauseSolution
Balance seems wrongCalculation pendingRefresh page
Missing entitlementContract not updatedContact HR
No TOIL showingNot accrued yetCheck with manager
Carry-over missingExpired or not appliedCheck policy dates

Error Messages

ErrorMeaningAction
"Balance not found"No entitlement setContact HR
"Year not available"Future year requestedSelect valid year
"Calculation error"System issueRetry or contact support


Source Files:

  • src/app/api/staff/leave-balance/route.ts - Leave balance API
  • src/components/staff/LeaveBalanceCard.tsx - Balance card component
  • src/lib/utils/leave.ts - Leave calculation utilities
  • src/types/leave.types.ts - Type definitions