196 lines
5.9 KiB
JavaScript
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}`
|
|
}
|