diff --git a/.circleci/config.yml b/.circleci/config.yml index 98b66ba..0d7d4ee 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -67,10 +67,6 @@ workflows: branches: only: - develop - - PM-3532 - - pm-2539 - - PS-511 - - PS-513-Hotfix # Production builds are exectuted only on tagged commits to the # master branch. diff --git a/src/common/prismaHelper.js b/src/common/prismaHelper.js index 50ea920..1eed63e 100644 --- a/src/common/prismaHelper.js +++ b/src/common/prismaHelper.js @@ -107,7 +107,7 @@ function buildMemberSkills (skillList) { ret.displayMode = _.pick(first.userSkillDisplayMode, ['id', 'name']) } - if (first.skill && first.skill.skillEvents) { + if (first.skill && first.skill.skillEvents?.length) { const events = _.orderBy(first.skill.skillEvents || [], 'createdAt', 'desc') const grouped = _.groupBy(events, 'sourceType.name') ret.lastUsedDate = events[0].createdAt diff --git a/src/common/profileTemplate.js b/src/common/profileTemplate.js index a0f21ea..e269998 100644 --- a/src/common/profileTemplate.js +++ b/src/common/profileTemplate.js @@ -77,7 +77,13 @@ const styles = StyleSheet.create({ statusBar: { backgroundColor: '#000000', padding: 8, - marginBottom: 20 + marginBottom: 10 + }, + statusBarSeparator: { + height: 1, + backgroundColor: '#AAAAAA', + marginTop: 10, + marginBottom: 10 }, statusBarText: { color: '#FFFFFF', @@ -174,7 +180,9 @@ const styles = StyleSheet.create({ }, itemSkills: { fontSize: 10, - marginTop: 2, + marginTop: 4, + marginBottom: 6, + lineHeight: 1.4, color: '#000000' }, bulletPoint: { @@ -184,16 +192,25 @@ const styles = StyleSheet.create({ lineHeight: 1.4, color: '#000000' }, - // Work description HTML list alignment (ul/ol/li) + // Work description HTML: tighten paragraph spacing so typed (p) and pasted (br) look consistent descriptionListStylesheet: { + p: { margin: 0, marginBottom: 2 }, ul: { paddingLeft: 15, marginTop: 3, marginBottom: 3 }, ol: { paddingLeft: 15, marginTop: 3, marginBottom: 3 }, li: { marginBottom: 2 } }, - // Certifications + // Certifications & Courses certificationItem: { fontSize: 10, - marginBottom: 3, + marginBottom: 8, + lineHeight: 1.5, + color: '#000000' + }, + courseItem: { + fontSize: 10, + marginTop: 10, + marginBottom: 8, + lineHeight: 1.5, color: '#000000' }, certificationLabel: { @@ -337,15 +354,17 @@ function buildProfileTemplate (pdfData) { { style: styles.handleInfo }, `Topcoder Handle: ${member.handle}${member.createdAt ? ` | Member Since ${new Date(member.createdAt).getFullYear()}` : ''}` ), - member.statusBarText ? React.createElement( - View, - { style: styles.statusBar }, - React.createElement( - Text, - { style: styles.statusBarText }, - member.statusBarText - ) - ) : null + member.statusBarText + ? React.createElement( + View, + { style: styles.statusBar }, + React.createElement( + Text, + { style: styles.statusBarText }, + member.statusBarText + ) + ) + : React.createElement(View, { style: styles.statusBarSeparator }) ) ) @@ -567,7 +586,7 @@ function buildProfileTemplate (pdfData) { certContent.push( React.createElement( Text, - { key: 'courses', style: [styles.certificationItem, { marginTop: 5 }] }, + { key: 'courses', style: styles.courseItem }, React.createElement(Text, { style: styles.certificationLabel }, 'Courses: '), coursesText ) diff --git a/src/services/MemberService.js b/src/services/MemberService.js index 9bfdd91..d6a1651 100644 --- a/src/services/MemberService.js +++ b/src/services/MemberService.js @@ -1756,22 +1756,86 @@ async function getMemberSkill (currentUser, handle, skillId) { if (skill.activity) { const fetchPromises = [] - // Prepare challenge fetch + // Prepare challenge fetch – group by resource role, last 3 per role by endDate const challengeSources = _.get(skill, 'activity.challenge.sources', []) if (challengeSources.length > 0) { const challengeIds = challengeSources + fetchPromises.push( - challengesPrisma.Challenge.findMany({ - where: { id: { in: challengeIds.slice(0, 3) } }, - select: { id: true, name: true } - }).then(dbChallenges => { + Promise.all([ + resourcesPrisma.resource.findMany({ + where: { + memberId: String(member.userId), + challengeId: { in: challengeIds } + }, + select: { challengeId: true, resourceRole: { select: { name: true } } } + }), + // Get challenge details (including endDate for ordering) from challenges DB + challengesPrisma.Challenge.findMany({ + where: { id: { in: challengeIds } }, + select: { + id: true, + name: true, + endDate: true, + taskIsTask: true, + winners: { + where: { userId: helper.bigIntToNumber(member.userId) }, + select: { userId: true } + } + } + }) + ]).then(([resources, dbChallenges]) => { + const roleMap = new Map() + resources.forEach(resource => { + if (!roleMap.has(resource.challengeId)) { + roleMap.set(resource.challengeId, new Set()) + } + roleMap.get(resource.challengeId).add(resource.resourceRole.name) + }) const challengeMap = new Map(dbChallenges.map(c => [c.id, c])) - skill.activity.challenge = { - count: challengeIds.length, - lastSources: challengeIds - .map(id => challengeMap.get(id)) - .filter(Boolean) + const winnerSet = new Set( + dbChallenges + .filter(c => c.winners && c.winners.length > 0) + .map(c => c.id) + ) + + // Group challenges by role + const groups = {} + for (const challengeId of challengeIds) { + const challenge = challengeMap.get(challengeId) + if (challenge) { + const roles = roleMap.get(challengeId) + const roleNames = roles && roles.size + ? Array.from(roles).map(role => ( + role === 'Submitter' && winnerSet.has(challengeId) + ? 'Winner' + : role + )) + : [challenge?.taskIsTask ? 'Task' : 'Unknown'] + + roleNames.forEach(roleName => { + if (!groups[roleName]) groups[roleName] = [] + groups[roleName].push(challenge) + }) + } } + + // For each role: sort by endDate desc, keep last 3, include total count + skill.activity.challenge = Object.fromEntries( + Object.entries(groups).map(([role, challenges]) => { + const sorted = challenges.sort((a, b) => + new Date(b.endDate || 0) - new Date(a.endDate || 0) + ) + return [role, { + count: sorted.length, + lastSources: sorted.slice(0, 3).map(c => ({ + id: c.id, + name: c.name, + role + })) + }] + }) + ) }) ) } diff --git a/src/services/MemberTraitService.js b/src/services/MemberTraitService.js index b1e22b7..699d54e 100644 --- a/src/services/MemberTraitService.js +++ b/src/services/MemberTraitService.js @@ -287,6 +287,21 @@ async function validateWorkAssociatedSkills (skillIds) { } } +/** + * Remove a field if it's provided as an empty/blank string. + * @param {Object} item object containing data fields + * @param {String} key field name + */ +function omitBlankStringField (item, key) { + if (!Object.prototype.hasOwnProperty.call(item, key)) { + return + } + + if (_.isString(item[key]) && _.trim(item[key]) === '') { + delete item[key] + } +} + /** * Build prisma data for creating/updating traits * @param {Object} data query data @@ -344,12 +359,20 @@ function buildTraitPrismaData (data, operatorId, result) { if (t.timePeriodTo && !t.endDate) { t.endDate = new Date(t.timePeriodTo) } + // industry is optional; treat blank values as omitted + omitBlankStringField(t, 'industry') + omitBlankStringField(t, 'otherIndustry') + if (t.industry !== 'Other') { + delete t.otherIndustry + } // Remove unknown keys that Prisma model does not accept delete t.company delete t.timePeriodFrom delete t.timePeriodTo return t }) + // Keep downstream response/event payloads aligned with normalized DB payload + item.traits.data = payload } _.forEach(payload, t => { diff --git a/test/unit/MemberTraitService.test.js b/test/unit/MemberTraitService.test.js index c37c73d..8634fef 100644 --- a/test/unit/MemberTraitService.test.js +++ b/test/unit/MemberTraitService.test.js @@ -214,6 +214,28 @@ describe('member trait service unit tests', () => { // should.equal(result[0].updatedBy, 'sub2') }) + it('update member traits successfully when industry is blank', async () => { + await service.updateTraits({ isMachine: true, sub: 'sub2' }, member1.handle, [{ + traitId: 'work', + categoryName: 'Work', + traits: { + traitId: 'work', + data: [{ + industry: ' ', + companyName: 'JP Morgan 3', + position: 'Manager 3' + }] + } + }]) + + const traits = await service.getTraits({}, member1.handle, { traitIds: 'work' }) + should.equal(traits.length, 1) + should.equal(traits[0].traitId, 'work') + should.equal(traits[0].traits.data.length, 1) + should.equal(traits[0].traits.data[0].companyName, 'JP Morgan 3') + should.not.equal(traits[0].traits.data[0].industry, '') + }) + it('update member traits - trait not found', async () => { try { await service.updateTraits({ isMachine: true, sub: 'sub1' }, member1.handle, [{