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
4 changes: 4 additions & 0 deletions public/gamification-admin/bulk.sample.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
tc_handle,badge_id,badge_name,awarded_by,awarded_at
kirildev,858dafad-5fcd-4bc3-ab7c-849453d139ad,,kirildev,2022-08-15T07:25:43.187Z
jcori,bc39b152-e0e3-4984-962b-f1eba67229dd,TCO22 Development Finalist,,2022-08-15T07:25:43.187Z
amy_admin,bc39b152-e0e3-4984-962b-f1eba67229dd,TCO22 Development Finalist,,
1 change: 1 addition & 0 deletions src-ts/lib/form/form-groups/form-input/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './input-file-picker'
export * from './input-image-picker'
export * from './form-input-autcomplete-option.enum'
export * from './input-rating'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
@import "../../../../styles/includes";
@import "../../../../styles/variables";

.filePicker {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: $black-5;
border-radius: 4px;
width: 100%;
padding: $space-xxxxl;
position: relative;

@include ltemd {
width: 100%;
}

.fileName {
margin-bottom: $space-sm;
text-align: center;
}

.filePickerButton {
color: $turq-160;
}

.filePickerInput {
display: none;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// tslint:disable:no-null-keyword
import { createRef, Dispatch, FC, RefObject, SetStateAction, useEffect, useState } from 'react'

import { Button, useCheckIsMobile } from '../../../..'
import { InputValue } from '../../../form-input.model'

import styles from './InputFilePicker.module.scss'

interface InputFilePickerProps {
readonly fileConfig?: {
readonly acceptFileType?: string
readonly maxFileSize?: number
}
readonly name: string
readonly onChange: (fileList: FileList | null) => void
readonly value?: InputValue
}

const InputFilePicker: FC<InputFilePickerProps> = (props: InputFilePickerProps) => {

const isMobile: boolean = useCheckIsMobile()

const fileInputRef: RefObject<HTMLInputElement> = createRef<HTMLInputElement>()

const [files, setFiles]: [FileList | null, Dispatch<SetStateAction<FileList | null>>] = useState<FileList | null>(null)
const [fileName, setFileName]: [string | undefined, Dispatch<SetStateAction<string | undefined>>] = useState<string | undefined>()

useEffect(() => {
if (files && files.length) {
setFileName(files[0].name)
} else if (fileName) {
setFileName(undefined)
}
}, [
files,
fileName,
])

return (
<div className={styles.filePicker}>
{
fileName && <p className={styles.fileName}>{fileName}</p>
}
<Button
buttonStyle='secondary'
className={styles.filePickerButton}
label={fileName ? 'Clear' : 'Browse'}
onClick={() => {
if (fileName) {
setFiles(null)
props.onChange(null)
} else {
fileInputRef.current?.click()
}
}}
size={isMobile ? 'xs' : 'sm'}
/>
<input
name={props.name}
type={'file'}
accept={props.fileConfig?.acceptFileType || '*'}
className={styles.filePickerInput}
ref={fileInputRef}
onChange={event => {
setFiles(event.target.files)
props.onChange(event.target.files)
}}
size={props.fileConfig?.maxFileSize || Infinity}
/>
</div>
)
}

export default InputFilePicker
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as InputFilePicker } from './InputFilePicker'
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const AwardedMembersTab: FC<AwardedMembersTabProps> = (props: AwardedMembersTabP
const pageHandler: InfinitePageHandler<MemberBadgeAward> = useGetGameBadgeAssigneesPage(props.badge, sort)

useEffect(() => {
if (props.forceRefresh && pageHandler) {
if (props.forceRefresh && pageHandler && !pageHandler.isValidating) {
pageHandler.mutate()
}
}, [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ const BadgeDetailPage: FC = () => {
}
}

function onManualAssign(): void {
function onAssign(): void {
// refresh awardedMembers data
setForceAwardedMembersTabRefresh(true)
setActiveTab(BadgeDetailsTabViews.awardedMembers)
Expand All @@ -279,11 +279,14 @@ const BadgeDetailPage: FC = () => {
if (activeTab === BadgeDetailsTabViews.manualAward) {
activeTabElement = <ManualAwardTab
badge={badgeDetailsHandler.data as GameBadge}
onManualAssign={onManualAssign}
onManualAssign={onAssign}
/>
}
if (activeTab === BadgeDetailsTabViews.batchAward) {
activeTabElement = <BatchAwardTab />
activeTabElement = <BatchAwardTab
badge={badgeDetailsHandler.data as GameBadge}
onBatchAssign={onAssign}
/>
}

// show page loader if we fetching results
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,55 @@
@use '../../../../../lib/styles/typography';
@import "../../../../../lib/styles/variables/palette";
@import "../../../../../lib/styles/includes";

$error-line-height: 14px;

.tabWrap {
display: flex;
flex-direction: column;
padding-bottom: 260px;

.batchFormWrap {
display: grid;
grid-template-columns: 1fr 1fr;
gap: $space-xxxxl;
margin-top: $space-xxl;

@include ltemd {
grid-template-columns: 1fr;
}

.templateLink {
text-transform: uppercase;
color: $turq-160;
font-weight: $font-weight-bold;
margin-top: $space-lg;
display: inline-block;
}

.batchForm {
display: flex;
flex-direction: column;

.error {
display: flex;
align-items: center;
color: $red-100;
// extend body ultra small and override it
@extend .ultra-small;
line-height: $error-line-height;
margin-top: $space-xs;

svg {
@include icon-md;
fill: $red-100;
margin-right: $space-xs;
}
}

.actionsWrap {
margin-top: $space-xxl;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,92 @@
import { FC } from 'react'
// tslint:disable:no-null-keyword
import { Dispatch, FC, SetStateAction, useState } from 'react'

import { Button, IconSolid, InputFilePicker } from '../../../../../lib'
import { GameBadge } from '../../../game-lib'
import { BadgeAssignedModal } from '../../../game-lib/modals/badge-assigned-modal'
import { batchAssignRequestAsync } from '../badge-details.functions'

import styles from './BatchAwardTab.module.scss'
interface BatchAwardTabProps {
badge: GameBadge,
onBatchAssign: () => void
}

const BatchAwardTab: FC<BatchAwardTabProps> = (props: BatchAwardTabProps) => {

const [showBadgeAssigned, setShowBadgeAssigned]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(false)

const [files, setFiles]: [FileList | null, Dispatch<SetStateAction<FileList | null>>] = useState<FileList | null>(null)

const [errorText, setErrorText]: [string, Dispatch<SetStateAction<string>>] = useState<string>('')

function onFilePick(fileList: FileList | null): void {
if (fileList && fileList[0] && fileList[0].type !== 'text/csv') {
setErrorText('Only CSV files are allowed.')
} else {
setFiles(fileList)
setErrorText('')
}
}

function onAward(): void {
batchAssignRequestAsync(files?.item(0) as File)
.then(() => {
setShowBadgeAssigned(true)
setFiles(null)
})
.catch(e => {
let message: string = e.message
if (e.errors && e.errors[0] && e.errors[0].path === 'user_id') {
message = `CSV file contains duplicate data. There are members included already owning this badge.`
}
setErrorText(message)
})
}

const BatchAwardTab: FC = () => {
return (
<div className={styles.tabWrap}>
<h3>Batch Award</h3>
<div className={styles.batchFormWrap}>
<div>
<p>If you would like to assign multiple people to multiple badges, this area is for you. Download the template below, populate the file with your data, and upload that file to the right once completed.</p>
<a target={'_blank'} href='/gamification-admin/bulk.sample.csv' download='bulk.smaple.csv' className={styles.templateLink}>Download template CSV</a>
</div>
<div className={styles.batchForm}>
<InputFilePicker
fileConfig={{
acceptFileType: 'text/csv',
}}
name='batch-import-file'
onChange={onFilePick}
/>
{errorText && (
<div className={styles.error}>
<IconSolid.ExclamationIcon />
{errorText}
</div>
)}
<div className={styles.actionsWrap}>
<Button
buttonStyle='secondary'
label='Award'
className={styles.awardBtn}
disable={!files?.length}
onClick={onAward}
/>
</div>
</div>
</div>
{
showBadgeAssigned && <BadgeAssignedModal
badge={props.badge}
isOpen={showBadgeAssigned}
onClose={() => {
setShowBadgeAssigned(false)
props.onBatchAssign()
}}
/>
}
</div>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { GamificationConfig } from '../../game-config'
import { GameBadge } from '../../game-lib'

import { submitRequestAsync as submitBatchAssignRequestAsync } from './batch-assign-badge.store'
import { submitRequestAsync as submitBadgeAssingRequestAsync } from './manual-assign-badge.store'
import { submitRequestAsync as submitBadgeUpdateRequestAsync } from './update-badge.store'
import { UpdateBadgeRequest } from './updated-badge-request.model'
Expand All @@ -18,3 +19,7 @@ export function generateCSV(input: Array<Array<string | number>>): string {
export async function manualAssignRequestAsync(csv: string): Promise<any> {
return submitBadgeAssingRequestAsync(csv)
}

export async function batchAssignRequestAsync(batchFile: File): Promise<any> {
return submitBatchAssignRequestAsync(batchFile)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { EnvironmentConfig } from '../../../../config'
import { xhrPostAsync } from '../../../../lib'

export async function submitRequestAsync(batchFile: File): Promise<any> {
const url: string = `${EnvironmentConfig.API.V5}/gamification/badges/assign`

const form: any = new FormData()

// fill the form
form.append('file', batchFile)

return xhrPostAsync(url, form, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
}