mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 07:26:30 +00:00
Comp/Flex days (#1074) — complete. A second leave type that does not deduct from the vacation entitlement (flextime / overtime offset). New kind column on vacay_entries, excluded from usedDays via a CASE sum; toggleEntry carries kind through service/nest/store with change-detection; desktop and mobile get a comp toolbar toggle next to the half-day one (orthogonal, so a half comp day is free); comp segments render as a diagonal hatch of the person colour, per-segment for split days (Version B). comp_used is surfaced separately in getStats. Translated into all 23 locales. Configurable vacation year (#737) — backend only. Per-user vacay_user_settings (calendar / fiscal / anniversary), resolveYearWindow, and usedDays / compUsedDays / getStats / carry-over now computed over the leave-year [start,end) window. 'calendar' resolves byte-identically to the old date-prefix behaviour, so nothing changes for existing users and the feature is inert until the settings UI lands. Frontend (settings selector + calendar window rollover), the settings endpoint, window-aware getEntries and holiday loading are still to do — see the PR description.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Building2, ChevronLeft, ChevronRight, Eye, Minus, PenLine, Pencil, Plus, Settings2, Share2, ShieldCheck, Trash2, Unlink, UserPlus } from 'lucide-react'
|
||||
import { Building2, ChevronLeft, ChevronRight, Clock, Eye, Minus, PenLine, Pencil, Plus, Settings2, Share2, ShieldCheck, Trash2, Unlink, UserPlus } from 'lucide-react'
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import MSheet from '../../components/MSheet'
|
||||
import MIconBtn from '../../components/MIconBtn'
|
||||
@@ -261,8 +261,20 @@ export default function MVacay() {
|
||||
{t('vacay.modeCompany')}
|
||||
</button>
|
||||
)}
|
||||
{/* Divider — the half-day switch modifies the selected person's logging, it isn't a mode. */}
|
||||
{/* Divider — comp/flex and half-day modify the selected person's logging, they aren't modes. */}
|
||||
<span className="mx-[1px] h-5 w-px self-center bg-[color:var(--m-shbr)]" aria-hidden />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => v.setCompDay(c => !c)}
|
||||
aria-pressed={v.compDay}
|
||||
aria-label={t('vacay.modeComp')}
|
||||
title={t('vacay.modeCompHint')}
|
||||
className={`flex h-9 w-9 flex-none items-center justify-center rounded-full ${
|
||||
v.compDay ? 'bg-m-act text-m-actfg' : 'bg-[color:var(--m-ic)] text-m-muted'
|
||||
}`}
|
||||
>
|
||||
<Clock size={16} strokeWidth={2.2} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => v.setHalfDay(h => !h)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { schoolHolidayBand } from '../../../components/Vacay/holidayVisual'
|
||||
import { dayVisual, localDateStr, monthLead, type DayVisualContext } from './vacayDayModel'
|
||||
import { dayVisual, hatchTint, localDateStr, monthLead, type DayVisualContext } from './vacayDayModel'
|
||||
|
||||
interface MVacayMonthProps {
|
||||
year: number
|
||||
@@ -45,7 +45,23 @@ export default function MVacayMonth({
|
||||
}`}
|
||||
style={{ background: visual.background, color: visual.numColor, boxShadow: visual.boxShadow }}
|
||||
>
|
||||
{day}
|
||||
{/* 2+ people: equal-width segment overlays, each solid (vacation) or hatched (comp) (#1074). */}
|
||||
{visual.segments && visual.segments.length > 1 && (
|
||||
<div className="absolute inset-0 overflow-hidden" style={{ borderRadius: 'inherit' }} aria-hidden>
|
||||
{visual.segments.map((seg, si) => (
|
||||
<div
|
||||
key={si}
|
||||
className="absolute top-0 h-full"
|
||||
style={{
|
||||
left: `${(si * 100) / visual.segments!.length}%`,
|
||||
width: `${100 / visual.segments!.length}%`,
|
||||
background: seg.comp ? hatchTint(seg.color) : seg.color,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<span className="relative z-[1]">{day}</span>
|
||||
{tripDates.has(dateStr) && (
|
||||
<span
|
||||
aria-hidden
|
||||
|
||||
@@ -42,6 +42,8 @@ export function useMVacay() {
|
||||
const [mode, setMode] = useState<MVacayMode>('vacation')
|
||||
// Half-day modifier: when on, taps log the selected person's day as 0.5 (#552).
|
||||
const [halfDay, setHalfDay] = useState(false)
|
||||
// Comp/Flex day modifier (#1074): when on, taps log the day as kind='comp' (free).
|
||||
const [compDay, setCompDay] = useState(false)
|
||||
const [sheet, setSheet] = useState<MVacaySheet>(null)
|
||||
const [tripDates, setTripDates] = useState<Set<string>>(new Set())
|
||||
|
||||
@@ -155,8 +157,8 @@ export function useMVacay() {
|
||||
}
|
||||
if (blockWeekends && isWeekend(dateStr, weekendDays)) return
|
||||
if (companyHolidaysEnabled && companyHolidaySet.has(dateStr)) return
|
||||
await toggleEntry(dateStr, selectedUserId || undefined, halfDay ? 0.5 : 1)
|
||||
}, [view, mode, halfDay, companyHolidaysEnabled, blockWeekends, weekendDays, companyHolidaySet, toggleEntry, toggleCompanyHoliday, selectedUserId])
|
||||
await toggleEntry(dateStr, selectedUserId || undefined, halfDay ? 0.5 : 1, compDay ? 'comp' : 'vacation')
|
||||
}, [view, mode, halfDay, compDay, companyHolidaysEnabled, blockWeekends, weekendDays, companyHolidaySet, toggleEntry, toggleCompanyHoliday, selectedUserId])
|
||||
|
||||
// Entitlement stepper: never below what is already used this year
|
||||
// (carried-over days cover the difference when used > entitlement).
|
||||
@@ -198,7 +200,7 @@ export function useMVacay() {
|
||||
users, isFused, currentUser,
|
||||
incomingInvites, acceptInvite, declineInvite,
|
||||
incomingShares, toggleShareHidden,
|
||||
view, month, mode, halfDay, setHalfDay, sheet, setSheet, setMode, setMonth,
|
||||
view, month, mode, halfDay, setHalfDay, compDay, setCompDay, sheet, setSheet, setMode, setMonth,
|
||||
tripDates, tripDotColor,
|
||||
blockWeekends, companyHolidaysEnabled, holidaysEnabled, weekStart, weekendDays,
|
||||
dayCtx, monthNamesShort, monthNameLong,
|
||||
|
||||
@@ -11,6 +11,10 @@ export interface DayVisual {
|
||||
boxShadow?: string
|
||||
// At least one person logged this day as a half day (#552) — the cell shows a ½ badge.
|
||||
half?: boolean
|
||||
// Per-person fill segments for this day (#1074): each carries the person's tint and
|
||||
// whether they logged a comp/flex day (hatched, kind='comp') vs. a vacation day
|
||||
// (solid). Set only when someone is off; the cell renders overlays for 2+ people.
|
||||
segments?: { color: string; comp: boolean }[]
|
||||
// Colours of the school-holiday calendars covering this day — drawn as a rounded
|
||||
// accent band under the number, on top of whatever fill the cell already has.
|
||||
school?: string[]
|
||||
@@ -72,6 +76,11 @@ export function holidayInk(color: string): string {
|
||||
return `hsl(${hsl.h} ${s}% 42%)`
|
||||
}
|
||||
|
||||
/** Diagonal hatch of a tint for comp/flex days (#1074), matching the desktop card. */
|
||||
export function hatchTint(color: string): string {
|
||||
return `repeating-linear-gradient(45deg, ${color} 0 2.5px, transparent 2.5px 5px)`
|
||||
}
|
||||
|
||||
/** Hard split background when several persons logged the same day. */
|
||||
export function splitBackground(colors: string[]): string {
|
||||
const n = colors.length
|
||||
@@ -104,11 +113,14 @@ function baseDayVisual(dateStr: string, dayOfWeek: number, ctx: DayVisualContext
|
||||
}
|
||||
const entries = ctx.entryMap[dateStr]
|
||||
if (entries && entries.length > 0) {
|
||||
const tints = entries.map(e => personTint(e.person_color || FALLBACK_PERSON_COLOR))
|
||||
// The fill still shows WHO is off; half days (#552) keep it and add a corner
|
||||
// dot, so a half day never looks like a two-person split. Only set `half` when
|
||||
// true so full days keep their exact { background, numColor } shape.
|
||||
const visual: DayVisual = { background: tints.length === 1 ? tints[0] : splitBackground(tints), numColor: '#101013' }
|
||||
// The fill still shows WHO is off; half days (#552) keep it and add a corner dot.
|
||||
// Comp/flex days (#1074) hatch their segment. 1 person fills the cell directly
|
||||
// (solid or hatched); several render segment overlays, so the background stays
|
||||
// transparent underneath them.
|
||||
const segments = entries.map(e => ({ color: personTint(e.person_color || FALLBACK_PERSON_COLOR), comp: e.kind === 'comp' }))
|
||||
const single = segments.length === 1
|
||||
const background = single ? (segments[0].comp ? hatchTint(segments[0].color) : segments[0].color) : 'transparent'
|
||||
const visual: DayVisual = { background, numColor: '#101013', segments }
|
||||
if (entries.some(e => (e.fraction ?? 1) === 0.5)) visual.half = true
|
||||
return withSchool(visual)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user