• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

atlp-rwanda / eagles-ec-fe / 10199464713

01 Aug 2024 01:39PM UTC coverage: 70.877% (+0.1%) from 70.752%
10199464713

push

github

web-flow
Merge pull request #39 from atlp-rwanda/fix-admin-dashboard

 fix:admin dashboard

280 of 541 branches covered (51.76%)

Branch coverage included in aggregate %.

9 of 19 new or added lines in 5 files covered. (47.37%)

4 existing lines in 3 files now uncovered.

1409 of 1842 relevant lines covered (76.49%)

8.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

47.66
/src/components/dashboard/admin/DataTable.tsx
1
import React, { useEffect, useState } from "react";
2✔
2
import {
2✔
3
  FaRegBell, FaShoppingCart, FaStore, FaUsers,
4
} from "react-icons/fa";
5
import "react-loading-skeleton/dist/skeleton.css";
2✔
6
import { ToastContainer, toast } from "react-toastify";
2✔
7
import axios from "axios";
2✔
8
import Pagination from "@mui/material/Pagination";
2✔
9
import {
2✔
10
  Table,
11
  TableBody,
12
  TableCell,
13
  TableContainer,
14
  TableHead,
15
  TableRow,
16
  Paper,
17
  Skeleton,
18
} from "@mui/material";
19
import { useNavigate } from "react-router-dom";
2✔
20

21
import api from "../../../redux/api/api";
2✔
22
import ToggleSwitch from "../ToggleSwitch";
2✔
23

24
import SearchFilterBar from "./SearchFilterBar";
2✔
25
import NumberCard from "./NumberCard";
2✔
26
import UserAvatar from "./UserAvatar";
2✔
27
import UserRoleSelect from "./UserRoleSelect";
2✔
28

29
interface User {
30
  id: number;
31
  name: string;
32
  email: string;
33
  roleId: number;
34
  isActive: boolean;
35
}
36

37
interface Role {
38
  id: number;
39
  name: string;
40
}
41

