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 @@ -339,6 +339,7 @@ export const ManageSubmissionPage: FC<Props> = (props: Props) => {
challengeId,
file: selectedFile,
fileName: selectedFile.name,
memberHandle: String(selectedHandle.label),
memberId: selectedHandle.value,
})

Expand Down
5 changes: 5 additions & 0 deletions src/apps/admin/src/lib/services/submissions.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const SUBMISSIONS_PER_PAGE = 100
interface ManualSubmissionUploadPayload {
challengeId: string
memberId: number | string
memberHandle?: string
file: File
fileName?: string
submittedDate?: string
Expand Down Expand Up @@ -227,6 +228,10 @@ export const uploadManualSubmission = async (
formData.append('memberId', String(payload.memberId))
formData.append('type', payload.type ?? 'CONTEST_SUBMISSION')

if (payload.memberHandle) {
formData.append('memberHandle', payload.memberHandle)
}

if (payload.fileName) {
formData.append('fileName', payload.fileName)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ const mockedFetchWinningPaymentDetails = (
const expectedWorkManagerLink
= 'https://challenges.example.com/projects/project-789/engagements/engagement-456/assignments'
+ '?assignmentId=assignment-123'
const expectedProjectLink
= 'https://challenges.example.com/projects/project-789'

describe('PaymentView', () => {
const payment: Winning = {
Expand Down Expand Up @@ -98,6 +100,7 @@ describe('PaymentView', () => {
ratePerHour: '10.60',
standardHoursPerWeek: 43.75,
},
paymentCreatorHandle: 'copilot-manager',
workLog: {
hoursWorked: 43.75,
remarks: 'Completed sprint support and bug triage. Reference: https://example.com/worklog',
Expand All @@ -119,7 +122,7 @@ describe('PaymentView', () => {

expect(await screen.findByRole('heading', { name: 'Engagement Details' }))
.toBeTruthy()
expect(await screen.findByText('Wipro - US Foods / Snowflake Developer - Vikash'))
expect(await screen.findByText('copilot-manager'))
.toBeTruthy()
await waitFor(() => {
expect(screen.getAllByText('43.75'))
Expand All @@ -135,6 +138,15 @@ describe('PaymentView', () => {
expect(descriptionLink.getAttribute('href'))
.toBe(expectedWorkManagerLink)

const projectLink = await screen.findByRole('link', {
name: 'Wipro - US Foods',
})

expect(projectLink.getAttribute('href'))
.toBe(expectedProjectLink)
expect(projectLink.getAttribute('target'))
.toBe('_blank')

const remarksLink = await screen.findByRole('link', {
name: 'https://google.com',
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
} from '../../services/wallet'
import {
buildWorkManagerAssignmentUrl,
formatEngagementProjectName,
buildWorkManagerProjectUrl,
formatOptionalDate,
formatOptionalText,
renderOptionalLinkedText,
Expand Down Expand Up @@ -139,6 +139,7 @@ const PaymentView: React.FC<PaymentViewProps> = (props: PaymentViewProps) => {
const descriptionLink = isEngagementPayment
? buildWorkManagerAssignmentUrl(paymentDetails?.engagementDetails)
: `${TOPCODER_URL}/challenges/${props.payment.externalId}`
const projectLink = buildWorkManagerProjectUrl(paymentDetails?.engagementDetails)

return (
<div className={styles.formContainer}>
Expand All @@ -163,6 +164,16 @@ const PaymentView: React.FC<PaymentViewProps> = (props: PaymentViewProps) => {
<span className={styles.label}>Handle</span>
<p className={styles.value}>{props.payment.handle}</p>
</div>
{isEngagementPayment && (
<div className={styles.infoItem}>
<span className={styles.label}>Payment Creator</span>
<p className={styles.value}>
{isPaymentDetailsLoading
? 'Loading...'
: formatOptionalText(paymentDetails?.paymentCreatorHandle)}
</p>
</div>
)}

<div className={styles.infoItem}>
<span className={styles.label}>Type</span>
Expand Down Expand Up @@ -215,10 +226,23 @@ const PaymentView: React.FC<PaymentViewProps> = (props: PaymentViewProps) => {
{!isPaymentDetailsLoading && !paymentDetailsError && hasEngagementDetails && (
<div className={styles.sectionGrid}>
<div className={styles.infoItem}>
<span className={styles.label}>Engagement / Project Name</span>
<p className={styles.value}>
{formatEngagementProjectName(paymentDetails?.engagementDetails)}
</p>
<span className={styles.label}>Project Name</span>
{projectLink && paymentDetails?.engagementDetails?.projectName
? (
<a
className={styles.value}
href={projectLink}
target='_blank'
rel='noreferrer'
>
{paymentDetails.engagementDetails.projectName}
</a>
)
: (
<p className={styles.value}>
{formatOptionalText(paymentDetails?.engagementDetails?.projectName)}
</p>
)}
</div>
<div className={styles.infoItem}>
<span className={styles.label}>Billing Start Date</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,31 @@ export function buildWorkManagerAssignmentUrl(
return `${assignmentPath}?assignmentId=${engagementDetails.assignmentId}`
}

/**
* Builds the Work Manager project destination used by wallet-admin payment
* details when engagement payments expose a linked project.
*
* @param engagementDetails Engagement metadata attached to the payment.
* @returns The absolute project URL when a project id is available; otherwise
* `undefined`.
*
* @remarks The payment details popup opens this URL in a new tab so admins can
* jump directly from a payment to its owning project workspace.
*
* @throws This helper does not raise exceptions.
*/
export function buildWorkManagerProjectUrl(
engagementDetails?: PaymentEngagementDetails,
): string | undefined {
if (!engagementDetails?.projectId) {
return undefined
}

const baseUrl = EnvironmentConfig.ADMIN.WORK_MANAGER_URL.replace(/\/$/, '')

return `${baseUrl}/projects/${engagementDetails.projectId}`
}

export function formatOptionalText(
value?: number | string | null,
): string {
Expand Down Expand Up @@ -119,18 +144,3 @@ export function formatOptionalDate(
year: 'numeric',
})
}

export function formatEngagementProjectName(
engagementDetails?: PaymentEngagementDetails,
): string {
const projectName = String(engagementDetails?.projectName || '')
.trim()
const engagementTitle = String(engagementDetails?.engagementTitle || '')
.trim()

if (projectName && engagementTitle) {
return `${projectName} / ${engagementTitle}`
}

return projectName || engagementTitle || '-'
}
1 change: 1 addition & 0 deletions src/apps/wallet-admin/src/lib/models/WinningDetail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export interface PaymentWorkLog {

export interface WinningPaymentDetails {
engagementDetails?: PaymentEngagementDetails
paymentCreatorHandle?: string
workLog?: PaymentWorkLog
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,16 @@
grid-template-areas: none;
}

.projectMode > * {
// Match the desktop/tablet selector specificity so the named grid areas
// are actually cleared when the layout collapses to a single column.
.projectMode .searchField,
.projectMode .statusField,
.projectMode .typeField,
.projectMode .startDateFromField,
.projectMode .startDateToField,
.projectMode .endDateFromField,
.projectMode .endDateToField,
.projectMode .actionsField {
grid-area: auto;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,27 @@

.remarks {
color: #5b5b5b;
overflow-wrap: anywhere;
white-space: pre-wrap;
}

.hoursWorked {
color: #5b5b5b;
font-size: 12px;
}

.paymentCreator {
color: #5b5b5b;
display: flex;
flex-wrap: wrap;
font-size: 12px;
gap: 4px;
}

.paymentCreatorLabel {
font-weight: 600;
}

.date {
color: #767676;
font-size: 12px;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/* eslint-disable import/no-extraneous-dependencies, ordered-imports/ordered-imports */
import {
render,
screen,
} from '@testing-library/react'

import PaymentHistoryModal from './PaymentHistoryModal'

const mockUseFetchAssignmentPayments = jest.fn()

jest.mock('../../hooks', () => ({
useFetchAssignmentPayments: (...args: unknown[]): unknown => mockUseFetchAssignmentPayments(...args),
}))

jest.mock('~/libs/ui', () => ({
BaseModal: (props: {
buttons?: JSX.Element
children: JSX.Element
open: boolean
title: string
}): JSX.Element => (
props.open ? (
<div>
<h1>{props.title}</h1>
{props.children}
{props.buttons}
</div>
) : <></>
),
Button: (props: {
label: string
onClick: () => void
}): JSX.Element => (
<button onClick={props.onClick} type='button'>
{props.label}
</button>
),
}), {
virtual: true,
})

describe('PaymentHistoryModal', () => {
beforeEach(() => {
mockUseFetchAssignmentPayments.mockReset()
})

it('renders clickable remarks links and the payment creator handle', async () => {
mockUseFetchAssignmentPayments.mockReturnValue({
error: undefined,
isLoading: false,
isValidating: false,
mutate: jest.fn(),
payments: [
{
amount: 120,
attributes: {
remarks: 'Support ticket: https://topcoder.zendesk.com/agent/tickets/141390.',
},
createdAt: '2026-03-31T00:00:00.000Z',
createdByHandle: 'payment.manager',
id: 'payment-1',
title: 'Salesforce support',
},
],
})

render(
<PaymentHistoryModal
assignmentId='assignment-1'
memberHandle='salesforce'
onClose={jest.fn()}
open
/>,
)

const remarksLink = await screen.findByRole('link', {
name: 'https://topcoder.zendesk.com/agent/tickets/141390',
})

expect(remarksLink.getAttribute('href'))
.toBe('https://topcoder.zendesk.com/agent/tickets/141390')
expect(remarksLink.getAttribute('target'))
.toBe('_blank')
expect(screen.getByText('Payment Creator:'))
.toBeTruthy()
expect(screen.getByText('payment.manager'))
.toBeTruthy()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import {
useFetchAssignmentPayments,
} from '../../hooks'
import {
formatCurrency,
getPaymentAmount,
getPaymentCreatorLabel,
getPaymentHoursWorked,
getPaymentRemarks,
getPaymentStatus,
} from '../../utils'
import {
formatCurrency,
renderPaymentLinkedText,
} from '../../utils/payment.utils'

import styles from './PaymentHistoryModal.module.scss'
Expand Down Expand Up @@ -89,6 +89,7 @@ const PaymentHistoryModal: FC<PaymentHistoryModalProps> = (
const paymentStatus = getPaymentStatus(payment)
const paymentHoursWorked = getPaymentHoursWorked(payment)
const paymentRemarks = getPaymentRemarks(payment)
const paymentCreator = getPaymentCreatorLabel(payment)
const normalizedPaymentStatus = paymentStatus
.trim()
.toLowerCase()
Expand All @@ -109,7 +110,11 @@ const PaymentHistoryModal: FC<PaymentHistoryModalProps> = (
<div className={styles.itemBody}>
<div>{payment.title || payment.description || 'Payment'}</div>
{paymentRemarks
? <div className={styles.remarks}>{paymentRemarks}</div>
? (
<div className={styles.remarks}>
{renderPaymentLinkedText(paymentRemarks)}
</div>
)
: <div className={styles.remarks}>No remarks</div>}
{paymentHoursWorked
? (
Expand All @@ -120,6 +125,12 @@ const PaymentHistoryModal: FC<PaymentHistoryModalProps> = (
</div>
)
: undefined}
<div className={styles.paymentCreator}>
<span className={styles.paymentCreatorLabel}>
Payment Creator:
</span>
<span>{paymentCreator || '-'}</span>
</div>
<div className={styles.date}>
{formatDate(payment.createdAt || payment.updatedAt)}
</div>
Expand Down
2 changes: 2 additions & 0 deletions src/apps/work/src/lib/models/Engagement.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ export interface AssignmentPayment {
hoursWorked?: number | string
remarks?: string
}
createdBy?: string
createdByHandle?: string
createdAt?: string
description?: string
details?: Array<{
Expand Down
Loading
Loading