mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-10-13 00:03:18 +01:00
add date range navigation, unify picker components
This commit is contained in:
135
resources/js/packages/ui/src/utils/dateRange.ts
Normal file
135
resources/js/packages/ui/src/utils/dateRange.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import type { Dayjs } from 'dayjs';
|
||||
import { formatDate, getDayJsInstance, getLocalizedDayJs, type DateFormat } from './time';
|
||||
|
||||
export type DateRangeDirection = -1 | 1;
|
||||
|
||||
function isSameDay(left: Dayjs, right: Dayjs): boolean {
|
||||
return left.isSame(right, 'day');
|
||||
}
|
||||
|
||||
function isRange(start: Dayjs, end: Dayjs, expectedStart: Dayjs, expectedEnd: Dayjs): boolean {
|
||||
return isSameDay(start, expectedStart) && isSameDay(end, expectedEnd);
|
||||
}
|
||||
|
||||
function formatCompactRange(start: Dayjs, end: Dayjs): string {
|
||||
if (start.year() !== end.year()) {
|
||||
return `${start.format('MMM D, YYYY')} – ${end.format('MMM D, YYYY')}`;
|
||||
}
|
||||
if (start.month() !== end.month()) {
|
||||
return `${start.format('MMM D')} – ${end.format('MMM D, YYYY')}`;
|
||||
}
|
||||
return `${start.format('MMM D')} – ${end.format('D, YYYY')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a human-friendly label for an inclusive date range. Relative labels
|
||||
* are intentionally shared by reporting, the timesheet, and the calendar.
|
||||
*/
|
||||
export function getDateRangeLabel(
|
||||
start: Dayjs,
|
||||
end: Dayjs,
|
||||
options: { dateFormat?: DateFormat; referenceDate?: Dayjs } = {}
|
||||
): string {
|
||||
// Ensure the configured first day of the week is applied before using startOf('week').
|
||||
getDayJsInstance();
|
||||
|
||||
const reference = (options.referenceDate ?? getLocalizedDayJs()).startOf('day');
|
||||
const rangeStart = start.startOf('day');
|
||||
const rangeEnd = end.startOf('day');
|
||||
|
||||
const relativeDays: Array<[number, string]> = [
|
||||
[0, 'Today'],
|
||||
[-1, 'Yesterday'],
|
||||
[1, 'Tomorrow'],
|
||||
];
|
||||
for (const [offset, label] of relativeDays) {
|
||||
const day = reference.add(offset, 'day');
|
||||
if (isRange(rangeStart, rangeEnd, day, day)) return label;
|
||||
}
|
||||
|
||||
const relativePeriods: Array<['week' | 'month' | 'year', number, string]> = [
|
||||
['week', 0, 'This Week'],
|
||||
['week', -1, 'Last Week'],
|
||||
['week', 1, 'Next Week'],
|
||||
['month', 0, 'This Month'],
|
||||
['month', -1, 'Last Month'],
|
||||
['month', 1, 'Next Month'],
|
||||
['year', 0, 'This Year'],
|
||||
['year', -1, 'Last Year'],
|
||||
['year', 1, 'Next Year'],
|
||||
];
|
||||
for (const [unit, offset, label] of relativePeriods) {
|
||||
const period = reference.add(offset, unit);
|
||||
if (isRange(rangeStart, rangeEnd, period.startOf(unit), period.endOf(unit))) return label;
|
||||
}
|
||||
|
||||
// Named calendar periods outside the relative windows above, e.g. "May 2025"
|
||||
// for a full month and "2025" for a full year.
|
||||
const monthStart = rangeStart.startOf('month');
|
||||
if (isRange(rangeStart, rangeEnd, monthStart, monthStart.endOf('month'))) {
|
||||
return rangeStart.format('MMMM YYYY');
|
||||
}
|
||||
const yearStart = rangeStart.startOf('year');
|
||||
if (isRange(rangeStart, rangeEnd, yearStart, yearStart.endOf('year'))) {
|
||||
return rangeStart.format('YYYY');
|
||||
}
|
||||
|
||||
const rollingRanges: Array<[number, string]> = [
|
||||
[14, 'Last 14 Days'],
|
||||
[30, 'Last 30 Days'],
|
||||
[90, 'Last 90 Days'],
|
||||
];
|
||||
for (const [days, label] of rollingRanges) {
|
||||
if (isRange(rangeStart, rangeEnd, reference.subtract(days - 1, 'day'), reference)) {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
if (isRange(rangeStart, rangeEnd, reference.subtract(12, 'month'), reference)) {
|
||||
return 'Last 12 Months';
|
||||
}
|
||||
|
||||
if (isSameDay(rangeStart, rangeEnd)) {
|
||||
return options.dateFormat
|
||||
? formatDate(rangeStart.format('YYYY-MM-DD'), options.dateFormat)
|
||||
: rangeStart.format('MMM D, YYYY');
|
||||
}
|
||||
|
||||
if (options.dateFormat) {
|
||||
return `${formatDate(rangeStart.format('YYYY-MM-DD'), options.dateFormat)} – ${formatDate(
|
||||
rangeEnd.format('YYYY-MM-DD'),
|
||||
options.dateFormat
|
||||
)}`;
|
||||
}
|
||||
return formatCompactRange(rangeStart, rangeEnd);
|
||||
}
|
||||
|
||||
/** Shift an inclusive range by one period, preserving full calendar periods. */
|
||||
export function shiftDateRange(
|
||||
start: Dayjs,
|
||||
end: Dayjs,
|
||||
direction: DateRangeDirection
|
||||
): { start: Dayjs; end: Dayjs } {
|
||||
getDayJsInstance();
|
||||
|
||||
const rangeStart = start.startOf('day');
|
||||
const rangeEnd = end.endOf('day');
|
||||
|
||||
if (isRange(rangeStart, rangeEnd, rangeStart.startOf('year'), rangeStart.endOf('year'))) {
|
||||
const shiftedStart = rangeStart.add(direction, 'year').startOf('year');
|
||||
return { start: shiftedStart, end: shiftedStart.endOf('year') };
|
||||
}
|
||||
if (isRange(rangeStart, rangeEnd, rangeStart.startOf('month'), rangeStart.endOf('month'))) {
|
||||
const shiftedStart = rangeStart.add(direction, 'month').startOf('month');
|
||||
return { start: shiftedStart, end: shiftedStart.endOf('month') };
|
||||
}
|
||||
if (isRange(rangeStart, rangeEnd, rangeStart.startOf('week'), rangeStart.endOf('week'))) {
|
||||
const shiftedStart = rangeStart.add(direction, 'week').startOf('week');
|
||||
return { start: shiftedStart, end: shiftedStart.endOf('week') };
|
||||
}
|
||||
|
||||
const days = rangeEnd.startOf('day').diff(rangeStart, 'day') + 1;
|
||||
return {
|
||||
start: rangeStart.add(days * direction, 'day').startOf('day'),
|
||||
end: rangeEnd.add(days * direction, 'day').endOf('day'),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user