diff --git a/src-ts/lib/functions/xhr-functions/xhr.functions.ts b/src-ts/lib/functions/xhr-functions/xhr.functions.ts index 8df50e55e..2ea1d30f7 100644 --- a/src-ts/lib/functions/xhr-functions/xhr.functions.ts +++ b/src-ts/lib/functions/xhr-functions/xhr.functions.ts @@ -74,6 +74,8 @@ function interceptError(instance: AxiosInstance): void { // if there is server error message, then return it inside `message` property of error error.message = error?.response?.data?.message || error.message + // if there is server errors data, then return it inside `errors` property of error + error.errors = error?.response?.data?.errors return Promise.reject(error) } diff --git a/src-ts/tools/gamification-admin/game-config/gamification-config.model.ts b/src-ts/tools/gamification-admin/game-config/gamification-config.model.ts index c397769c1..0024cbf16 100644 --- a/src-ts/tools/gamification-admin/game-config/gamification-config.model.ts +++ b/src-ts/tools/gamification-admin/game-config/gamification-config.model.ts @@ -1,5 +1,6 @@ export interface GamificationConfigModel { ACCEPTED_BADGE_MIME_TYPES: string + CSV_HEADER: Array, MAX_BADGE_IMAGE_FILE_SIZE: number ORG_ID: string PAGE_SIZE: number diff --git a/src-ts/tools/gamification-admin/game-config/gamification.default.config.ts b/src-ts/tools/gamification-admin/game-config/gamification.default.config.ts index b35ada502..05a4f7b51 100644 --- a/src-ts/tools/gamification-admin/game-config/gamification.default.config.ts +++ b/src-ts/tools/gamification-admin/game-config/gamification.default.config.ts @@ -2,6 +2,7 @@ import { GamificationConfigModel } from './gamification-config.model' export const GamificationConfigDefault: GamificationConfigModel = { ACCEPTED_BADGE_MIME_TYPES: 'image/svg+xml,image/svg', + CSV_HEADER: ['tc_handle', 'badge_id'], MAX_BADGE_IMAGE_FILE_SIZE: 5000000, // 5mb in bytes ORG_ID: '6052dd9b-ea80-494b-b258-edd1331e27a3', PAGE_SIZE: 12, diff --git a/src-ts/tools/gamification-admin/game-lib/modals/badge-assigned-modal/BadgeAssignedModal.module.scss b/src-ts/tools/gamification-admin/game-lib/modals/badge-assigned-modal/BadgeAssignedModal.module.scss new file mode 100644 index 000000000..abd8dc340 --- /dev/null +++ b/src-ts/tools/gamification-admin/game-lib/modals/badge-assigned-modal/BadgeAssignedModal.module.scss @@ -0,0 +1,54 @@ +@import "../../../../../lib/styles/variables"; +@import "../../../../../lib/styles/includes"; + +.wrapper { + display: flex; + flex-direction: column; + justify-content: space-between; + + .badge { + display: flex; + align-items: center; + margin-bottom: $space-xxl; + + @include ltemd { + margin-bottom: 0; + } + + .badge-image { + width: 43px; + height: 43px; + margin-right: $space-xl; + } + + .badge-image-disabled { + width: 43px; + height: 43px; + margin-right: $space-xl; + opacity: 0.5; + filter: grayscale(1); + } + + .badge-name { + font-size: 16px; + } + } + + .actions-wrap { + display: flex; + flex-direction: column; + + .actions { + display: flex; + align-items: center; + + @include ltemd { + justify-content: flex-end; + } + + a { + margin-right: $space-md; + } + } + } +} \ No newline at end of file diff --git a/src-ts/tools/gamification-admin/game-lib/modals/badge-assigned-modal/BadgeAssignedModal.tsx b/src-ts/tools/gamification-admin/game-lib/modals/badge-assigned-modal/BadgeAssignedModal.tsx new file mode 100644 index 000000000..5cce15d7f --- /dev/null +++ b/src-ts/tools/gamification-admin/game-lib/modals/badge-assigned-modal/BadgeAssignedModal.tsx @@ -0,0 +1,55 @@ +import { FC } from 'react' + +import { BaseModal, Button, PageDivider, useCheckIsMobile } from '../../../../../lib' +import { GameBadge } from '../../game-badge.model' + +import styles from './BadgeAssignedModal.module.scss' +export interface BadgeAssignedModalProps { + badge: GameBadge + isOpen: boolean + onClose: () => void +} + +const BadgeAssignedModal: FC = (props: BadgeAssignedModalProps) => { + + const isMobile: boolean = useCheckIsMobile() + + function onClose(): void { + props.onClose() + } + + return ( + +
+
+ {props.badge.badge_name} +

{props.badge.badge_name} badge has been sucessfully awarded.

+
+
+ { + isMobile && + } +
+
+
+
+
+ ) +} + +export default BadgeAssignedModal diff --git a/src-ts/tools/gamification-admin/game-lib/modals/badge-assigned-modal/index.ts b/src-ts/tools/gamification-admin/game-lib/modals/badge-assigned-modal/index.ts new file mode 100644 index 000000000..7f2b04a0b --- /dev/null +++ b/src-ts/tools/gamification-admin/game-lib/modals/badge-assigned-modal/index.ts @@ -0,0 +1 @@ +export { default as BadgeAssignedModal } from './BadgeAssignedModal' diff --git a/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/AwardedMembersTab.tsx b/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/AwardedMembersTab.tsx index faea0f664..eff8fe2ff 100644 --- a/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/AwardedMembersTab.tsx +++ b/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/AwardedMembersTab.tsx @@ -5,7 +5,7 @@ import { GameBadge } from '../../../game-lib' import styles from './AwardedMembersTab.module.scss' export interface AwardedMembersTabProps { - awardedMembers?: GameBadge['member_badges'] + badge: GameBadge } const AwardedMembersTab: FC = (props: AwardedMembersTabProps) => { diff --git a/src-ts/tools/gamification-admin/pages/badge-detail/BadgeDetailPage.tsx b/src-ts/tools/gamification-admin/pages/badge-detail/BadgeDetailPage.tsx index aa79d15f9..1b092176d 100644 --- a/src-ts/tools/gamification-admin/pages/badge-detail/BadgeDetailPage.tsx +++ b/src-ts/tools/gamification-admin/pages/badge-detail/BadgeDetailPage.tsx @@ -193,11 +193,11 @@ const BadgeDetailPage: FC = () => { // default tab let activeTabElement: JSX.Element - = + = if (activeTab === BadgeDetailsTabViews.manualAward) { - activeTabElement = + activeTabElement = } if (activeTab === BadgeDetailsTabViews.batchAward) { activeTabElement = diff --git a/src-ts/tools/gamification-admin/pages/badge-detail/ManualAwardTab/ManualAwardTab.tsx b/src-ts/tools/gamification-admin/pages/badge-detail/ManualAwardTab/ManualAwardTab.tsx index 4864655ad..284ad8f8d 100644 --- a/src-ts/tools/gamification-admin/pages/badge-detail/ManualAwardTab/ManualAwardTab.tsx +++ b/src-ts/tools/gamification-admin/pages/badge-detail/ManualAwardTab/ManualAwardTab.tsx @@ -1,14 +1,17 @@ +import { find } from 'lodash' import { Dispatch, FC, SetStateAction, useState } from 'react' import { Button } from '../../../../../lib' import { InputHandleAutocomplete } from '../../../../../lib/member-autocomplete' import { MembersAutocompeteResult } from '../../../../../lib/member-autocomplete/input-handle-functions' import { GameBadge } from '../../../game-lib' +import { BadgeAssignedModal } from '../../../game-lib/modals/badge-assigned-modal' +import { generateCSV, manualAssignRequestAsync } from '../badge-details.functions' import styles from './ManualAwardTab.module.scss' export interface ManualAwardTabProps { - awardedMembers?: GameBadge['member_badges'] + badge: GameBadge } const ManualAwardTab: FC = (props: ManualAwardTabProps) => { @@ -16,8 +19,28 @@ const ManualAwardTab: FC = (props: ManualAwardTabProps) => const [selectedMembers, setSelectedMembers]: [Array, Dispatch>>] = useState>([]) + const [showBadgeAssigned, setShowBadgeAssigned]: [boolean, Dispatch>] = useState(false) + + const [badgeAssignError, setBadgeAssignError]: [string | undefined, Dispatch>] = useState() + function onAward(): void { - setSelectedMembers([]) + const csv: string = generateCSV( + selectedMembers.map(m => [m.handle, props.badge?.id as string]) + ) + setBadgeAssignError(undefined) + manualAssignRequestAsync(csv) + .then(() => { + setShowBadgeAssigned(true) + setSelectedMembers([]) + }) + .catch(e => { + let message: string = e.message + if (e.errors && e.errors[0] && e.errors[0].path === 'user_id') { + const handleOrId: string = find(selectedMembers, { userId: e.errors[0].value })?.handle || e.errors[0].value + message = `Member ${handleOrId} alredy owns this badge.` + } + setBadgeAssignError(message) + }) } return ( @@ -33,6 +56,8 @@ const ManualAwardTab: FC = (props: ManualAwardTabProps) => onChange={setSelectedMembers} tabIndex={0} value={selectedMembers} + error={badgeAssignError} + dirty={!!badgeAssignError} />
+ { + showBadgeAssigned && { + setShowBadgeAssigned(false) + }} + /> + } ) } diff --git a/src-ts/tools/gamification-admin/pages/badge-detail/badge-details.functions.ts b/src-ts/tools/gamification-admin/pages/badge-detail/badge-details.functions.ts index 20ceae6ec..27b8ba585 100644 --- a/src-ts/tools/gamification-admin/pages/badge-detail/badge-details.functions.ts +++ b/src-ts/tools/gamification-admin/pages/badge-detail/badge-details.functions.ts @@ -1,8 +1,20 @@ +import { GamificationConfig } from '../../game-config' import { GameBadge } from '../../game-lib' +import { submitRequestAsync as submitBadgeAssingRequestAsync } from './manual-assign-badge.store' import { submitRequestAsync as submitBadgeUpdateRequestAsync } from './update-badge.store' import { UpdateBadgeRequest } from './updated-badge-request.model' export async function submitRequestAsync(request: UpdateBadgeRequest): Promise { return submitBadgeUpdateRequestAsync(request) } + +export function generateCSV(input: Array>): string { + input.unshift(GamificationConfig.CSV_HEADER) + + return input.map(row => row.join(',')).join('\n') +} + +export async function manualAssignRequestAsync(csv: string): Promise { + return submitBadgeAssingRequestAsync(csv) +} diff --git a/src-ts/tools/gamification-admin/pages/badge-detail/manual-assign-badge.store.ts b/src-ts/tools/gamification-admin/pages/badge-detail/manual-assign-badge.store.ts new file mode 100644 index 000000000..ae0cbc545 --- /dev/null +++ b/src-ts/tools/gamification-admin/pages/badge-detail/manual-assign-badge.store.ts @@ -0,0 +1,13 @@ +import { EnvironmentConfig } from '../../../../config' +import { xhrPostAsync } from '../../../../lib' + +export async function submitRequestAsync(csv: string): Promise { + const url: string = `${EnvironmentConfig.API.V5}/gamification/badges/assign` + + const form: any = new FormData() + + // fill the form + form.append('file', new Blob([csv], { type: 'text/csv' }), 'data.csv') + + return xhrPostAsync(url, form) +}