Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,26 @@ $payment-field-label-color: #2a2a2a;

.summaryRowFive {
gap: 12px;
grid-template-columns: repeat(5, minmax(0, 1fr));
// Size columns to the nowrap labels so "Budget Approver" / "Payment Approver"
// stay on one line; extra space is shared equally.
grid-template-columns: repeat(5, minmax(max-content, 1fr));
min-width: min-content;

.summaryItem {
min-width: max-content;
}

.summaryValue {
// Do not let long handles inflate column min-width; ellipsize instead.
min-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 0;
}

.summaryItem:last-child {
margin-left: 16px;
}
}

Expand All @@ -42,6 +56,7 @@ $payment-field-label-color: #2a2a2a;
color: $payment-field-label-color;
font-size: 16px;
font-weight: 700;
white-space: nowrap;
}

.summaryValue {
Expand Down Expand Up @@ -308,4 +323,21 @@ $payment-field-label-color: #2a2a2a;
.detailsGrid {
grid-template-columns: 1fr;
}

.summaryRowFive {
min-width: 0;
}

.summaryRowFive .summaryItem {
min-width: 0;
}

.summaryRowFive .summaryValue {
min-width: 0;
width: auto;
}

.summaryRowFive .summaryItem:last-child {
margin-left: 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
getMemberHandle,
} from '../../services/wallet'
import { PaymentView } from '.'
import { formatAuditTimestamp } from './payment-view.utils'

jest.mock('../../services/wallet', () => ({
fetchAuditLogs: jest.fn(),
Expand Down Expand Up @@ -447,6 +448,41 @@ describe('PaymentView', () => {
})
})

it('formats release date values in audit actions in the user timezone', async () => {
const fromIso = '2026-06-11T05:05:29.611Z'
const toIso = '2026-06-03T05:05:29.000Z'
const topgearPayment: Winning = {
...payment,
description: 'Test Project Topgeader BA - Week Ending: May 02, 2026',
externalId: 'topgear-challenge-1',
type: 'topgear payment',
}

mockedFetchAuditLogs.mockResolvedValue([{
action: `Modified release date from ${fromIso} to ${toIso}`,
createdAt: '2026-05-27T05:49:00.000Z',
id: 'audit-1',
note: 'Changed payment and release date',
userId: 'mess',
winningsId: topgearPayment.id,
}])

render(<PaymentView payment={topgearPayment} onClose={jest.fn()} />)

await userEvent.click(screen.getByRole('tab', { name: 'Audit History' }))

expect(await screen.findByText('Changed payment and release date'))
.toBeTruthy()
expect(screen.queryByText(fromIso))
.toBeNull()
expect(screen.queryByText(toIso))
.toBeNull()
expect(screen.getByText(formatAuditTimestamp(fromIso)))
.toBeTruthy()
expect(screen.getByText(formatAuditTimestamp(toIso)))
.toBeTruthy()
})

it('renders taas payment details with handle and payment summary only', async () => {
const taasPayment: Winning = {
...payment,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
buildWorkAppChallengeUrl,
buildWorkManagerAssignmentUrl,
buildWorkManagerProjectUrl,
formatAuditActionValue,
getPaymentDetailsSummaryConfig,
isChallengePaymentType,
resolvePaymentAgreementSummary,
Expand Down Expand Up @@ -264,11 +265,11 @@ const PaymentView: React.FC<PaymentViewProps> = (props: PaymentViewProps) => {
{beforeFrom}
from
{' '}
<strong>{fromValue}</strong>
<strong>{formatAuditActionValue(fromValue)}</strong>
{' '}
to
{' '}
<strong>{toValue}</strong>
<strong>{formatAuditActionValue(toValue)}</strong>
</>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {
formatAuditActionValue,
formatAuditTimestamp,
} from './payment-view.utils'

jest.mock('~/config', () => ({
EnvironmentConfig: {
ADMIN: {
WORK_MANAGER_URL: 'https://challenges.example.com',
},
},
}), { virtual: true })

describe('formatAuditActionValue', () => {
it('formats ISO timestamps in the user timezone', () => {
const iso = '2026-06-11T05:05:29.611Z'

expect(formatAuditActionValue(iso))
.toBe(formatAuditTimestamp(iso))
expect(formatAuditActionValue(iso))
.toMatch(/^\d{2}\/\d{2}\/\d{4}, \d{1,2}:\d{2} (AM|PM)$/)
expect(formatAuditActionValue(iso))
.not.toContain('T')
})

it('formats ISO timestamps that include a timezone offset', () => {
const iso = '2026-06-03T05:05:29.000+00:00'

expect(formatAuditActionValue(iso))
.toBe(formatAuditTimestamp(iso))
expect(formatAuditActionValue(` ${iso} `))
.toBe(formatAuditTimestamp(iso))
})

it('leaves non-date action values unchanged', () => {
expect(formatAuditActionValue('OWED'))
.toBe('OWED')
expect(formatAuditActionValue('1000'))
.toBe('1000')
expect(formatAuditActionValue(' $2,000.00 '))
.toBe('$2,000.00')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,20 @@ export function formatAuditTimestamp(value: string): string {
return `${parts.dateLine} ${parts.timeLine}`
}

const ISO_DATE_TIME_PATTERN
= /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/

/** Formats ISO datetimes in audit action from/to values using the user's timezone. */
export function formatAuditActionValue(value: string): string {
const trimmed = value.trim()

if (!ISO_DATE_TIME_PATTERN.test(trimmed)) {
return trimmed
}

return formatAuditTimestamp(trimmed)
}

export function getEngagementHoursPerDay(
engagementDetails?: PaymentEngagementDetails,
): number | undefined {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,8 @@ describe('EngagementPaymentPage', () => {
.toEqual(expect.arrayContaining([
'Billing Start Date*',
'Rate Per Hour*',
'Standard Hours Per Week*',
'Standard Hours Per Day*',
'Payment Cycle*',
]))

fireEvent.click(screen.getByRole('button', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1104,7 +1104,10 @@ export const EngagementPaymentPage: FC = () => {
</span>
</div>
<div>
<span className={styles.label}>Payment Cycle</span>
<span className={styles.label}>
Payment Cycle
<span aria-hidden='true' className={styles.required}>*</span>
</span>
<span className={styles.value}>{formatPaymentCycle(assignment.paymentCycle)}</span>
</div>
<div>
Expand Down
Loading