Skip to content

Commit 87a89e3

Browse files
committed
Added BulkRatingModal
1 parent fe125aa commit 87a89e3

File tree

12 files changed

+856
-27
lines changed

12 files changed

+856
-27
lines changed

package-lock.json

Lines changed: 61 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
"url-polyfill": "^1.1.12",
115115
"vite": "^5.4.7",
116116
"vite-tsconfig-paths": "^5.0.1",
117+
"xlsx": "^0.18.5",
117118
"zod": "^3.23.8"
118119
},
119120
"devDependencies": {

src/Mutations/Ratings.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,55 @@ export const REJECT_RATING = gql`
149149
rejectRating(user: $user, sprint: $sprint)
150150
}
151151
`;
152+
153+
export const GET_RATINGS_BY_USER_COHORT = gql`
154+
query getRatingsByUserCohort($orgToken: String!){
155+
getRatingsByUserCohort(orgToken: $orgToken){
156+
id
157+
sprint
158+
}
159+
}
160+
`
161+
162+
export const ADD_RATINGS_BY_FILE = gql`
163+
mutation addRatingsByFile($file: Upload!, $sprint: Int!, $orgToken: String!){
164+
addRatingsByFile(file: $file, sprint: $sprint orgToken: $orgToken){
165+
NewRatings {
166+
user {
167+
email
168+
}
169+
sprint
170+
phase
171+
quality
172+
quantity
173+
professional_Skills
174+
feedbacks {
175+
sender {
176+
email
177+
}
178+
content
179+
createdAt
180+
}
181+
cohort {
182+
name
183+
}
184+
}
185+
RejectedRatings{
186+
email
187+
quantity
188+
quality
189+
professional_skills
190+
feedBacks
191+
}
192+
UpdatedRatings {
193+
quantity
194+
quality
195+
professional_Skills
196+
feedbacks {
197+
content
198+
}
199+
oldFeedback
200+
}
201+
}
202+
}
203+
`

src/Mutations/teamMutation.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,15 @@ export const DeleteTeam = gql`
5353
}
5454
`;
5555

56+
export const GET_TEAMS_BY_USER_ROLE = gql`
57+
query getTeamsByUserRole($orgToken: String!) {
58+
getTeamsByUserRole(orgToken: $orgToken){
59+
id
60+
name
61+
members {
62+
email
63+
role
64+
}
65+
}
66+
}
67+
`

src/components/BulkRatingModal.tsx

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import { useLazyQuery, useMutation } from "@apollo/client"
2+
import React, { useEffect, useRef, useState } from "react"
3+
import { useTranslation } from "react-i18next"
4+
import * as XLSX from "xlsx"
5+
import { ADD_RATINGS_BY_FILE, GET_RATINGS_BY_USER_COHORT } from "../Mutations/Ratings"
6+
import { toast } from "react-toastify"
7+
import { GET_TEAMS_BY_USER_ROLE } from "../Mutations/teamMutation"
8+
9+
type BulkRatingModalProps = {
10+
bulkRateModal: boolean,
11+
setBulkRateModal: React.Dispatch<React.SetStateAction<boolean>>
12+
}
13+
14+
type AddRatingsByFileFormData = {
15+
sprint: string,
16+
file: File | null,
17+
}
18+
19+
const BulkRatingModal = ({ bulkRateModal, setBulkRateModal }: BulkRatingModalProps) => {
20+
const { t } = useTranslation()
21+
const [getRatingsByUserCohort, { data: ratings, loading: loadingRatings, error: ratingsError }] = useLazyQuery(GET_RATINGS_BY_USER_COHORT, {
22+
variables: {
23+
orgToken: localStorage.getItem('orgToken')
24+
},
25+
fetchPolicy: 'network-only',
26+
})
27+
const [getTeamsByUserRole, {data: teams, loading: loadingTeams, error: teamsError}] = useLazyQuery(GET_TEAMS_BY_USER_ROLE, {
28+
variables: {
29+
orgToken: localStorage.getItem('orgToken')
30+
},
31+
fetchPolicy: 'network-only',
32+
})
33+
const [addRatingsByFile, { data: bulkRatings, loading: loadingBulkRatings, error: bulkRatingsError }] = useMutation(ADD_RATINGS_BY_FILE)
34+
const [formData, setFormData] = useState<AddRatingsByFileFormData>({
35+
sprint: '',
36+
file: null
37+
})
38+
const [selectedTeam, setSelectedTeam] = useState<string>('')
39+
const fileUploadRef = useRef<HTMLInputElement>(null)
40+
41+
const saveRatings = async (e: React.FormEvent) => {
42+
try {
43+
e.preventDefault()
44+
if(!formData.sprint) throw new Error("Please select a sprint")
45+
if(!formData.file) throw new Error("Please select a file")
46+
await addRatingsByFile({
47+
variables: {
48+
file: formData.file,
49+
sprint: parseInt(formData.sprint, 10),
50+
orgToken: localStorage.getItem('orgToken')
51+
},
52+
})
53+
getRatingsByUserCohort()
54+
toast.success("Rating completed successfully")
55+
if(fileUploadRef.current){
56+
fileUploadRef.current.value = ''
57+
setFormData({...formData, file: null})
58+
}
59+
} catch (err: any) {
60+
toast.error(err?.message)
61+
}
62+
}
63+
64+
const downloadTeamFile = async(e: any)=>{
65+
try{
66+
if(selectedTeam === '') throw new Error("No Team was selected")
67+
const team = teams.getTeamsByUserRole.find((team:any)=>team.id === selectedTeam)
68+
const rows: any = []
69+
team.members.forEach((member: any) => {
70+
if (member.role === "trainee") {
71+
rows.push({
72+
email: member.email,
73+
quantity: '',
74+
quality: '',
75+
professional_skills: '',
76+
feedBacks: ''
77+
})
78+
}
79+
})
80+
const workSheet = rows.length ? XLSX.utils.json_to_sheet(rows) : XLSX.utils.json_to_sheet([{
81+
email: '',
82+
quantity: '',
83+
quality: '',
84+
professional_skills:'',
85+
feedBacks: ''
86+
}])
87+
const workBook = XLSX.utils.book_new()
88+
workSheet["!cols"] = [ { wch: 20 } ]
89+
XLSX.utils.book_append_sheet(workBook, workSheet,"ratings")
90+
XLSX.writeFile(workBook, `${team.name.replace(' ','')}_Ratings.xlsx`)
91+
}catch(err: any){
92+
toast.error(err?.message)
93+
}
94+
}
95+
96+
useEffect(() => {
97+
getRatingsByUserCohort()
98+
getTeamsByUserRole()
99+
}, [])
100+
101+
return (
102+
<div className={`${bulkRateModal ? "block" : "hidden"} h-screen w-screen z-20 bg-black bg-opacity-30 backdrop-blur-sm fixed top-0 left-0 flex items-center justify-center px-4`}>
103+
<div className="w-full p-4 pb-8 bg-indigo-100 rounded-lg dark:bg-dark-bg sm:w-3/4 xl:w-4/12">
104+
<div className="flex flex-wrap items-center justify-center w-full card-title">
105+
<h3 className="w-11/12 text-sm font-bold text-center dark:text-white">
106+
{t('Bulk Rating')}
107+
</h3>
108+
<hr className="w-full my-3 border-b bg-primary" />
109+
</div>
110+
<div>
111+
<form data-testid="bulk-rating-form" className="flex flex-col gap-5" onSubmit={saveRatings}>
112+
<div className="flex flex-col gap-1">
113+
<label>Choose a sprint</label>
114+
<select data-testid="select-sprint" className="p-2 text-black dark:text-white rounded-lg bg-white dark:bg-dark border-2 border-primary"
115+
defaultValue={""}
116+
onChange={(e) => {
117+
e.preventDefault()
118+
setFormData({ ...formData, sprint: e.target.value })
119+
}}
120+
>
121+
<option>Choose a sprint</option>
122+
{
123+
ratings && !ratings.getRatingsByUserCohort.length ?
124+
<option data-testid="sprint-default-option" value={1}>Sprint 1</option>
125+
: ''
126+
}
127+
{
128+
ratings && ratings.getRatingsByUserCohort.length ?
129+
[...ratings.getRatingsByUserCohort].map((rating: any) =>
130+
<option data-testid={`sprint-option-${rating.id}`} key={rating.id} value={rating.sprint}>Sprint {rating.sprint}</option>
131+
)
132+
: ''
133+
}
134+
{
135+
ratings && ratings.getRatingsByUserCohort.length ?
136+
<option data-testid="sprint-new-option" value={[...ratings.getRatingsByUserCohort].pop().sprint+1}>Sprint {[...ratings.getRatingsByUserCohort].pop().sprint+1}</option>
137+
: ''
138+
}
139+
{
140+
loadingRatings ?
141+
<option data-testid="sprint-loading-option">Loading...</option>
142+
: ''
143+
}
144+
{
145+
ratingsError ?
146+
<option data-testid="sprint-error-option">No sprints found...</option>
147+
: ''
148+
}
149+
</select>
150+
</div>
151+
<div className="flex items-center justify-between">
152+
<input
153+
data-testid="file-input"
154+
className="w-1/2 h-full bg-gray-600 rounded-md"
155+
type="file"
156+
ref={fileUploadRef}
157+
onChange={(e) => {
158+
const file = e.target.files?.[0]
159+
setFormData({ ...formData, file: file ? file : null })
160+
}}
161+
accept=".xlsx, .xls"
162+
>
163+
</input>
164+
<div className="flex gap-2">
165+
<select data-testid="select-team" className="p-2 text-sm text-black dark:text-white rounded-lg bg-white dark:bg-dark border-2 border-primary" defaultValue={""} onChange={(e)=>setSelectedTeam(e.target.value)}>
166+
<option data-testid="team-default-option">Choose a team</option>
167+
{
168+
teams && teams.getTeamsByUserRole.length > 0 ?
169+
teams.getTeamsByUserRole.map((team: any)=><option data-testid={`team-option-${team.id}`} key={team.id} value={team.id}>{team.name}</option>)
170+
: ''
171+
}
172+
{
173+
loadingTeams ?
174+
<option>Loading...</option>
175+
: ''
176+
}
177+
{
178+
teamsError ?
179+
<option>No teams found...</option>
180+
: ''
181+
}
182+
</select>
183+
<button data-testid="download-button" type="button" onClick={downloadTeamFile} className="p-3 text-white rounded-lg bg-green-500 text-sm font-serif font-semibold">Download</button>
184+
</div>
185+
</div>
186+
187+
<div>
188+
{
189+
bulkRatings && bulkRatings.addRatingsByFile.RejectedRatings.length > 0 ?
190+
<div className="my-1 overflow-x-auto">
191+
<table className="table-fixed min-w-full">
192+
<caption className="caption-top text-left my-2">
193+
Rejected Ratings
194+
</caption>
195+
<thead className="border-b bg-neutral-700 border-neutral-400">
196+
<tr>
197+
<th scope="col" className="text-left py-4 px-2">Email</th>
198+
<th scope="col" className="text-left py-4 px-2">Quantity</th>
199+
<th scope="col" className="text-left py-4 px-2">Quality</th>
200+
<th scope="col" className="text-left py-4 px-2">Professional_Skills</th>
201+
<th scope="col" className="text-left py-4 px-2">Feedback</th>
202+
</tr>
203+
</thead>
204+
<tbody>
205+
{bulkRatings.addRatingsByFile?.RejectedRatings.map((rating: any, index: number) =>
206+
<tr key={`${Math.random() * Date.now()}`} className="text-red-400">
207+
<td className="text-left py-1 px-2">{rating.email ? rating.email : "No Value"}</td>
208+
<td className="text-left py-1 px-2">{rating.quantity !== null ? rating.quantity : "No Value"}</td>
209+
<td className="text-left py-1 px-2">{rating.quality !== null ? rating.quality : "No Value"}</td>
210+
<td className="text-left py-1 px-2">{rating.professional_skills !== null ? rating.professional_skills : "No Value"}</td>
211+
<td className="text-left py-1 px-2">{rating.feedBacks ? rating.feedBacks : "No Value"}</td>
212+
</tr>
213+
)}
214+
</tbody>
215+
</table>
216+
</div>
217+
: ''
218+
}
219+
</div>
220+
<div className="flex justify-between w-full">
221+
<button className="w-[40%] md:w-1/4 p-3 text-white rounded-lg bg-primary text-sm font-serif font-semibold" type="button" onClick={() => setBulkRateModal(false)}>
222+
Cancel
223+
</button>
224+
<button className="w-[40%] md:w-1/4 p-3 text-white rounded-lg bg-primary text-sm font-serif font-semibold" type="submit">
225+
Save
226+
</button>
227+
</div>
228+
</form>
229+
</div>
230+
</div>
231+
</div>
232+
)
233+
}
234+
235+
export default BulkRatingModal

0 commit comments

Comments
 (0)