Files
accounted/extensions/restaurant/tip-tracking/lib/tip-calculator.ts
T
EmilandClaude Opus 4.6 6a5b2b7792 feat: overhaul all 12 sector extensions with full CRUD, validation, and tests
Add shared components (ConfirmDeleteDialog, EditEntryDialog, validation utils),
enhance all 12 extension workspaces with edit/delete dialogs, input validation,
period comparisons, and new analytics features. Fix critical bugs in
ProjectBilling margin calculation and EarningsPerLiter revenue allocation.
Add pure calculation modules with 183 new tests across all extensions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 15:21:49 +01:00

181 lines
4.5 KiB
TypeScript

/**
* Pure calculation functions for the Tip Tracking extension.
* Handles tip summaries, per-employee totals, pool distribution
* (equal, hours-based, custom percentage), and monthly trends.
*
* All monetary values use Math.round(x * 100) / 100 for precision.
*/
export interface TipEntry {
date: string
shift: string
employeeId: string
employeeName: string
amount: number
}
export interface TipSummary {
totalTips: number
avgPerShift: number
entryCount: number
}
export interface EmployeeTipSummary {
employeeId: string
name: string
total: number
count: number
average: number
}
export interface PoolDistribution {
employeeId: string
name: string
share: number
}
/**
* Calculate tip summary for a set of entries.
* Returns total tips, average per shift, and entry count.
*/
export function calculateTipSummary(entries: TipEntry[]): TipSummary {
if (entries.length === 0) {
return { totalTips: 0, avgPerShift: 0, entryCount: 0 }
}
const totalTips = entries.reduce((sum, e) => sum + e.amount, 0)
const roundedTotal = Math.round(totalTips * 100) / 100
const avgPerShift = Math.round((roundedTotal / entries.length) * 100) / 100
return {
totalTips: roundedTotal,
avgPerShift,
entryCount: entries.length,
}
}
/**
* Calculate per-employee totals from tip entries.
* Groups entries by employeeId and returns sorted by total descending.
*/
export function calculateEmployeeTotals(entries: TipEntry[]): EmployeeTipSummary[] {
const map = new Map<string, { name: string; total: number; count: number }>()
for (const entry of entries) {
const existing = map.get(entry.employeeId)
if (existing) {
existing.total += entry.amount
existing.count += 1
} else {
map.set(entry.employeeId, {
name: entry.employeeName,
total: entry.amount,
count: 1,
})
}
}
const results: EmployeeTipSummary[] = []
for (const [employeeId, data] of map) {
const total = Math.round(data.total * 100) / 100
const average = Math.round((total / data.count) * 100) / 100
results.push({
employeeId,
name: data.name,
total,
count: data.count,
average,
})
}
return results.sort((a, b) => b.total - a.total)
}
/**
* Calculate pool distribution using equal split.
* Each employee receives an equal share of the pool amount.
*/
export function calculateEqualSplit(
poolAmount: number,
employees: { id: string; name: string }[]
): PoolDistribution[] {
if (employees.length === 0) {
return []
}
const share = Math.round((poolAmount / employees.length) * 100) / 100
return employees.map(e => ({
employeeId: e.id,
name: e.name,
share,
}))
}
/**
* Calculate pool distribution by hours worked.
* Each employee's share is proportional to their hours.
* Employees with zero hours receive zero.
*/
export function calculateHoursSplit(
poolAmount: number,
employeeHours: { id: string; name: string; hours: number }[]
): PoolDistribution[] {
if (employeeHours.length === 0) {
return []
}
const totalHours = employeeHours.reduce((sum, e) => sum + e.hours, 0)
if (totalHours === 0) {
return employeeHours.map(e => ({
employeeId: e.id,
name: e.name,
share: 0,
}))
}
return employeeHours.map(e => ({
employeeId: e.id,
name: e.name,
share: Math.round((poolAmount * (e.hours / totalHours)) * 100) / 100,
}))
}
/**
* Calculate pool distribution by custom percentages.
* Each employee's share is determined by their assigned percentage.
* Percentages do not need to sum to 100 — each is applied independently.
*/
export function calculateCustomSplit(
poolAmount: number,
employeePcts: { id: string; name: string; pct: number }[]
): PoolDistribution[] {
return employeePcts.map(e => ({
employeeId: e.id,
name: e.name,
share: Math.round((poolAmount * (e.pct / 100)) * 100) / 100,
}))
}
/**
* Calculate monthly trend — total tips per month.
* Returns an array sorted by month (YYYY-MM) in ascending order.
*/
export function calculateMonthlyTipTrend(entries: TipEntry[]): { month: string; total: number }[] {
const map = new Map<string, number>()
for (const entry of entries) {
// Extract YYYY-MM from date string
const month = entry.date.substring(0, 7)
map.set(month, (map.get(month) ?? 0) + entry.amount)
}
const results: { month: string; total: number }[] = []
for (const [month, total] of map) {
results.push({ month, total: Math.round(total * 100) / 100 })
}
return results.sort((a, b) => a.month.localeCompare(b.month))
}