189 lines
5.0 KiB
TypeScript
189 lines
5.0 KiB
TypeScript
/**
|
|
* Frontend Date Filtering Utilities for D-1 Logic
|
|
* Mirrors backend dateUtils for client-side awareness
|
|
*/
|
|
|
|
const CUTOFF_HOUR = 17;
|
|
const WIB_TIME_ZONE = 'Asia/Jakarta';
|
|
|
|
type WibDateParts = {
|
|
year: number;
|
|
month: number;
|
|
day: number;
|
|
hour: number;
|
|
minute: number;
|
|
second: number;
|
|
};
|
|
|
|
function getWibDateParts(referenceDate?: Date): WibDateParts {
|
|
const now = referenceDate || new Date();
|
|
|
|
const formatter = new Intl.DateTimeFormat('en-CA', {
|
|
timeZone: WIB_TIME_ZONE,
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
hour12: false,
|
|
});
|
|
|
|
const parts = formatter.formatToParts(now);
|
|
const lookup = Object.fromEntries(
|
|
parts.filter((part) => part.type !== 'literal').map((part) => [part.type, part.value])
|
|
) as Record<string, string>;
|
|
|
|
return {
|
|
year: Number(lookup.year),
|
|
month: Number(lookup.month),
|
|
day: Number(lookup.day),
|
|
hour: Number(lookup.hour),
|
|
minute: Number(lookup.minute),
|
|
second: Number(lookup.second),
|
|
};
|
|
}
|
|
|
|
function createLocalDateFromWibParts(parts: WibDateParts): Date {
|
|
const date = new Date(
|
|
parts.year,
|
|
parts.month - 1,
|
|
parts.day,
|
|
parts.hour,
|
|
parts.minute,
|
|
parts.second
|
|
);
|
|
date.setMilliseconds(0);
|
|
return date;
|
|
}
|
|
|
|
/**
|
|
* Get current date/time in WIB (Asia/Jakarta, UTC+7) timezone
|
|
* @param referenceDate - Optional reference date for testing
|
|
* @returns Current date in WIB
|
|
*/
|
|
export function getCurrentWibDate(referenceDate?: Date): Date {
|
|
return createLocalDateFromWibParts(getWibDateParts(referenceDate));
|
|
}
|
|
|
|
/**
|
|
* Check if current WIB time is before 17:00
|
|
* @param referenceDate - Optional reference date for testing
|
|
* @returns True if before cutoff, false otherwise
|
|
*/
|
|
export function shouldUsePreviousDay(referenceDate?: Date): boolean {
|
|
return getWibDateParts(referenceDate).hour < CUTOFF_HOUR;
|
|
}
|
|
|
|
/**
|
|
* Get effective date for data queries (D-1 or today)
|
|
* @param referenceDate - Optional reference date for testing
|
|
* @returns Effective date for queries
|
|
*/
|
|
export function getEffectiveDate(referenceDate?: Date): Date {
|
|
const wibParts = getWibDateParts(referenceDate);
|
|
const wibToday = createLocalDateFromWibParts(wibParts);
|
|
|
|
if (shouldUsePreviousDay(referenceDate)) {
|
|
// Return D-1 (previous day)
|
|
const yesterday = new Date(wibToday);
|
|
yesterday.setDate(yesterday.getDate() - 1);
|
|
return yesterday;
|
|
}
|
|
|
|
return wibToday;
|
|
}
|
|
|
|
/**
|
|
* Calculate effective cycle day with D-1 adjustment
|
|
* @param cycleStartDate - Cycle start date
|
|
* @param referenceDate - Optional reference date for testing
|
|
* @returns Effective cycle day (0-indexed)
|
|
*/
|
|
export function getEffectiveCycleDay(cycleStartDate: Date | string, referenceDate?: Date): number {
|
|
const effectiveDate = getEffectiveDate(referenceDate);
|
|
const startDate = new Date(cycleStartDate);
|
|
|
|
// Reset hours to compare dates only
|
|
effectiveDate.setHours(0, 0, 0, 0);
|
|
startDate.setHours(0, 0, 0, 0);
|
|
|
|
const diffMs = effectiveDate.getTime() - startDate.getTime();
|
|
|
|
// Return -1 if effective date is before cycle start
|
|
if (diffMs < 0) {
|
|
return -1;
|
|
}
|
|
|
|
const dayOfCycle = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
return dayOfCycle;
|
|
}
|
|
|
|
/**
|
|
* Format date as YYYY-MM-DD
|
|
* @param date - Date object to format
|
|
* @returns Date in YYYY-MM-DD format
|
|
*/
|
|
export function formatDateForDb(date: Date): string {
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
return `${year}-${month}-${day}`;
|
|
}
|
|
|
|
/**
|
|
* Format effective date label with disclaimer if D-1 active
|
|
* @param date - Date to format
|
|
* @param referenceDate - Optional reference date for testing
|
|
* @returns Formatted date string with optional disclaimer
|
|
*/
|
|
export function formatEffectiveDateLabel(date: Date, referenceDate?: Date): string {
|
|
const monthNames = [
|
|
'Januari',
|
|
'Februari',
|
|
'Maret',
|
|
'April',
|
|
'Mei',
|
|
'Juni',
|
|
'Juli',
|
|
'Agustus',
|
|
'September',
|
|
'Oktober',
|
|
'November',
|
|
'Desember',
|
|
];
|
|
|
|
const day = date.getDate();
|
|
const month = monthNames[date.getMonth()];
|
|
const year = date.getFullYear();
|
|
const baseLabel = `${day} ${month} ${year}`;
|
|
|
|
if (shouldUsePreviousDay(referenceDate)) {
|
|
return `${baseLabel} (Data Hari Kemarin)`;
|
|
}
|
|
|
|
return baseLabel;
|
|
}
|
|
|
|
/**
|
|
* Get adjusted date range for API queries (applies D-1 filter if needed)
|
|
* @param startDate - Start date string (YYYY-MM-DD)
|
|
* @param endDate - End date string (YYYY-MM-DD)
|
|
* @param referenceDate - Optional reference date for testing
|
|
* @returns Adjusted start and end dates
|
|
*/
|
|
export function getAdjustedDateRange(
|
|
startDate: string,
|
|
endDate: string,
|
|
referenceDate?: Date
|
|
): { start: string; end: string } {
|
|
// Only adjust if querying a single day and before cutoff
|
|
if (startDate === endDate && shouldUsePreviousDay(referenceDate)) {
|
|
const adjusted = getEffectiveDate(referenceDate);
|
|
const adjustedStr = formatDateForDb(adjusted);
|
|
return { start: adjustedStr, end: adjustedStr };
|
|
}
|
|
|
|
return { start: startDate, end: endDate };
|
|
}
|