HR-ATS-Portal/frontend/src/lib/format.js

196 lines
5.9 KiB
JavaScript

/** Display formatting for identifiers stored snake_case in the DB. */
/** 'system_administrator' → 'System Administrator', 'INTERVIEW' → 'Interview'. */
export function formatRole(name) {
const s = String(name || '').trim()
if (!s) return s
return s
.split(/[_\s]+/)
.map((w) => (w ? w[0].toUpperCase() + w.slice(1).toLowerCase() : w))
.join(' ')
}
/* Platform date/time display. One spelling everywhere:
date → 3rd Sept 2026
time → 7:30pm (12-hour, lowercase, no space)
both → 3rd Sept 2026 - 7:30pm
Painted in Pakistan Standard Time (Asia/Karachi, UTC+5, no DST).
Naive stamps (no Z / offset) are already PKT digits.
UTC instants (…Z or +00:00) convert +5 to PKT. */
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec']
const WEEKDAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
const WEEKDAYS_SHORT = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
const API_TS = /^(\d{4})-(\d{2})-(\d{2})(?:[T\s](\d{1,2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?)?/
/** Parse an API timestamp into a Date whose local fields match the payload. */
export function toDate(value) {
if (value == null || value === '') return null
if (value instanceof Date) {
return Number.isNaN(value.getTime()) ? null : value
}
if (typeof value === 'number' && Number.isFinite(value)) {
const d = new Date(value)
return Number.isNaN(d.getTime()) ? null : d
}
const s = String(value).trim()
const m = s.match(API_TS)
if (!m) return null
const year = Number(m[1])
const month = Number(m[2])
const day = Number(m[3])
const hour = m[4] != null ? Number(m[4]) : 0
const minute = m[5] != null ? Number(m[5]) : 0
const second = m[6] != null ? Number(m[6]) : 0
if (month < 1 || month > 12 || day < 1 || day > 31) return null
const d = new Date(year, month - 1, day, hour, minute, second)
if (d.getFullYear() !== year || d.getMonth() !== month - 1 || d.getDate() !== day) return null
return d
}
const HAS_OFFSET = /[zZ]|[+-]\d{2}:?\d{2}$/
/** Recruiter clock. PKT has no DST — UTC+5 year-round. */
const DISPLAY_TZ = 'Asia/Karachi'
function pad2(n) {
return String(n).padStart(2, '0')
}
function pktParts(d) {
const out = {}
for (const p of new Intl.DateTimeFormat('en-GB', {
timeZone: DISPLAY_TZ,
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
hourCycle: 'h23',
}).formatToParts(d)) {
if (p.type !== 'literal') out[p.type] = Number(p.value)
}
return out
}
/**
* Parse a UTC instant (Graph receivedDateTime, timestamptz) into a Date
* Display uses Asia/Karachi (PKT, UTC+5), not the browser zone.
*/
export function toInstant(value) {
if (value == null || value === '') return null
if (value instanceof Date) {
return Number.isNaN(value.getTime()) ? null : value
}
if (typeof value === 'number' && Number.isFinite(value)) {
const d = new Date(value)
return Number.isNaN(d.getTime()) ? null : d
}
let s = String(value).trim()
if (!s) return null
if (/^\d{4}-\d{2}-\d{2}[ T]\d/.test(s) && !HAS_OFFSET.test(s)) {
s = `${s.replace(' ', 'T')}Z`
} else {
s = s.replace(' ', 'T')
}
const d = new Date(s)
return Number.isNaN(d.getTime()) ? null : d
}
function ordinal(n) {
const v = n % 100
if (v >= 11 && v <= 13) return `${n}th`
switch (n % 10) {
case 1: return `${n}st`
case 2: return `${n}nd`
case 3: return `${n}rd`
default: return `${n}th`
}
}
/** Date to paint. Naive ISO digits are PKT; Z / +00:00 are UTC → PKT. */
export function toDisplayDate(value) {
if (value instanceof Date) {
return Number.isNaN(value.getTime()) ? null : value
}
if (value == null || value === '') return null
const s = String(value).trim()
if (!s) return null
if (HAS_OFFSET.test(s)) return toInstant(s) || toDate(s)
const m = s.match(API_TS)
if (m) {
const hour = m[4] != null ? Number(m[4]) : 0
const minute = m[5] != null ? Number(m[5]) : 0
const second = m[6] != null ? Number(m[6]) : 0
const iso = `${m[1]}-${m[2]}-${m[3]}T${pad2(hour)}:${pad2(minute)}:${pad2(second)}+05:00`
const d = new Date(iso)
if (!Number.isNaN(d.getTime())) return d
}
return toDate(s) || toInstant(s)
}
/** 3rd Sept 2026 (PKT calendar) */
export function fmtDate(value) {
const d = toDisplayDate(value)
if (!d) return ''
const p = pktParts(d)
if (p.day == null) return ''
return `${ordinal(p.day)} ${MONTHS[p.month - 1]} ${p.year}`
}
/** Same as fmtDate — the platform uses one date spelling. */
export function fmtShort(value) {
return fmtDate(value)
}
/** 7:30pm */
export function fmtTime(value) {
const d = toDisplayDate(value)
if (!d) return ''
const p = pktParts(d)
if (p.hour == null) return ''
let hours = p.hour
const minutes = p.minute
const suffix = hours >= 12 ? 'pm' : 'am'
hours = hours % 12
if (hours === 0) hours = 12
return `${hours}:${pad2(minutes)}${suffix}`
}
/** 3rd Sept 2026 - 7:30pm */
export function fmtDateTime(value, sep = ' - ') {
const d = toDisplayDate(value)
if (!d) return ''
return `${fmtDate(d)}${sep}${fmtTime(d)}`
}
/** Sept 2026 — calendar month headers, not a day. */
export function fmtMonthYear(value) {
const d = toDate(value)
if (!d) return ''
return `${MONTHS[d.getMonth()]} ${d.getFullYear()}`
}
/** Thursday, 3rd Sept 2026 — dashboard "today" line. */
export function fmtWeekdayDate(value) {
const d = toDate(value)
if (!d) return ''
return `${WEEKDAYS[d.getDay()]}, ${fmtDate(d)}`
}
export function fmtWeekdayShort(value) {
const d = toDate(value)
if (!d) return ''
return WEEKDAYS_SHORT[d.getDay()]
}
/** yyyy-mm-dd for <input type="date">, from the payload's calendar day. */
export function toDateInput(value) {
const d = toDate(value)
if (!d) return ''
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day}`
}