diff --git a/src/app/recoveryIncidents/RecoveryIncidentsApi.ts b/src/app/recoveryIncidents/RecoveryIncidentsApi.ts index 27f2452..9f064ee 100644 --- a/src/app/recoveryIncidents/RecoveryIncidentsApi.ts +++ b/src/app/recoveryIncidents/RecoveryIncidentsApi.ts @@ -1,10 +1,54 @@ import { ApiClient } from '../api/ApiClient'; -import type { RecoveryIncident } from './RecoveryIncidentsTypes'; +import type { RecoveryIncident, MetricCardData } from './RecoveryIncidentsTypes'; + + export function getRecoveryIncidents(): Promise { return ApiClient.get('/recovery-incidents'); } +export function getRecoveryMetrics(): Promise { + return ApiClient.get('/recovery-incidents/metrics') + .catch(() => [ + { + id: 'total-recoveries', + title: "Total Recoveries", + value: "1,284", + trendValue: "40%", + trendText: "since last week", + trendType: "positive", + sparklineColor: "green", + }, + { + id: 'pending-approval', + title: "Pending Approval", + value: "274", + trendValue: "High Priority", + trendText: "since last week", + trendType: "positive", + sparklineColor: "green", + }, + { + id: 'refund-value', + title: "Refund Value", + value: "$412k", + trendValue: "MTD", + trendText: "since last week", + trendType: "negative", + sparklineColor: "red", + }, + { + id: 'customer-satisfaction', + title: "Customer Satisfaction", + value: "94%", + trendValue: "+2.1%", + trendText: "since last week", + trendType: "positive", + sparklineColor: "green", + }, + ]); +} + export function getRecoveryIncident(id: string): Promise { return ApiClient.get(`/recovery-incidents/${id}`); } diff --git a/src/app/recoveryIncidents/RecoveryIncidentsTypes.ts b/src/app/recoveryIncidents/RecoveryIncidentsTypes.ts index 492f526..22b6d4d 100644 --- a/src/app/recoveryIncidents/RecoveryIncidentsTypes.ts +++ b/src/app/recoveryIncidents/RecoveryIncidentsTypes.ts @@ -12,8 +12,20 @@ export interface RecoveryIncident { flightNumber: string; flightRoute: string; category?: string; - statuses: IncidentStatus[]; + statuses?: IncidentStatus[]; + status?: string; value: string; isPerksClaimed?: boolean; isGroupHeader?: boolean; } + +export interface MetricCardData { + id?: string; + title: string; + value?: string; + trendText: string; + trendValue: string; + trendType: "positive" | "negative" | "neutral"; + sparklineColor: "green" | "red"; +} + diff --git a/src/app/recoveryIncidents/components/AddRecoveryIncidents.tsx b/src/app/recoveryIncidents/components/AddRecoveryIncidents.tsx index 6466242..5858fa1 100644 --- a/src/app/recoveryIncidents/components/AddRecoveryIncidents.tsx +++ b/src/app/recoveryIncidents/components/AddRecoveryIncidents.tsx @@ -7,6 +7,7 @@ import { CustomCheckBox, } from "../../../components/custom"; import { createRecoveryIncident, updateRecoveryIncident } from '../RecoveryIncidentsApi'; +import { getMembershipTiers, getCategoryValues } from '../../configuration/masterData/MasterDataApi'; import type { RecoveryIncident } from '../RecoveryIncidentsTypes'; interface AddRecoveryIncidentsProps { @@ -19,6 +20,9 @@ const SECTION_TITLE_CLASS = "flex items-center gap-2 mb-4 text-[#4A5568] font-bo const SECTION_CONTAINER_CLASS = "bg-[#F9FAFB] rounded-[14px] p-5 border border-gray-100"; export default function AddRecoveryIncidents({ isOpen, onClose, incident }: AddRecoveryIncidentsProps) { + const [loyaltyTierOptions, setLoyaltyTierOptions] = useState<{ label: string; value: string }[]>([]); + const [jurisdictionOptions, setJurisdictionOptions] = useState<{ label: string; value: string }[]>([]); + const [scenarioOptions, setScenarioOptions] = useState<{ label: string; value: string }[]>([]); const [formData, setFormData] = useState({ passengerName: "", pnr: "", @@ -34,13 +38,70 @@ export default function AddRecoveryIncidents({ isOpen, onClose, incident }: AddR isPerksClaimed: false, }); + useEffect(() => { + if (isOpen) { + // 1. Fetch Loyalty Tier Master Data + getMembershipTiers() + .then((items) => { + if (Array.isArray(items) && items.length > 0) { + const activeOptions = items + .filter((m) => m.isActive !== false) + .map((m) => ({ + label: m.label || m.value, + value: m.value || m.id || m.label, + })); + if (activeOptions.length > 0) { + setLoyaltyTierOptions(activeOptions); + } + } + }) + .catch((err) => { + console.error("Failed to fetch loyalty tier master data:", err); + }); + + // 2. Fetch Jurisdiction Master Data + getCategoryValues('jurisdiction') + .then((items) => { + if (Array.isArray(items) && items.length > 0) { + const activeOptions = items + .filter((m) => m.isActive !== false) + .map((m) => ({ + label: m.label || m.name || m.value, + value: m.value || m.code || m.id || m.label, + })); + if (activeOptions.length > 0) { + setJurisdictionOptions(activeOptions); + } + } + }) + .catch(() => { }); + + // 3. Fetch Scenario Master Data + getCategoryValues('flight-disruption-type') + .then((items) => { + if (Array.isArray(items) && items.length > 0) { + const activeOptions = items + .filter((m) => m.isActive !== false) + .map((m) => ({ + label: m.label || m.name || m.value, + value: m.value || m.code || m.id || m.label, + })); + if (activeOptions.length > 0) { + setScenarioOptions(activeOptions); + } + } + }) + .catch(() => { }); + } + }, [isOpen]); + useEffect(() => { if (incident && isOpen) { const [origin, destination] = incident.flightRoute ? incident.flightRoute.split(' → ') : ["", ""]; setFormData({ passengerName: incident.passengerName || "", pnr: incident.pnr || "", - loyaltyTier: "", + loyaltyTier: (incident as any).loyaltyTier || "", flightNumber: incident.flightNumber || "", date: incident.date ? incident.date.split('T')[0] : "", origin: origin?.trim() || "", @@ -83,18 +144,18 @@ export default function AddRecoveryIncidents({ isOpen, onClose, incident }: AddR setLoading(true); try { const payload = { - recoveryCode: incident ? incident.recoveryCode : "REC-" + Math.floor(Math.random() * 10000), + recoveryCode: incident ? incident.recoveryCode : "REC-" + Math.floor(Math.random() * 10000), passengerName: formData.passengerName || "Unknown", pnr: formData.pnr || "N/A", flightNumber: formData.flightNumber || "TBD", flightRoute: `${formData.origin || 'UNK'} → ${formData.destination || 'UNK'}`, date: formData.date ? new Date(formData.date).toISOString() : new Date().toISOString(), category: formData.category || "General", - statuses: incident ? incident.statuses : [{ text: "New", variant: "info" as const }], + status: incident ? (incident.status || "Pending") : "Pending", value: incident ? incident.value : "$0", isPerksClaimed: formData.isPerksClaimed, }; - + console.log("Submitting payload:", payload); if (incident) { await updateRecoveryIncident(incident.id, payload); @@ -150,14 +211,10 @@ export default function AddRecoveryIncidents({ isOpen, onClose, incident }: AddR /> handleInputChange("loyaltyTier", val as string)} - options={[ - { label: "Gold", value: "gold" }, - { label: "Silver", value: "silver" }, - { label: "Bronze", value: "bronze" }, - ]} + options={loyaltyTierOptions} /> @@ -235,21 +292,14 @@ export default function AddRecoveryIncidents({ isOpen, onClose, incident }: AddR placeholder="Selected Option" value={formData.scenario} onChange={(val) => handleInputChange("scenario", val as string)} - options={[ - { label: "Delay", value: "delay" }, - { label: "Cancellation", value: "cancellation" }, - { label: "Denied Boarding", value: "denied_boarding" }, - ]} + options={scenarioOptions} /> handleInputChange("jurisdiction", val as string)} - options={[ - { label: "EU261", value: "eu261" }, - { label: "US DOT", value: "us_dot" }, - ]} + options={jurisdictionOptions} /> +
+ {title} +
+
+
+ {value && ( + + {value} + + )} +
+ + {trendValue} + + + {trendText} + +
+
+ + {/* Simple SVG Sparkline placeholder based on color */} +
+ {sparklineColor === "green" ? ( + + + + ) : ( + + + + )} +
+
+ + ); +} + +export default MetricCard; diff --git a/src/app/recoveryIncidents/components/RecoveryIncidentsList.tsx b/src/app/recoveryIncidents/components/RecoveryIncidentsList.tsx index 133b4ed..40e3d1d 100644 --- a/src/app/recoveryIncidents/components/RecoveryIncidentsList.tsx +++ b/src/app/recoveryIncidents/components/RecoveryIncidentsList.tsx @@ -1,17 +1,17 @@ import { useState, useEffect, useCallback, useMemo } from "react"; import { useNavigate } from "react-router-dom"; import { - Plus, - MagnifyingGlass, - CaretUp, - CaretDown, - FadersHorizontal, - SquaresFour, - Eye, - PencilSimple, - CheckCircle, - XCircle, - Clock, + EyeIcon, + PencilSimpleIcon, + CheckCircleIcon, + XCircleIcon, + ClockIcon, + FunnelSimpleIcon, + SquaresFourIcon, + PlusIcon, + CaretDownIcon, + CaretUpIcon, + MagnifyingGlassIcon, } from "@phosphor-icons/react"; import { CustomTable, @@ -24,9 +24,11 @@ import { CustomActionItem, } from "../../../components/custom"; import type { Column } from "../../../components/custom/CustomTable"; -import type { RecoveryIncident } from "../RecoveryIncidentsTypes"; +import type { RecoveryIncident, MetricCardData } from "../RecoveryIncidentsTypes"; import AddRecoveryIncidents from "./AddRecoveryIncidents"; -import { getRecoveryIncidents } from "../RecoveryIncidentsApi"; +import { MetricCard } from "./MetricCard"; +import { getRecoveryIncidents, getRecoveryMetrics } from "../RecoveryIncidentsApi"; +import { formatDate } from "../../../utils/formatDate"; const PAGE_SIZE = 10; @@ -71,94 +73,14 @@ function BadgeLabel({ text }: { text: string }) { ); } -// ─── Metric Card Component ─────────────────────────────────────────────────── - -interface MetricCardProps { - title: string; - value?: string; - trendText: string; - trendValue: string; - trendType: "positive" | "negative" | "neutral"; - sparklineColor: "green" | "red"; -} - -function MetricCard({ - title, - value, - trendText, - trendValue, - trendType, - sparklineColor, -}: MetricCardProps) { - return ( -
-
- {title} -
-
-
- {value && ( - - {value} - - )} -
- - {trendValue} - - - {trendText} - -
-
- - {/* Simple SVG Sparkline placeholder based on color */} -
- {sparklineColor === "green" ? ( - - - - ) : ( - - - - )} -
-
-
- ); +function getStatusVariant(status?: string): "success" | "error" | "warning" | "info" | "neutral" | "brand" { + if (!status) return "neutral"; + const s = status.toLowerCase(); + if (s.includes("appr") || s.includes("active") || s.includes("success")) return "success"; + if (s.includes("reject") || s.includes("denied") || s.includes("error")) return "error"; + if (s.includes("pend") || s.includes("review") || s.includes("warn")) return "warning"; + if (s.includes("new") || s.includes("info")) return "info"; + return "neutral"; } // ─── Component ─────────────────────────────────────────────────────────────── @@ -166,12 +88,17 @@ function MetricCard({ export default function RecoveryIncidentsList() { const navigate = useNavigate(); const [incidents, setIncidents] = useState([]); + const [metrics, setMetrics] = useState([]); const [loading, setLoading] = useState(true); + useEffect(() => { + getRecoveryMetrics() + .then((data) => setMetrics(data)) + .catch((err) => console.error("Failed to fetch recovery metrics:", err)); + }, []); + // Pagination const [currentPage, setCurrentPage] = useState(1); - const [totalItems, setTotalItems] = useState(0); - const [totalPages, setTotalPages] = useState(1); // Filters const [search, setSearch] = useState(""); @@ -185,41 +112,29 @@ export default function RecoveryIncidentsList() { // ─── Fetch data ───────────────────────────────────────────── - const fetchIncidents = useCallback( - async (page: number) => { - setLoading(true); - - try { - const data = await getRecoveryIncidents(); - - const filteredData = data.filter((p) => { - const matchesSearch = p.recoveryCode.toLowerCase().includes(search.toLowerCase()) || - p.flightNumber.toLowerCase().includes(search.toLowerCase()); - const matchesGroup = isGrouped ? true : !p.isGroupHeader; - return matchesSearch && matchesGroup; - }); - - const total = filteredData.length; - const pages = Math.ceil(total / PAGE_SIZE); - const start = (page - 1) * PAGE_SIZE; - const paginatedData = filteredData.slice(start, start + PAGE_SIZE); - - setIncidents(paginatedData); - setTotalItems(total); - setTotalPages(pages || 1); - } catch (error) { - console.error("Failed to fetch recovery incidents", error); - setIncidents([]); - } finally { - setLoading(false); - } - }, - [search, isGrouped], - ); + const fetchIncidents = useCallback(async () => { + setLoading(true); + try { + const data = await getRecoveryIncidents(); + setIncidents(data); + } catch (error) { + console.error("Failed to fetch recovery incidents", error); + setIncidents([]); + } finally { + setLoading(false); + } + }, []); useEffect(() => { - fetchIncidents(currentPage); - }, [currentPage, search, isGrouped, fetchIncidents]); + fetchIncidents(); + }, [fetchIncidents]); + + useEffect(() => { + if (incidents.length > 0) { + const keys = new Set(incidents.map((i) => i.flightNumber || "Other")); + setCollapsedGroups(keys); + } + }, [incidents]); // ─── Handlers ────────────────────────────────────────────────────────────── @@ -250,40 +165,123 @@ export default function RecoveryIncidentsList() { setSelectedIds(newSelected); }; - const toggleGroup = (id: string, e: React.MouseEvent) => { - e.stopPropagation(); + const toggleGroup = (groupKey: string, e?: React.MouseEvent) => { + if (e) e.stopPropagation(); setCollapsedGroups((prev) => { const next = new Set(prev); - if (next.has(id)) { - next.delete(id); + if (next.has(groupKey)) { + next.delete(groupKey); } else { - next.add(id); + next.add(groupKey); } return next; }); }; - const handleStatusChange = async (incident: RecoveryIncident, text: string, variant: "success" | "error" | "warning" | "info" | "neutral" | "brand") => { + const handleStatusChange = async (incident: RecoveryIncident, text: string) => { try { const { updateRecoveryIncident } = await import('../RecoveryIncidentsApi'); await updateRecoveryIncident(incident.id, { - statuses: [{ text, variant }] + status: text }); - fetchIncidents(currentPage); + fetchIncidents(); } catch (error) { console.error("Failed to update status", error); } }; + const filteredIncidents = useMemo(() => { + return incidents.filter((p) => { + const q = search.toLowerCase(); + return ( + (p.recoveryCode || "").toLowerCase().includes(q) || + (p.flightNumber || "").toLowerCase().includes(q) || + (p.passengerName || "").toLowerCase().includes(q) || + (p.pnr || "").toLowerCase().includes(q) + ); + }); + }, [incidents, search]); + + const displayData = useMemo(() => { + if (!isGrouped) { + return filteredIncidents; + } + + // Group items by flight number + const groupMap = new Map(); + filteredIncidents.forEach((item) => { + const key = item.flightNumber || "Other"; + if (!groupMap.has(key)) { + groupMap.set(key, []); + } + groupMap.get(key)!.push(item); + }); + + const result: (RecoveryIncident & { groupKey?: string })[] = []; + + groupMap.forEach((groupItems, flightNo) => { + const firstItem = groupItems[0]; + + // Calculate status summary for group header + const pendingCount = groupItems.filter(i => { + const s = (i.status || "").toLowerCase(); + return s.includes("pending") || s.includes("review") || s.includes("new"); + }).length; + const approvedCount = groupItems.filter(i => { + const s = (i.status || "").toLowerCase(); + return s.includes("approved") || s.includes("active"); + }).length; + + const headerStatuses = [ + { text: `${pendingCount} Pending`, variant: "warning" as const }, + { text: `${approvedCount} Approved`, variant: "success" as const }, + ]; + + // Calculate sum of values + const sumVal = groupItems.reduce((acc, curr) => { + const num = parseFloat((curr.value || "").replace(/[^0-9.]/g, "")) || 0; + return acc + num; + }, 0); + + const groupKey = flightNo; + + // Group Header Row + result.push({ + id: `header-${groupKey}`, + recoveryCode: firstItem.recoveryCode, + date: firstItem.date, + flightNumber: flightNo, + flightRoute: firstItem.flightRoute, + category: firstItem.category, + statuses: headerStatuses, + value: `$${sumVal}`, + isGroupHeader: true, + groupKey: groupKey, + }); + + // Child Rows (if group is expanded) + if (!collapsedGroups.has(groupKey)) { + groupItems.forEach((child) => { + result.push({ + ...child, + groupKey: groupKey, + }); + }); + } + }); + + return result; + }, [filteredIncidents, isGrouped, collapsedGroups]); + + const totalItems = displayData.length; + const totalPages = Math.ceil(totalItems / PAGE_SIZE) || 1; const startIndex = totalItems > 0 ? (currentPage - 1) * PAGE_SIZE + 1 : 0; const endIndex = Math.min(currentPage * PAGE_SIZE, totalItems); - const displayData = useMemo(() => { - if (!isGrouped) return incidents; - return incidents.filter( - (p) => p.isGroupHeader || !collapsedGroups.has(p.recoveryCode) - ); - }, [incidents, isGrouped, collapsedGroups]); + const paginatedDisplayData = useMemo(() => { + const start = (currentPage - 1) * PAGE_SIZE; + return displayData.slice(start, start + PAGE_SIZE); + }, [displayData, currentPage]); // ─── Table columns ───────────────────────────────────────────────────────── @@ -311,7 +309,7 @@ export default function RecoveryIncidentsList() { accessor: (row) => (
- +
), }, @@ -319,7 +317,7 @@ export default function RecoveryIncidentsList() { header: ( } + rightIcon={} /> ), accessor: (row) => @@ -334,7 +332,7 @@ export default function RecoveryIncidentsList() { header: ( } + rightIcon={} /> ), accessor: (row) => ( @@ -351,23 +349,34 @@ export default function RecoveryIncidentsList() { }, { header: , - accessor: (row) => ( -
- {row.statuses.map((status, idx) => ( - - ))} -
- ), + accessor: (row) => { + if (row.isGroupHeader) { + return ( +
+ {(row.statuses || []).map((status, idx) => ( + + ))} +
+ ); + } + const statusText = row.status || "Pending"; + return ( + + ); + }, }, { header: ( } + rightIcon={} /> ), accessor: (row) => , @@ -382,62 +391,90 @@ export default function RecoveryIncidentsList() { { header: , className: "text-right", - accessor: (row) => ( -
- {row.isGroupHeader ? ( -
toggleGroup(row.recoveryCode, e)} - > - {collapsedGroups.has(row.recoveryCode) ? ( - - ) : ( - - )} -
- ) : ( - - navigate(`/recovery/${row.id}`)} - icon={} + accessor: (row) => { + const key = (row as any).groupKey || row.flightNumber || row.recoveryCode; + return ( +
+ {row.isGroupHeader ? ( +
toggleGroup(key, e)} > - View Details - - { - setEditingIncident(row); - setIsModalOpen(true); - }} - icon={} - > - Edit Incident - - } - onClick={() => handleStatusChange(row, "Approved", "success")} - > - Approve - - } - onClick={() => handleStatusChange(row, "Rejected", "error")} - > - Reject - - } - onClick={() => handleStatusChange(row, "Under Review", "warning")} - > - Mark for Review - - - )} -
- ), + {collapsedGroups.has(key) ? ( + + ) : ( + + )} +
+ ) : ( + + navigate(`/recovery/${row.id}`)} + icon={} + > + View Details + + { + setEditingIncident(row); + setIsModalOpen(true); + }} + icon={} + > + Edit Incident + + } + onClick={() => handleStatusChange(row, "Approved")} + > + Approve + + } + onClick={() => handleStatusChange(row, "Rejected")} + > + Reject + + } + onClick={() => handleStatusChange(row, "Under Review")} + > + Mark for Review + + + )} +
+ ); + }, }, ]; + const isAnyGroupExpanded = useMemo(() => { + if (!isGrouped) return true; + const allGroupKeys = Array.from( + new Set(incidents.map((i) => i.flightNumber || "Other")) + ); + return allGroupKeys.some((key) => !collapsedGroups.has(key)); + }, [isGrouped, incidents, collapsedGroups]); + + const activeColumns = useMemo(() => { + if (!isGrouped || isAnyGroupExpanded) { + return columns; + } + return columns.filter((col) => { + const headerText = + typeof col.header === "object" && col.header !== null && "props" in col.header + ? (col.header as any).props.text + : ""; + return ( + headerText !== "Passenger / PNR" && + headerText !== "Category" && + headerText !== "Perks Claimed" + ); + }); + }, [columns, isGrouped, isAnyGroupExpanded]); + // ─── Loading skeleton ────────────────────────────────────────────────────── if (loading) { @@ -464,52 +501,22 @@ export default function RecoveryIncidentsList() {
{/* Metrics Row */}
- - - - + {metrics.map((metric) => ( + + ))}
{/* Table Section */} - columns={columns} - data={displayData} + columns={activeColumns} + data={paginatedDisplayData} leftHeaderActions={
handleSearchChange(e.target.value)} - leftIcon={} + leftIcon={} className="!bg-[#F3F6F5] !rounded-[10px] !h-[40px] !border !border-[#E5E7EB]" containerClassName="!gap-0" /> @@ -520,15 +527,7 @@ export default function RecoveryIncidentsList() { } - className="!rounded-[10px] !gap-[8px] !h-[40px] !border-primary !text-primary hover:!bg-primary/5" - > - Filters - - } + leftIcon={} className="!rounded-[10px] !gap-[8px] !h-[40px] !border-primary !text-primary hover:!bg-primary/5" onClick={() => setIsGrouped(!isGrouped)} > @@ -537,7 +536,7 @@ export default function RecoveryIncidentsList() { } + leftIcon={} className="!rounded-[10px] !gap-[8px] !h-[40px] !bg-[#1B9869] hover:!bg-[#14704E]" onClick={() => { setEditingIncident(null); @@ -554,20 +553,27 @@ export default function RecoveryIncidentsList() { startIndex={startIndex} endIndex={endIndex} onPageChange={handlePageChange} - itemName="Policies" - onRowClick={(row) => !row.isGroupHeader && navigate(`/recovery/${row.id}`)} - rowClassName={(row) => row.isGroupHeader ? "bg-white" : "bg-[#F9FAFB] border-transparent cursor-pointer hover:bg-[#F3F4F6]"} + itemName="Recovery Incidents" + onRowClick={(row) => { + if (row.isGroupHeader) { + const key = (row as any).groupKey || row.flightNumber || row.recoveryCode; + toggleGroup(key); + } else { + navigate(`/recovery/${row.id}`); + } + }} + rowClassName={(row) => row.isGroupHeader ? "bg-white border-b border-gray-100 hover:bg-gray-50/60" : "bg-[#F9FAFB] border-transparent cursor-pointer hover:bg-[#F3F4F6]"} /> - + {/* Modal */} - { setIsModalOpen(false); setEditingIncident(null); - fetchIncidents(currentPage); // Refresh list - }} + fetchIncidents(); // Refresh list + }} />
); diff --git a/src/utils/formatDate.ts b/src/utils/formatDate.ts new file mode 100644 index 0000000..053b033 --- /dev/null +++ b/src/utils/formatDate.ts @@ -0,0 +1,26 @@ +export function formatDate(dateString?: string | Date): string { + if (!dateString) return ''; + const date = typeof dateString === 'string' ? new Date(dateString) : dateString; + if (isNaN(date.getTime())) return String(dateString); + + const isISO = typeof dateString === 'string' && (dateString.includes('T') || dateString.includes('Z')); + const day = isISO ? date.getUTCDate() : date.getDate(); + const monthIdx = isISO ? date.getUTCMonth() : date.getMonth(); + const year = isISO ? date.getUTCFullYear() : date.getFullYear(); + + const monthNames = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + ]; + const month = monthNames[monthIdx]; + + let hours = isISO ? date.getUTCHours() : date.getHours(); + const minutes = (isISO ? date.getUTCMinutes() : date.getMinutes()).toString().padStart(2, '0'); + const ampm = hours >= 12 ? 'pm' : 'am'; + hours = hours % 12; + hours = hours ? hours : 12; + + return `${day} ${month} ${year}, ${hours}:${minutes}${ampm}`; +} + +export default formatDate;