Merged in feat/LOY-391-my-points-transactions-table-design (pull request #3415)
Feat/LOY-391 my points transactions table design * feat(LOY-391): Added new design to point transaction table * fix(LOY-391): rebase fix * fix(LOY-391): fix * fix(LOY-391): fix * fix(LOY-391): fixed sticky header etc. * feat(LOY-391): added focus on the newest loaded item in the list * fix(LOY-391): cleaned up * fix(LOY-391): style fix * fix(LOY-391): fixed PR-comments, types, removed the old files for earn and burn table * fix(LOY-391): fixed PR-comments * feat(LOY-391): added useCallback so scrolling is avoided when clicking see all on expiring points Approved-by: Anton Gunnarsson Approved-by: Matilda Landström
This commit is contained in:
@@ -1,45 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { keepPreviousData } from "@tanstack/react-query"
|
|
||||||
import { useState } from "react"
|
|
||||||
|
|
||||||
import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner"
|
|
||||||
import { trpc } from "@scandic-hotels/trpc/client"
|
|
||||||
|
|
||||||
import Pagination from "@/components/MyPages/Pagination"
|
|
||||||
import useLang from "@/hooks/useLang"
|
|
||||||
|
|
||||||
import ClientTable from "./ClientTable"
|
|
||||||
|
|
||||||
export default function TransactionTable() {
|
|
||||||
const limit = 5
|
|
||||||
const [page, setPage] = useState(1)
|
|
||||||
const lang = useLang()
|
|
||||||
const { data, isFetching, isLoading } =
|
|
||||||
trpc.user.transaction.friendTransactions.useQuery(
|
|
||||||
{
|
|
||||||
limit,
|
|
||||||
page,
|
|
||||||
lang,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
placeholderData: keepPreviousData,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return isLoading ? (
|
|
||||||
<LoadingSpinner />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ClientTable transactions={data?.data.transactions || []} />
|
|
||||||
{data && data.meta.totalPages > 1 ? (
|
|
||||||
<Pagination
|
|
||||||
handlePageChange={setPage}
|
|
||||||
pageCount={data.meta.totalPages}
|
|
||||||
isFetching={isFetching}
|
|
||||||
currentPage={page}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { usePathname } from "next/navigation"
|
|
||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import { dt } from "@scandic-hotels/common/dt"
|
|
||||||
import Table from "@scandic-hotels/design-system/Table"
|
|
||||||
import { TextLink } from "@scandic-hotels/design-system/TextLink"
|
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
|
||||||
import { Transactions } from "@scandic-hotels/trpc/enums/transactions"
|
|
||||||
|
|
||||||
import { webviews } from "@/constants/routes/webviews"
|
|
||||||
|
|
||||||
import useLang from "@/hooks/useLang"
|
|
||||||
|
|
||||||
import AwardPoints from "../../../AwardPoints"
|
|
||||||
|
|
||||||
import type { Transaction } from "@/types/components/myPages/myPage/earnAndBurn"
|
|
||||||
|
|
||||||
interface RowProps {
|
|
||||||
transaction: Transaction
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Row({ transaction }: RowProps) {
|
|
||||||
const intl = useIntl()
|
|
||||||
const lang = useLang()
|
|
||||||
const pathName = usePathname()
|
|
||||||
const isWebview = webviews.includes(pathName)
|
|
||||||
|
|
||||||
const { hotelName, city } = transaction
|
|
||||||
const nightsMsg = intl.formatMessage(
|
|
||||||
{
|
|
||||||
id: "booking.numberOfNights",
|
|
||||||
defaultMessage: "{totalNights, plural, one {# night} other {# nights}}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
totalNights: transaction.nights,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
let description =
|
|
||||||
transaction.confirmationNumber === "non-transactional" &&
|
|
||||||
transaction.nights === 0
|
|
||||||
? intl.formatMessage({
|
|
||||||
id: "earnAndBurn.journeyTable.pointsActivity",
|
|
||||||
defaultMessage: "Point activity",
|
|
||||||
})
|
|
||||||
: hotelName && city
|
|
||||||
? `${hotelName}, ${city} ${nightsMsg}`
|
|
||||||
: `${nightsMsg}`
|
|
||||||
|
|
||||||
switch (transaction.type) {
|
|
||||||
case Transactions.rewardType.stay:
|
|
||||||
case Transactions.rewardType.stayAdj:
|
|
||||||
if (transaction.hotelId === "ORS") {
|
|
||||||
description = intl.formatMessage({
|
|
||||||
id: "earnAndBurn.journeyTable.formerScandicHotel",
|
|
||||||
defaultMessage: "Former Scandic Hotel",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (transaction.confirmationNumber === "BALFWD") {
|
|
||||||
description = intl.formatMessage({
|
|
||||||
id: "earnAndBurn.journeyTable.pointsEarnedPriorMay2021",
|
|
||||||
defaultMessage: "Points earned prior to May 1, 2021",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case Transactions.rewardType.ancillary:
|
|
||||||
description = intl.formatMessage({
|
|
||||||
id: "earnAndBurn.journeyTable.extrasToBooking",
|
|
||||||
defaultMessage: "Extras to your booking",
|
|
||||||
})
|
|
||||||
break
|
|
||||||
case Transactions.rewardType.enrollment:
|
|
||||||
description = intl.formatMessage({
|
|
||||||
id: "earnAndBurn.journeyTable.signUpBonus",
|
|
||||||
defaultMessage: "Sign up bonus",
|
|
||||||
})
|
|
||||||
break
|
|
||||||
case Transactions.rewardType.mastercard_points:
|
|
||||||
description = intl.formatMessage({
|
|
||||||
id: "earnAndBurn.journeyTable.scandicFriendsMastercard",
|
|
||||||
defaultMessage: "Scandic Friends Mastercard",
|
|
||||||
})
|
|
||||||
break
|
|
||||||
case Transactions.rewardType.tui_points:
|
|
||||||
description = intl.formatMessage({
|
|
||||||
id: "earnAndBurn.journeyTable.tuiPoints",
|
|
||||||
defaultMessage: "TUI Points",
|
|
||||||
})
|
|
||||||
|
|
||||||
case Transactions.rewardType.pointShop:
|
|
||||||
description = intl.formatMessage({
|
|
||||||
id: "earnAndBurn.journeyTable.pointShop",
|
|
||||||
defaultMessage: "Scandic Friends Point Shop",
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
const arrival = dt(transaction.checkinDate).locale(lang).format("DD MMM YYYY")
|
|
||||||
|
|
||||||
function renderConfirmationNumber() {
|
|
||||||
if (
|
|
||||||
transaction.confirmationNumber === "BALFWD" ||
|
|
||||||
transaction.confirmationNumber === "non-transactional"
|
|
||||||
)
|
|
||||||
return null
|
|
||||||
|
|
||||||
if (
|
|
||||||
!isWebview &&
|
|
||||||
transaction.bookingUrl &&
|
|
||||||
(transaction.type === Transactions.rewardType.stay ||
|
|
||||||
transaction.type === Transactions.rewardType.rewardNight)
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<TextLink href={transaction.bookingUrl}>
|
|
||||||
{transaction.confirmationNumber}
|
|
||||||
</TextLink>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return transaction.confirmationNumber
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Table.TR>
|
|
||||||
<Table.TD>
|
|
||||||
<AwardPoints
|
|
||||||
awardPoints={transaction.awardPoints}
|
|
||||||
isCalculated={transaction.pointsCalculated}
|
|
||||||
/>
|
|
||||||
</Table.TD>
|
|
||||||
<Table.TD>
|
|
||||||
<Typography variant="Body/Paragraph/mdBold">
|
|
||||||
<p>{description}</p>
|
|
||||||
</Typography>
|
|
||||||
</Table.TD>
|
|
||||||
<Table.TD>{renderConfirmationNumber()}</Table.TD>
|
|
||||||
<Table.TD>
|
|
||||||
{transaction.checkinDate && transaction.confirmationNumber !== "BALFWD"
|
|
||||||
? arrival
|
|
||||||
: null}
|
|
||||||
</Table.TD>
|
|
||||||
</Table.TR>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
.container {
|
|
||||||
overflow-x: auto;
|
|
||||||
border-radius: var(--Corner-radius-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder {
|
|
||||||
width: 100%;
|
|
||||||
padding: 24px;
|
|
||||||
text-align: center;
|
|
||||||
border: 1px solid var(--Scandic-Brand-Pale-Peach);
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
|
||||||
.container {
|
|
||||||
border-radius: var(--Corner-radius-lg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import Table from "@scandic-hotels/design-system/Table"
|
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
|
||||||
|
|
||||||
import Row from "./Row"
|
|
||||||
|
|
||||||
import styles from "./clientTable.module.css"
|
|
||||||
|
|
||||||
import type { Transactions } from "@/types/components/myPages/myPage/earnAndBurn"
|
|
||||||
|
|
||||||
interface ClientTableProps {
|
|
||||||
transactions: Transactions
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ClientTable({ transactions }: ClientTableProps) {
|
|
||||||
const intl = useIntl()
|
|
||||||
|
|
||||||
const tableHeadings = [
|
|
||||||
intl.formatMessage({
|
|
||||||
id: "common.points",
|
|
||||||
defaultMessage: "Points",
|
|
||||||
}),
|
|
||||||
intl.formatMessage({
|
|
||||||
id: "earnAndBurn.journeyTable.description",
|
|
||||||
defaultMessage: "Description",
|
|
||||||
}),
|
|
||||||
intl.formatMessage({
|
|
||||||
id: "earnAndBurn.journeyTable.bookingNumberReference",
|
|
||||||
defaultMessage: "Booking number / Reference",
|
|
||||||
}),
|
|
||||||
intl.formatMessage({
|
|
||||||
id: "earnAndBurn.journeyTable.date",
|
|
||||||
defaultMessage: "Date",
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.container}>
|
|
||||||
<Table>
|
|
||||||
<Table.THead>
|
|
||||||
<Table.TR>
|
|
||||||
{tableHeadings.map((heading) => (
|
|
||||||
<Table.TH key={heading}>
|
|
||||||
<Typography variant="Body/Paragraph/mdBold">
|
|
||||||
<p>{heading}</p>
|
|
||||||
</Typography>
|
|
||||||
</Table.TH>
|
|
||||||
))}
|
|
||||||
</Table.TR>
|
|
||||||
</Table.THead>
|
|
||||||
<Table.TBody>
|
|
||||||
{transactions.length ? (
|
|
||||||
transactions.map((transaction, index) => (
|
|
||||||
<Row
|
|
||||||
key={`${transaction.confirmationNumber}-${index}`}
|
|
||||||
transaction={transaction}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<Table.TR className={styles.placeholder}>
|
|
||||||
<Table.TD colSpan={tableHeadings.length}>
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "earnAndBurn.journeyTable.noTransactions",
|
|
||||||
defaultMessage: "No transactions available",
|
|
||||||
})}
|
|
||||||
</Table.TD>
|
|
||||||
</Table.TR>
|
|
||||||
)}
|
|
||||||
</Table.TBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import ClientJourney from "./Client"
|
|
||||||
|
|
||||||
export default async function JourneyTable() {
|
|
||||||
return <ClientJourney />
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,7 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
|
|||||||
|
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
import AwardPoints from "../../EarnAndBurn/AwardPoints"
|
import AwardPoints from "../AwardPoints"
|
||||||
|
|
||||||
export default function ExpiringPointsTable({
|
export default function ExpiringPointsTable({
|
||||||
points,
|
points,
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Link } from "react-aria-components"
|
||||||
|
|
||||||
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
|
|
||||||
|
export function ConditionalPointTransactionLink({
|
||||||
|
href,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
canLinkBookingUrl,
|
||||||
|
focusRef,
|
||||||
|
}: {
|
||||||
|
href?: string
|
||||||
|
children: React.ReactNode
|
||||||
|
className?: string
|
||||||
|
canLinkBookingUrl: boolean
|
||||||
|
focusRef: React.Ref<HTMLAnchorElement>
|
||||||
|
}) {
|
||||||
|
if (canLinkBookingUrl) {
|
||||||
|
return (
|
||||||
|
<Link className={className} href={href} ref={focusRef}>
|
||||||
|
{children}
|
||||||
|
<MaterialIcon icon="arrow_forward" color="Icon/Interactive/Default" />
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return <div className={className}>{children}</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
"use client"
|
||||||
|
import React from "react"
|
||||||
|
import { type IntlShape, useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import {
|
||||||
|
BALFWD,
|
||||||
|
NON_TRANSACTIONAL,
|
||||||
|
} from "@scandic-hotels/common/constants/transactionType"
|
||||||
|
import { dt } from "@scandic-hotels/common/dt"
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
import { Transactions } from "@scandic-hotels/trpc/enums/transactions"
|
||||||
|
|
||||||
|
import { ConditionalPointTransactionLink } from "../ConditionalPointTransactionLink"
|
||||||
|
|
||||||
|
import styles from "../pointTransactionList.module.css"
|
||||||
|
|
||||||
|
import type { Transaction } from "@/types/components/myPages/myPage/earnAndBurn"
|
||||||
|
|
||||||
|
export function PointTransactionRow({
|
||||||
|
transaction,
|
||||||
|
lang,
|
||||||
|
focusRef,
|
||||||
|
}: {
|
||||||
|
transaction: Transaction
|
||||||
|
lang: string
|
||||||
|
focusRef: React.Ref<HTMLAnchorElement>
|
||||||
|
}) {
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
const { confirmationNumber, bookingUrl, checkinDate, awardPoints } =
|
||||||
|
transaction.attributes
|
||||||
|
const balfwd = confirmationNumber === BALFWD
|
||||||
|
const nonTransactional = confirmationNumber === NON_TRANSACTIONAL
|
||||||
|
|
||||||
|
const day = checkinDate.split("-")[2].replace(/^0/, "")
|
||||||
|
const month = dt(checkinDate.split("-")[1]).locale(lang).format("MMM")
|
||||||
|
|
||||||
|
const formattedPoints = intl.formatNumber(Math.abs(awardPoints))
|
||||||
|
const calculatedPoints =
|
||||||
|
awardPoints === 0
|
||||||
|
? formattedPoints
|
||||||
|
: `${awardPoints > 0 ? "+" : "-"} ${formattedPoints}`
|
||||||
|
|
||||||
|
const canLinkBookingUrl = !balfwd && !nonTransactional
|
||||||
|
|
||||||
|
const description = getDescription(transaction, intl)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConditionalPointTransactionLink
|
||||||
|
className={styles.row}
|
||||||
|
href={bookingUrl}
|
||||||
|
canLinkBookingUrl={canLinkBookingUrl}
|
||||||
|
focusRef={focusRef}
|
||||||
|
>
|
||||||
|
<div className={styles.date}>
|
||||||
|
<Typography variant="Title/Subtitle/lg">
|
||||||
|
<time>{day}</time>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Tag/sm">
|
||||||
|
<time>{month}</time>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.textArea}>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<p>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: "common.pointsInLine",
|
||||||
|
defaultMessage: "{points} points",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
points: (
|
||||||
|
<Typography variant="Body/Paragraph/mdBold" key="points">
|
||||||
|
<strong>{calculatedPoints}</strong>
|
||||||
|
</Typography>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
className={styles.description}
|
||||||
|
variant="Body/Supporting text (caption)/smRegular"
|
||||||
|
>
|
||||||
|
<p>{description}</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</ConditionalPointTransactionLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDescription(transaction: Transaction, intl: IntlShape) {
|
||||||
|
const hotelInformation = transaction.attributes.hotelInformation
|
||||||
|
const balfwd = transaction.attributes.confirmationNumber === BALFWD
|
||||||
|
const nonTransactional =
|
||||||
|
transaction.attributes.confirmationNumber === NON_TRANSACTIONAL
|
||||||
|
switch (transaction.type) {
|
||||||
|
case Transactions.rewardType.stay:
|
||||||
|
return nonTransactional && transaction.attributes.nights === 0
|
||||||
|
? intl.formatMessage({
|
||||||
|
id: "myPoints.pointTransactions.pointsActivity",
|
||||||
|
defaultMessage: "Point activity",
|
||||||
|
})
|
||||||
|
: hotelInformation?.name
|
||||||
|
? intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: "myPoints.pointTransactions.stayAt",
|
||||||
|
defaultMessage: "Stay at {hotelName}",
|
||||||
|
},
|
||||||
|
{ hotelName: hotelInformation?.name }
|
||||||
|
)
|
||||||
|
: ""
|
||||||
|
|
||||||
|
case Transactions.rewardType.stayAdj:
|
||||||
|
if (transaction.attributes.hotelOperaId === "ORS") {
|
||||||
|
return intl.formatMessage({
|
||||||
|
id: "earnAndBurn.journeyTable.formerScandicHotel",
|
||||||
|
defaultMessage: "Former Scandic Hotel",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (balfwd) {
|
||||||
|
return intl.formatMessage({
|
||||||
|
id: "myPoints.pointTransactions.pointsEarnedPriorMay2021",
|
||||||
|
defaultMessage: "Points earned prior to May 1, 2021",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case Transactions.rewardType.ancillary:
|
||||||
|
return intl.formatMessage({
|
||||||
|
id: "myPoints.pointTransactions.extrasToBooking",
|
||||||
|
defaultMessage: "Extras to your booking",
|
||||||
|
})
|
||||||
|
|
||||||
|
case Transactions.rewardType.enrollment:
|
||||||
|
return intl.formatMessage({
|
||||||
|
id: "myPoints.pointTransactions.signUpBonus",
|
||||||
|
defaultMessage: "Sign up bonus",
|
||||||
|
})
|
||||||
|
|
||||||
|
case Transactions.rewardType.mastercard_points:
|
||||||
|
return intl.formatMessage({
|
||||||
|
id: "myPoints.pointTransactions.scandicFriendsMastercard",
|
||||||
|
defaultMessage: "Scandic Friends Mastercard",
|
||||||
|
})
|
||||||
|
|
||||||
|
case Transactions.rewardType.tui_points:
|
||||||
|
return intl.formatMessage({
|
||||||
|
id: "myPoints.pointTransactions.tuiPoints",
|
||||||
|
defaultMessage: "TUI Points",
|
||||||
|
})
|
||||||
|
|
||||||
|
case Transactions.rewardType.pointShop:
|
||||||
|
return intl.formatMessage({
|
||||||
|
id: "myPoints.pointTransactions.pointShop",
|
||||||
|
defaultMessage: "Scandic Friends Point Shop",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
"use client"
|
||||||
|
import { Fragment, useCallback, useRef } from "react"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import useStickyPosition from "@scandic-hotels/common/hooks/useStickyPosition"
|
||||||
|
import { StickyElementNameEnum } from "@scandic-hotels/common/stores/sticky-position"
|
||||||
|
import { Divider } from "@scandic-hotels/design-system/Divider"
|
||||||
|
import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner"
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
import { trpc } from "@scandic-hotels/trpc/client"
|
||||||
|
|
||||||
|
import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton"
|
||||||
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
|
import { PointTransactionRow } from "./PointTransactionRow"
|
||||||
|
|
||||||
|
import styles from "./pointTransactionList.module.css"
|
||||||
|
|
||||||
|
import type { Transactions } from "@/types/components/myPages/myPage/earnAndBurn"
|
||||||
|
|
||||||
|
export function PointTransactionList() {
|
||||||
|
const lang = useLang()
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =
|
||||||
|
trpc.user.transaction.friendTransactions.useInfiniteQuery(
|
||||||
|
{
|
||||||
|
limit: 10,
|
||||||
|
lang,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
getNextPageParam: (lastPage) => {
|
||||||
|
return lastPage?.nextCursor
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const transactions = data?.pages
|
||||||
|
.filter((page): page is Transactions => !!page?.data)
|
||||||
|
.flatMap((page) => page.data)
|
||||||
|
|
||||||
|
const groupedTransactions =
|
||||||
|
transactions?.reduce<Record<number, typeof transactions>>(
|
||||||
|
(acc, transaction) => {
|
||||||
|
const year = new Date(transaction.attributes.checkinDate).getFullYear()
|
||||||
|
if (!acc[year]) acc[year] = []
|
||||||
|
acc[year].push(transaction)
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
) ?? {}
|
||||||
|
|
||||||
|
const sortedYears = Object.keys(groupedTransactions)
|
||||||
|
.map(Number)
|
||||||
|
.sort((a, b) => b - a)
|
||||||
|
|
||||||
|
const lastPage = data?.pages?.[data.pages.length - 1]
|
||||||
|
const firstNewTransaction = lastPage?.data?.[0]
|
||||||
|
|
||||||
|
const headerRef = useRef<HTMLDivElement>(null)
|
||||||
|
useStickyPosition({
|
||||||
|
ref: headerRef,
|
||||||
|
name: StickyElementNameEnum.TRANSACTION_LIST_HEADER,
|
||||||
|
})
|
||||||
|
|
||||||
|
const isEmpty = !isLoading && (transactions?.length ?? 0) === 0
|
||||||
|
|
||||||
|
function loadMoreData() {
|
||||||
|
if (hasNextPage) {
|
||||||
|
shouldFocusNextItem.current = true
|
||||||
|
fetchNextPage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const shouldFocusNextItem = useRef(false)
|
||||||
|
|
||||||
|
const focusRef = useCallback((node: HTMLAnchorElement | null) => {
|
||||||
|
if (!node || !shouldFocusNextItem.current) return
|
||||||
|
|
||||||
|
node.focus()
|
||||||
|
shouldFocusNextItem.current = false
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.list}>
|
||||||
|
{isLoading && <LoadingSpinner />}
|
||||||
|
{isEmpty && (
|
||||||
|
<div className={styles.emptyState}>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<p>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "myPoints.pointTransactions.noTransactions",
|
||||||
|
defaultMessage: "No transactions available",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isLoading &&
|
||||||
|
!isEmpty &&
|
||||||
|
sortedYears.map((year) => (
|
||||||
|
<Fragment key={year}>
|
||||||
|
<div ref={headerRef} className={styles.yearHeader}>
|
||||||
|
<Typography variant="Title/Overline/sm">
|
||||||
|
<h2>{year}</h2>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<ul className={styles.listContent}>
|
||||||
|
{groupedTransactions[year].map((transaction, i) => {
|
||||||
|
const isFirstNewItem = transaction === firstNewTransaction
|
||||||
|
return (
|
||||||
|
<li key={`${year}-${i}`}>
|
||||||
|
<PointTransactionRow
|
||||||
|
transaction={transaction}
|
||||||
|
lang={lang}
|
||||||
|
focusRef={isFirstNewItem ? focusRef : null}
|
||||||
|
/>
|
||||||
|
{i < groupedTransactions[year].length - 1 && (
|
||||||
|
<div className={styles.divider}>
|
||||||
|
<Divider />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className={styles.showMoreButton}>
|
||||||
|
{isFetchingNextPage ? (
|
||||||
|
<LoadingSpinner />
|
||||||
|
) : hasNextPage ? (
|
||||||
|
<ShowMoreButton
|
||||||
|
loadMoreData={loadMoreData}
|
||||||
|
textShowMore={intl.formatMessage({
|
||||||
|
id: "myPoints.pointTransactions.showMoreTransactions",
|
||||||
|
defaultMessage: "Show more transactions",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Space-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
border-radius: var(--Corner-radius-md);
|
||||||
|
border: 1px solid var(--Border-Default);
|
||||||
|
background: var(--Surface-Primary-Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyState {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--Space-x4);
|
||||||
|
color: var(--Text-Tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yearHeader {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 var(--Space-x2);
|
||||||
|
background: var(--Surface-Brand-Primary-1-Default);
|
||||||
|
color: var(--Text-Brand-OnPrimary-1-Heading);
|
||||||
|
height: 37px;
|
||||||
|
align-items: center;
|
||||||
|
position: sticky;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yearHeader:first-child {
|
||||||
|
border-radius: calc(var(--Corner-radius-md) - 1px)
|
||||||
|
calc(var(--Corner-radius-md) - 1px) 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listContent {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
padding: 0 var(--Space-x2);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Space-x2);
|
||||||
|
padding: var(--Space-x2);
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--Text-Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
display: flex;
|
||||||
|
min-width: 50px;
|
||||||
|
min-height: 50px;
|
||||||
|
padding: var(--Space-x05);
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: var(--Corner-radius-sm);
|
||||||
|
color: var(--Text-Tertiary);
|
||||||
|
background: var(--Surface-Secondary-Default, #f2ece6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.date > * + * {
|
||||||
|
margin-top: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textArea {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: var(--Space-x05);
|
||||||
|
flex: 1 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.points {
|
||||||
|
display: flex;
|
||||||
|
height: var(--Space-x3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
display: flex;
|
||||||
|
height: var(--Space-x3);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showMoreButton {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
import { Section } from "@/components/Section"
|
import { Section } from "@/components/Section"
|
||||||
import { SectionHeader } from "@/components/Section/Header"
|
import { SectionHeader } from "@/components/Section/Header"
|
||||||
import SectionLink from "@/components/Section/Link"
|
|
||||||
|
|
||||||
import ClaimPoints from "../ClaimPoints"
|
import ClaimPoints from "../ClaimPoints"
|
||||||
import JourneyTable from "./JourneyTable"
|
import { PointTransactionList } from "./PointTransactionList"
|
||||||
|
|
||||||
import styles from "./earnAndBurn.module.css"
|
import styles from "./pointTransactions.module.css"
|
||||||
|
|
||||||
import type { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage"
|
import type { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage"
|
||||||
|
|
||||||
export default function EarnAndBurn({
|
export function PointTransactions({
|
||||||
link,
|
link,
|
||||||
subtitle,
|
subtitle,
|
||||||
title,
|
title,
|
||||||
@@ -17,12 +16,10 @@ export default function EarnAndBurn({
|
|||||||
return (
|
return (
|
||||||
<Section>
|
<Section>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<SectionHeader heading={title} link={link} preamble={subtitle} />
|
<SectionHeader link={link} preamble={subtitle} heading={title} />
|
||||||
|
|
||||||
<ClaimPoints />
|
<ClaimPoints />
|
||||||
</div>
|
</div>
|
||||||
<JourneyTable />
|
<PointTransactionList />
|
||||||
<SectionLink link={link} variant="mobile" />
|
|
||||||
</Section>
|
</Section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,6 @@ import { ManageCookieConsent } from "@/components/Blocks/DynamicContent/ManageCo
|
|||||||
import MyPagesOverviewShortcuts from "@/components/Blocks/DynamicContent/MyPagesOverviewShortcuts"
|
import MyPagesOverviewShortcuts from "@/components/Blocks/DynamicContent/MyPagesOverviewShortcuts"
|
||||||
import Overview from "@/components/Blocks/DynamicContent/Overview"
|
import Overview from "@/components/Blocks/DynamicContent/Overview"
|
||||||
import OverviewTable from "@/components/Blocks/DynamicContent/OverviewTable"
|
import OverviewTable from "@/components/Blocks/DynamicContent/OverviewTable"
|
||||||
import EarnAndBurn from "@/components/Blocks/DynamicContent/Points/EarnAndBurn"
|
|
||||||
import ExpiringPoints from "@/components/Blocks/DynamicContent/Points/ExpiringPoints"
|
import ExpiringPoints from "@/components/Blocks/DynamicContent/Points/ExpiringPoints"
|
||||||
import PointsOverview from "@/components/Blocks/DynamicContent/Points/Overview"
|
import PointsOverview from "@/components/Blocks/DynamicContent/Points/Overview"
|
||||||
import CurrentRewardsBlock from "@/components/Blocks/DynamicContent/Rewards/CurrentRewards"
|
import CurrentRewardsBlock from "@/components/Blocks/DynamicContent/Rewards/CurrentRewards"
|
||||||
@@ -25,6 +24,7 @@ import UpcomingStays from "@/components/Blocks/DynamicContent/Stays/Upcoming"
|
|||||||
import { ProfilingConsentBanner } from "@/components/MyPages/ProfilingConsent/Banner"
|
import { ProfilingConsentBanner } from "@/components/MyPages/ProfilingConsent/Banner"
|
||||||
import { SJWidget } from "@/components/SJWidget"
|
import { SJWidget } from "@/components/SJWidget"
|
||||||
|
|
||||||
|
import { PointTransactions } from "./Points/PointTransactions"
|
||||||
import JobylonFeed from "./JobylonFeed"
|
import JobylonFeed from "./JobylonFeed"
|
||||||
import { RewardNights } from "./RewardNights"
|
import { RewardNights } from "./RewardNights"
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ function DynamicContentBlocks(props: DynamicContentProps) {
|
|||||||
case DynamicContentEnum.Blocks.components.current_benefits:
|
case DynamicContentEnum.Blocks.components.current_benefits:
|
||||||
return <CurrentRewardsBlock {...dynamic_content} />
|
return <CurrentRewardsBlock {...dynamic_content} />
|
||||||
case DynamicContentEnum.Blocks.components.earn_and_burn:
|
case DynamicContentEnum.Blocks.components.earn_and_burn:
|
||||||
return <EarnAndBurn {...dynamic_content} />
|
return <PointTransactions {...dynamic_content} />
|
||||||
case DynamicContentEnum.Blocks.components.expiring_points:
|
case DynamicContentEnum.Blocks.components.expiring_points:
|
||||||
return <ExpiringPoints {...dynamic_content} />
|
return <ExpiringPoints {...dynamic_content} />
|
||||||
case DynamicContentEnum.Blocks.components.how_it_works:
|
case DynamicContentEnum.Blocks.components.how_it_works:
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { BlocksEnums } from "@scandic-hotels/trpc/types/blocksEnum"
|
|||||||
import { DynamicContentEnum } from "@scandic-hotels/trpc/types/dynamicContent"
|
import { DynamicContentEnum } from "@scandic-hotels/trpc/types/dynamicContent"
|
||||||
|
|
||||||
import Overview from "@/components/Blocks/DynamicContent/Overview"
|
import Overview from "@/components/Blocks/DynamicContent/Overview"
|
||||||
import EarnAndBurn from "@/components/Blocks/DynamicContent/Points/EarnAndBurn"
|
|
||||||
import PointsOverview from "@/components/Blocks/DynamicContent/Points/Overview"
|
import PointsOverview from "@/components/Blocks/DynamicContent/Points/Overview"
|
||||||
|
import { PointTransactions } from "@/components/Blocks/DynamicContent/Points/PointTransactions"
|
||||||
import CurrentRewardsBlock from "@/components/Blocks/DynamicContent/Rewards/CurrentRewards"
|
import CurrentRewardsBlock from "@/components/Blocks/DynamicContent/Rewards/CurrentRewards"
|
||||||
import NextLevelRewardsBlock from "@/components/Blocks/DynamicContent/Rewards/NextLevel"
|
import NextLevelRewardsBlock from "@/components/Blocks/DynamicContent/Rewards/NextLevel"
|
||||||
import ShortcutsList from "@/components/Blocks/ShortcutsList"
|
import ShortcutsList from "@/components/Blocks/ShortcutsList"
|
||||||
@@ -41,7 +41,7 @@ async function DynamicComponent({ dynamic_content }: AccountPageContentProps) {
|
|||||||
// return <ExpiringPoints />
|
// return <ExpiringPoints />
|
||||||
return null
|
return null
|
||||||
case DynamicContentEnum.Blocks.components.earn_and_burn:
|
case DynamicContentEnum.Blocks.components.earn_and_burn:
|
||||||
return <EarnAndBurn {...dynamicContent} />
|
return <PointTransactions {...dynamicContent} />
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import type { VariantProps } from "class-variance-authority"
|
import type { VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
import type { awardPointsVariants } from "@/components/Blocks/DynamicContent/Points/EarnAndBurn/AwardPoints/awardPointsVariants"
|
import type { awardPointsVariants } from "@/components/Blocks/DynamicContent/Points/ExpiringPoints/AwardPoints/awardPointsVariants"
|
||||||
import type { UserQueryRouter } from "../user"
|
import type { UserQueryRouter } from "../user"
|
||||||
|
|
||||||
type TransactionResponse = Awaited<
|
type TransactionResponse = Awaited<
|
||||||
ReturnType<UserQueryRouter["transaction"]["friendTransactions"]>
|
ReturnType<UserQueryRouter["transaction"]["friendTransactions"]>
|
||||||
>
|
>
|
||||||
type TransactionsNonNullResponseObject = NonNullable<TransactionResponse>
|
type TransactionsNonNullResponseObject = NonNullable<TransactionResponse>
|
||||||
export type Transactions =
|
export type Transactions = TransactionsNonNullResponseObject
|
||||||
NonNullable<TransactionsNonNullResponseObject>["data"]["transactions"]
|
export type Transaction = Transactions["data"][number]
|
||||||
export type Transaction = Transactions[number]
|
|
||||||
|
|
||||||
export type AwardPointsVariantProps = VariantProps<typeof awardPointsVariants>
|
export type AwardPointsVariantProps = VariantProps<typeof awardPointsVariants>
|
||||||
|
|||||||
4
packages/common/constants/transactionType.ts
Normal file
4
packages/common/constants/transactionType.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// 'BALFWD' are transactions from Opera migration that happended in May 2021
|
||||||
|
export const BALFWD = "BALFWD"
|
||||||
|
|
||||||
|
export const NON_TRANSACTIONAL = "non-transactional"
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
"./constants/routes/*": "./constants/routes/*.ts",
|
"./constants/routes/*": "./constants/routes/*.ts",
|
||||||
"./constants/sessionKeys": "./constants/sessionKeys.ts",
|
"./constants/sessionKeys": "./constants/sessionKeys.ts",
|
||||||
"./constants/signatureHotels": "./constants/signatureHotels.ts",
|
"./constants/signatureHotels": "./constants/signatureHotels.ts",
|
||||||
|
"./constants/transactionType": "./constants/transactionType.ts",
|
||||||
"./dataCache": "./dataCache/index.ts",
|
"./dataCache": "./dataCache/index.ts",
|
||||||
"./dt": "./dt/dt.ts",
|
"./dt": "./dt/dt.ts",
|
||||||
"./dt/utils/hasOverlappingDates": "./dt/utils/hasOverlappingDates.ts",
|
"./dt/utils/hasOverlappingDates": "./dt/utils/hasOverlappingDates.ts",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export enum StickyElementNameEnum {
|
|||||||
HOTEL_TAB_NAVIGATION = "HOTEL_TAB_NAVIGATION",
|
HOTEL_TAB_NAVIGATION = "HOTEL_TAB_NAVIGATION",
|
||||||
HOTEL_STATIC_MAP = "HOTEL_STATIC_MAP",
|
HOTEL_STATIC_MAP = "HOTEL_STATIC_MAP",
|
||||||
DESTINATION_SIDEBAR = "DESTINATION_SIDEBAR",
|
DESTINATION_SIDEBAR = "DESTINATION_SIDEBAR",
|
||||||
|
TRANSACTION_LIST_HEADER = "TRANSACTION_LIST_HEADER",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StickyElement {
|
export interface StickyElement {
|
||||||
@@ -39,6 +40,7 @@ const priorityMap: Record<StickyElementNameEnum, number> = {
|
|||||||
|
|
||||||
[StickyElementNameEnum.MEETING_PACKAGE_WIDGET]: 3,
|
[StickyElementNameEnum.MEETING_PACKAGE_WIDGET]: 3,
|
||||||
[StickyElementNameEnum.DESTINATION_SIDEBAR]: 3,
|
[StickyElementNameEnum.DESTINATION_SIDEBAR]: 3,
|
||||||
|
[StickyElementNameEnum.TRANSACTION_LIST_HEADER]: 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStickyPositionStore = create<StickyStore>((set, get) => ({
|
const useStickyPositionStore = create<StickyStore>((set, get) => ({
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ export const staysInput = z
|
|||||||
|
|
||||||
export const friendTransactionsInput = z
|
export const friendTransactionsInput = z
|
||||||
.object({
|
.object({
|
||||||
limit: z.number().int().positive(),
|
cursor: z.number().optional(),
|
||||||
page: z.number().int().positive(),
|
limit: z.number().min(0).default(10),
|
||||||
lang: z.nativeEnum(Lang).optional(),
|
lang: z.nativeEnum(Lang).optional(),
|
||||||
})
|
})
|
||||||
.default({ limit: 5, page: 1 })
|
.default({})
|
||||||
|
|
||||||
// Mutation
|
// Mutation
|
||||||
export const addCreditCardInput = z.object({
|
export const addCreditCardInput = z.object({
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { BALFWD } from "@scandic-hotels/common/constants/transactionType"
|
||||||
import { createCounter } from "@scandic-hotels/common/telemetry"
|
import { createCounter } from "@scandic-hotels/common/telemetry"
|
||||||
import { safeTry } from "@scandic-hotels/common/utils/safeTry"
|
import { safeTry } from "@scandic-hotels/common/utils/safeTry"
|
||||||
|
|
||||||
@@ -239,22 +240,23 @@ export const userQueryRouter = router({
|
|||||||
friendTransactions: languageProtectedProcedure
|
friendTransactions: languageProtectedProcedure
|
||||||
.input(friendTransactionsInput)
|
.input(friendTransactionsInput)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const { limit, page, lang } = input
|
const { limit, cursor, lang } = input
|
||||||
|
const language = lang ?? ctx.lang
|
||||||
|
|
||||||
|
const page = cursor ? Number(cursor) : 1
|
||||||
|
|
||||||
const friendTransactionsCounter = createCounter(
|
const friendTransactionsCounter = createCounter(
|
||||||
"trpc.user.transactions.friendTransactions"
|
"trpc.user.transactions.friendTransactions2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const metricsFriendTransactions = friendTransactionsCounter.init({
|
const metricsFriendTransactions = friendTransactionsCounter.init({
|
||||||
limit,
|
limit,
|
||||||
page,
|
cursor,
|
||||||
lang,
|
language,
|
||||||
})
|
})
|
||||||
|
|
||||||
metricsFriendTransactions.start()
|
metricsFriendTransactions.start()
|
||||||
|
|
||||||
const language = lang ?? ctx.lang
|
|
||||||
|
|
||||||
const apiResponse = await api.get(
|
const apiResponse = await api.get(
|
||||||
api.endpoints.v1.Profile.Transaction.friendTransactions,
|
api.endpoints.v1.Profile.Transaction.friendTransactions,
|
||||||
{
|
{
|
||||||
@@ -282,15 +284,13 @@ export const userQueryRouter = router({
|
|||||||
const updatedData = await updateStaysBookingUrl(
|
const updatedData = await updateStaysBookingUrl(
|
||||||
verifiedData.data.data,
|
verifiedData.data.data,
|
||||||
ctx.session,
|
ctx.session,
|
||||||
ctx.lang
|
language
|
||||||
)
|
)
|
||||||
|
const allTransactions = updatedData
|
||||||
const pageData = updatedData
|
|
||||||
.filter((t) => t.type !== Transactions.rewardType.expired)
|
.filter((t) => t.type !== Transactions.rewardType.expired)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
// 'BALFWD' are transactions from Opera migration that happended in May 2021
|
if (a.attributes.confirmationNumber === BALFWD) return 1
|
||||||
if (a.attributes.confirmationNumber === "BALFWD") return 1
|
if (b.attributes.confirmationNumber === BALFWD) return -1
|
||||||
if (b.attributes.confirmationNumber === "BALFWD") return -1
|
|
||||||
|
|
||||||
const dateA = new Date(
|
const dateA = new Date(
|
||||||
a.attributes.checkinDate
|
a.attributes.checkinDate
|
||||||
@@ -306,35 +306,20 @@ export const userQueryRouter = router({
|
|||||||
return dateA > dateB ? -1 : 1
|
return dateA > dateB ? -1 : 1
|
||||||
})
|
})
|
||||||
|
|
||||||
const slicedData = pageData.slice(limit * (page - 1), limit * page)
|
const startIndex = limit * (page - 1)
|
||||||
|
const endIndex = limit * page
|
||||||
const result = {
|
const paginatedTransactions = allTransactions.slice(
|
||||||
data: {
|
startIndex,
|
||||||
transactions: slicedData.map(({ type, attributes }) => {
|
endIndex
|
||||||
return {
|
)
|
||||||
type,
|
|
||||||
awardPoints: attributes.awardPoints,
|
|
||||||
checkinDate: attributes.checkinDate,
|
|
||||||
checkoutDate: attributes.checkoutDate,
|
|
||||||
confirmationNumber: attributes.confirmationNumber,
|
|
||||||
nights: attributes.nights,
|
|
||||||
pointsCalculated: attributes.pointsCalculated,
|
|
||||||
hotelId: attributes.hotelOperaId,
|
|
||||||
transactionDate: attributes.transactionDate,
|
|
||||||
bookingUrl: attributes.bookingUrl,
|
|
||||||
hotelName: attributes.hotelInformation?.name,
|
|
||||||
city: attributes.hotelInformation?.city,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
meta: {
|
|
||||||
totalPages: Math.ceil(pageData.length / limit),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const hasMore = endIndex < allTransactions.length
|
||||||
metricsFriendTransactions.success()
|
metricsFriendTransactions.success()
|
||||||
|
|
||||||
return result
|
return {
|
||||||
|
data: paginatedTransactions,
|
||||||
|
nextCursor: hasMore ? Number(page + 1) : undefined,
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user