feat: add Master Admin application functionality
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import { DateTimeFormat } from '../models/date-time-format';
|
||||
|
||||
export const DATE_TIME_FORMATS: Readonly<Record<DateTimeFormat, Intl.DateTimeFormatOptions>> =
|
||||
Object.freeze({
|
||||
[DateTimeFormat.Date]: Object.freeze({
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
}),
|
||||
[DateTimeFormat.ShortDate]: Object.freeze({
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric'
|
||||
}),
|
||||
[DateTimeFormat.LongDate]: Object.freeze({
|
||||
weekday: 'long',
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
}),
|
||||
[DateTimeFormat.DateTime]: Object.freeze({
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
}),
|
||||
[DateTimeFormat.ShortDateTime]: Object.freeze({
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
}),
|
||||
[DateTimeFormat.LongDateTime]: Object.freeze({
|
||||
weekday: 'long',
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: true
|
||||
}),
|
||||
[DateTimeFormat.Time]: Object.freeze({
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
}),
|
||||
[DateTimeFormat.TimeWithSeconds]: Object.freeze({
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: true
|
||||
}),
|
||||
[DateTimeFormat.MonthYear]: Object.freeze({
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
}),
|
||||
[DateTimeFormat.Year]: Object.freeze({
|
||||
year: 'numeric'
|
||||
})
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
export enum DateTimeFormat {
|
||||
Date = 'date',
|
||||
ShortDate = 'shortDate',
|
||||
LongDate = 'longDate',
|
||||
DateTime = 'dateTime',
|
||||
ShortDateTime = 'shortDateTime',
|
||||
LongDateTime = 'longDateTime',
|
||||
Time = 'time',
|
||||
TimeWithSeconds = 'timeWithSeconds',
|
||||
MonthYear = 'monthYear',
|
||||
Year = 'year'
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { DateTimeFormat } from '../models/date-time-format';
|
||||
import { DateTimeFormatterService } from '../services/date-time-formatter.service';
|
||||
import { AppDateTimePipe } from './app-date-time.pipe';
|
||||
|
||||
describe('AppDateTimePipe', () => {
|
||||
let formatter: DateTimeFormatterService;
|
||||
let pipe: AppDateTimePipe;
|
||||
|
||||
beforeEach(() => {
|
||||
formatter = new DateTimeFormatterService();
|
||||
pipe = new AppDateTimePipe(formatter);
|
||||
});
|
||||
|
||||
it('matches the formatter service output', () => {
|
||||
const value = '2026-07-20T14:30:45.000Z';
|
||||
const expected = formatter.format(value, {
|
||||
format: DateTimeFormat.LongDateTime,
|
||||
timeZone: 'Asia/Dubai',
|
||||
locale: 'en-GB',
|
||||
fallback: 'N/A'
|
||||
});
|
||||
|
||||
expect(pipe.transform(
|
||||
value,
|
||||
DateTimeFormat.LongDateTime,
|
||||
'Asia/Dubai',
|
||||
'en-GB',
|
||||
'N/A'
|
||||
)).toBe(expected);
|
||||
});
|
||||
|
||||
it('passes a custom fallback through to the formatter', () => {
|
||||
expect(pipe.transform(null, DateTimeFormat.Date, 'UTC', 'en-GB', 'N/A')).toBe('N/A');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
import { DateTimeFormat } from '../models/date-time-format';
|
||||
import { DateTimeFormatterService } from '../services/date-time-formatter.service';
|
||||
|
||||
@Pipe({
|
||||
name: 'appDateTime',
|
||||
standalone: true,
|
||||
pure: true
|
||||
})
|
||||
export class AppDateTimePipe implements PipeTransform {
|
||||
constructor(private readonly dateTimeFormatter: DateTimeFormatterService) {}
|
||||
|
||||
transform(
|
||||
value: string | number | Date | null | undefined,
|
||||
format?: DateTimeFormat,
|
||||
timeZone?: string,
|
||||
locale?: string,
|
||||
fallback?: string
|
||||
): string {
|
||||
return this.dateTimeFormatter.format(value, {
|
||||
format,
|
||||
timeZone,
|
||||
locale,
|
||||
fallback
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { DateTimeFormat } from '../models/date-time-format';
|
||||
import { DateTimeFormatterService } from './date-time-formatter.service';
|
||||
|
||||
describe('DateTimeFormatterService', () => {
|
||||
const isoDate = '2026-07-20T14:30:45.000Z';
|
||||
const utcOptions = { timeZone: 'UTC' } as const;
|
||||
let service: DateTimeFormatterService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new DateTimeFormatterService();
|
||||
});
|
||||
|
||||
it('formats an ISO UTC string using the default format', () => {
|
||||
expect(service.format(isoDate, utcOptions)).toBe('20 Jul 2026, 02:30 pm');
|
||||
});
|
||||
|
||||
it('formats a JavaScript Date', () => {
|
||||
expect(service.format(new Date(isoDate), {
|
||||
format: DateTimeFormat.Date,
|
||||
timeZone: 'UTC'
|
||||
})).toBe('20 Jul 2026');
|
||||
});
|
||||
|
||||
it('formats a numeric timestamp', () => {
|
||||
expect(service.format(Date.parse(isoDate), {
|
||||
format: DateTimeFormat.TimeWithSeconds,
|
||||
timeZone: 'UTC'
|
||||
})).toBe('02:30:45 pm');
|
||||
});
|
||||
|
||||
it.each([
|
||||
[DateTimeFormat.Date, '20 Jul 2026'],
|
||||
[DateTimeFormat.ShortDate, '20/07/2026'],
|
||||
[DateTimeFormat.LongDate, 'Monday, 20 July 2026'],
|
||||
[DateTimeFormat.DateTime, '20 Jul 2026, 02:30 pm'],
|
||||
[DateTimeFormat.ShortDateTime, '20/07/2026, 02:30 pm'],
|
||||
[DateTimeFormat.LongDateTime, 'Monday, 20 July 2026 at 02:30:45 pm'],
|
||||
[DateTimeFormat.Time, '02:30 pm'],
|
||||
[DateTimeFormat.TimeWithSeconds, '02:30:45 pm'],
|
||||
[DateTimeFormat.MonthYear, 'July 2026'],
|
||||
[DateTimeFormat.Year, '2026']
|
||||
])('formats %s using its predefined options', (format, expected) => {
|
||||
expect(service.format(isoDate, { format, timeZone: 'UTC' })).toBe(expected);
|
||||
});
|
||||
|
||||
it.each([null, undefined, '', ' ', 'not-a-date'])(
|
||||
'returns the default fallback for %s',
|
||||
value => {
|
||||
expect(service.format(value)).toBe('—');
|
||||
}
|
||||
);
|
||||
|
||||
it('uses a custom fallback', () => {
|
||||
expect(service.format('invalid', { fallback: 'N/A' })).toBe('N/A');
|
||||
});
|
||||
|
||||
it('supports a custom locale', () => {
|
||||
expect(service.format(isoDate, {
|
||||
format: DateTimeFormat.ShortDate,
|
||||
locale: 'de-DE',
|
||||
timeZone: 'UTC'
|
||||
})).toBe('20.07.2026');
|
||||
});
|
||||
|
||||
it('supports a valid IANA timezone', () => {
|
||||
expect(service.format(isoDate, {
|
||||
format: DateTimeFormat.Time,
|
||||
timeZone: 'Asia/Dubai'
|
||||
})).toBe('06:30 pm');
|
||||
});
|
||||
|
||||
it('handles a day change caused by timezone conversion', () => {
|
||||
expect(service.format('2026-07-20T23:30:00.000Z', {
|
||||
format: DateTimeFormat.Date,
|
||||
timeZone: 'Asia/Dubai'
|
||||
})).toBe('21 Jul 2026');
|
||||
});
|
||||
|
||||
it('returns the fallback instead of throwing for an invalid timezone', () => {
|
||||
expect(service.format(isoDate, { timeZone: 'Invalid/Timezone' })).toBe('—');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { DATE_TIME_FORMATS } from '../constants/date-time-formats';
|
||||
import { DateTimeFormat } from '../models/date-time-format';
|
||||
|
||||
export interface DateTimeFormattingOptions {
|
||||
readonly format?: DateTimeFormat;
|
||||
readonly locale?: string;
|
||||
readonly timeZone?: string;
|
||||
readonly fallback?: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DateTimeFormatterService {
|
||||
format(
|
||||
value: string | number | Date | null | undefined,
|
||||
options: DateTimeFormattingOptions = {}
|
||||
): string {
|
||||
const fallback = options.fallback ?? '—';
|
||||
|
||||
if (value === null || value === undefined || (typeof value === 'string' && !value.trim())) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const date = value instanceof Date
|
||||
? new Date(value.getTime())
|
||||
: new Date(value);
|
||||
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const format = options.format ?? DateTimeFormat.DateTime;
|
||||
const locale = options.locale?.trim() || 'en-GB';
|
||||
const timeZone = options.timeZone?.trim();
|
||||
|
||||
try {
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
...DATE_TIME_FORMATS[format],
|
||||
...(timeZone ? { timeZone } : {})
|
||||
}).format(date);
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user