42
const DataTable: React.FC = () => {
2✔
43
  const [users, setUsers] = useState<User[]>([]);
2✔
44
  const [loading, setLoading] = useState<boolean>(true);
2✔
45
  const [currentPage, setCurrentPage] = useState<number>(1);
2✔
46
  const [rowsPerPage, setRowsPerPage] = useState<number>(5);
2✔
47
  const [roles, setRoles] = useState<Role[]>([]);
2✔
48
  const [searchTerm, setSearchTerm] = useState<string>("");
2✔
49
  const [filterRole, setFilterRole] = useState<string>("All");
2✔
50
  const [loadingAction, setLoadingAction] = useState<{
2✔
51
    [key: number]: boolean;
52
  }>({});
53
  const navigate = useNavigate();
2✔
54

55
  const fetchData = async () => {
2✔
56
    const authToken = localStorage.getItem("accessToken");
2✔
57

58
    try {
2✔
59
      const [usersResponse, rolesResponse] = await Promise.all([
2✔
60
        api.get("/users", {
61
          headers: {
62
            accept: "*/*",
63
            Authorization: `Bearer ${authToken}`,
64
          },
65
        }),
66
        api.get("/roles", {
67
          headers: {
68
            accept: "*/*",
69
            Authorization: `Bearer ${authToken}`,
70
          },
71
        }),
72
      ]);
73
      setUsers(sortUsersByEmail(usersResponse.data.users));
×
74
      setRoles(rolesResponse.data.roles);
×
75
    } catch (error: any) {
76
      if (error.response && error.response.status === 401) {
×
77
        toast.error(`Error fetching users or roles: ${error.message}`);
×
78
        navigate("/");
×
79
      } else {
80
        toast.error(`Error fetching users or roles: ${error.message}`);
×
81
      }
82
    } finally {
83
      setLoading(false);
×
84
    }
85
  };
86

87
  useEffect(() => {
2✔
88
    fetchData();
2✔
89
  }, []);
90

91
  const handleSearch = (term: string) => {
2✔
92
    setSearchTerm(term);
×
93
  };
94

95
  const handleFilter = (role: string) => {
2✔
96
    setFilterRole(role);
×
97
  };
98

99
  const sortUsersByEmail = (users: User[]) =>
2✔
NEW
100
    users.sort((a, b) => a.email.localeCompare(b.email));
×
101

102
  const toggleActiveStatus = async (id: number) => {
2✔
103
    const authToken = localStorage.getItem("accessToken");
×
104
    setLoadingAction((prevState) => ({ ...prevState, [id]: true }));
×
105
    try {
×
106
      const response = await axios.patch(
×
107
        `${process.env.VITE_BASE_URL}/users/${id}/status`,
108
        null,
109
        {
110
          headers: {
111
            accept: "*/*",
112
            Authorization: `Bearer ${authToken}`,
113
          },
114
        },
115
      );
NEW
116
      setUsers((prevUsers) =>
×
NEW
117
        prevUsers.map((user) =>
×
NEW
118
          (user.id === id ? { ...user, isActive: !user.isActive } : user)));
×
UNCOV
119
      toast.success("User status updated successfully");
×
120
    } catch (error: any) {
121
      toast.error(`Error toggling active status: ${error.message}`);
×
122
    } finally {
123
      setLoadingAction((prevState) => ({ ...prevState, [id]: false }));
×
124
    }
125
  };
126

127
  const handlePageChange = (
2✔
128
    event: React.ChangeEvent<unknown>,
129
    pageNumber: number,
130
  ) => {
131
    setCurrentPage(pageNumber);
×
132
  };
133

134
  const changeRowsPerPage = (e: React.ChangeEvent<{ value: unknown }>) => {
2✔
135
    const value = e.target.value as number;
×
136
    setRowsPerPage(value);
×
137
    setCurrentPage(1);
×
138
  };
139

140
  const handleRoleChange = async (id: number, newRole: number) => {
2✔
141
    const authToken = localStorage.getItem("accessToken");
×
142
    setLoadingAction((prevState) => ({ ...prevState, [id]: true }));
×
143
    try {
×
144
      const response = await api.patch(
×
145
        `/users/${id}/role`,
146
        { roleId: newRole },
147
        {
148
          headers: {
149
            Authorization: `Bearer ${authToken}`,
150
          },
151
        },
152
      );
153
      toast.success(response.data.message);
×
NEW
154
      setUsers((prevUsers) =>
×
NEW
155
        prevUsers.map((user) =>
×
NEW
156
          (user.id === id ? { ...user, roleId: newRole } : user)));
×
157
    } catch (error: any) {
158
      toast.error(`Error changing user role: ${error.message}`);
×
159
    } finally {
160
      setLoadingAction((prevState) => ({ ...prevState, [id]: false }));
×
161
    }
162
  };
163

164
  const filteredUsers = users.filter((user) => {
2✔
165
    const matchesSearchTerm = user.name.toLowerCase().includes(searchTerm.toLowerCase())
×
166
      || user.email.toLowerCase().includes(searchTerm.toLowerCase());
167
    const matchesFilterRole = filterRole === "All" || user.roleId === Number(filterRole);
×
168
    return matchesSearchTerm && matchesFilterRole;
×
169
  });
170

171
  const numberOfUsers = filteredUsers.length;
2✔
172
  const numberOfSellers = filteredUsers.filter(
2✔
173
    (user) => user.roleId === 2,
×
174
  ).length;
175
  const numberOfBuyers = filteredUsers.filter(
2✔
176
    (user) => user.roleId === 1,
×
177
  ).length;
178

179
  const indexOfLastUser = currentPage * rowsPerPage;
2✔
180
  const indexOfFirstUser = indexOfLastUser - rowsPerPage;
2✔
181
  const currentUsers = filteredUsers.slice(indexOfFirstUser, indexOfLastUser);
2✔
182
  const totalPages = Math.ceil(filteredUsers.length / rowsPerPage);
2✔
183

184
  const renderSkeletonRows = () =>
2✔
185
    Array.from({ length: rowsPerPage }).map((_, index) => (
2✔
186
      <TableRow
10✔
187
        key={index}
188
        className="bg-white border-b"
189
        data-testid="skeleton-loader"
190
      >
191
        <TableCell className="flex items-center">
192
          <Skeleton width={40} height={40} />
193
          <Skeleton className="ml-2" width="70%" />
194
        </TableCell>
195
        <TableCell>
196
          <Skeleton width={150} />
197
        </TableCell>
198
        <TableCell>
199
          <Skeleton width={100} />
200
        </TableCell>
201
        <TableCell>
202
          <Skeleton width={40} />
203
        </TableCell>
204
      </TableRow>
205
    ));
206

207
  return (
2✔
208
    <>
209
      <ToastContainer />
210
      <div className="flex flex-wrap justify-between px-2 py-4 ">
211
        <NumberCard
212
          title="Users"
213
          number={numberOfUsers}
214
          Logo={FaUsers}
215
          loading={loading}
216
        />
217
        <NumberCard
218
          title="Sellers"
219
          number={numberOfSellers}
220
          Logo={FaStore}
221
          loading={loading}
222
        />
223
        <NumberCard
224
          title="Buyers"
225
          number={numberOfBuyers}
226
          Logo={FaShoppingCart}
227
          loading={loading}
228
        />
229
      </div>
230
      <SearchFilterBar
231
        onSearch={handleSearch}
232
        onFilter={handleFilter}
233
        roles={roles}
234
      />
235
      <TableContainer
236
        component={Paper}
237
        className="relative overflow-x-auto shadow-md sm:rounded-lg"
238
      >
239
        <Table>
240
          <TableHead className="text-xs text-gray-700 uppercase bg-gray-100">
241
            <TableRow>
242
              <TableCell>Name</TableCell>
243
              <TableCell>Email</TableCell>
244
              <TableCell>Role</TableCell>
245
              <TableCell>Active</TableCell>
246
            </TableRow>
247
          </TableHead>
248
          <TableBody>
249
            {loading
1!
250
              ? renderSkeletonRows()
251
              : currentUsers.map((user: any) => (
252
                <TableRow key={user.id} className="bg-white border-b">
×
253
                  <TableCell className="flex items-center">
254
                    <UserAvatar
255
                      name={user.name}
256
                      imageUrl={user.imageUrl}
257
                      data-testid={`user-avatar-${user.id}`}
258
                    />
259
                  </TableCell>
260
                  <TableCell>{user.email}</TableCell>
261
                  <TableCell>
262
                    <UserRoleSelect
263
                      user={user}
264
                      roles={roles}
265
                      handleRoleChange={handleRoleChange}
266
                      loadingAction={loadingAction}
267
                      loading={loading}
268
                    />
269
                  </TableCell>
270
                  <TableCell>
271
                    <ToggleSwitch
272
                      checked={user.isActive}
273
                      onChange={() => toggleActiveStatus(user.id)}
×
274
                      data-testid={`toggle-${user.id}`}
275
                    />
276
                  </TableCell>
277
                </TableRow>
278
              ))}
279
          </TableBody>
280
        </Table>
281
        <div className="flex items-center justify-between p-4">
282
          <div className="flex items-center space-x-2">
283
            <label htmlFor="rows-per-page" className="text-sm">
284
              Rows per page:
285
            </label>
286
            <select
287
              id="rows-per-page"
288
              value={rowsPerPage}
289
              onChange={changeRowsPerPage}
290
              className="p-2 border rounded-md"
291
            >
292
              <option value={5}>5</option>
293
              <option value={10}>10</option>
294
              <option value={25}>25</option>
295
            </select>
296
          </div>
297
          <Pagination
298
            size="medium"
299
            count={totalPages}
300
            page={currentPage}
301
            onChange={handlePageChange}
302
            shape="rounded"
303
            sx={{
304
              "& .MuiPaginationItem-root.Mui-selected": {
305
                color: "white",
306
                backgroundColor: "#EB5757",
307
                "&:hover": {
308
                  backgroundColor: "#EB5757",
309
                },
310
              },
311
            }}
312
          />
313
        </div>
314
      </TableContainer>
315
    </>
316
  );
317
};
318

319
export default DataTable;
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc