Skip to content

Commit

Permalink
Merge pull request #9 from fga-eps-mds/qas
Browse files Browse the repository at this point in the history
feat#(71)/gerenciar-role
  • Loading branch information
fernandes-natanael authored Jul 31, 2024
2 parents 51541cb + a85fe86 commit 262011f
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 112 deletions.
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ services:
restart: on-failure
build: .
ports:
- "3000:3000"
- "4000:4000"
environment:
- NODE_ENV=development
volumes:
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"dev": "next dev -p 4000",
"format": "prettier --write \"src/**/*.tsx\" \"src/**/*.ts\" \"test/**/*.ts\"",
"build": "next build",
"start": "next start -p 3000",
"start": "next start -p 4000",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest --passWithNoTests --no-cache --runInBand",
"test:all": "npm run test -- --coverage --config jest.config.ts"
Expand All @@ -24,6 +24,7 @@
"axios": "^1.7.2",
"cookie": "^0.6.0",
"dotenv": "^16.4.5",
"glob": "^9.3.5",
"jsonwebtoken": "^9.0.2",
"next": "^14.2.5",
"next-auth": "^4.24.7",
Expand Down
130 changes: 100 additions & 30 deletions src/app/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -1,63 +1,133 @@
'use client';

import Layout from '../components/clientLayout';
import React, { useState, useEffect } from 'react';
import { Box } from '@mui/material';
import SearchBar from '../components/admin/SearchBar';
import UserTable from '../components/admin/UserTable';
import { getUsers } from '../services/apiService';
import SearchBar from '../components/admin/SearchBar';
import { getUsers, updateUserRole } from '@/services/user.service';
import { CircularProgress, Box, Dialog, DialogActions, DialogContent, DialogTitle, Button, Typography } from '@mui/material';

type User = {
id: number;
_id: string;
username: string;
email: string;
role: string;
};

export default function UserManagement() {
const Admin: React.FC = () => {
const [users, setUsers] = useState<User[]>([]);
const [filteredUsers, setFilteredUsers] = useState<User[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [selectedRole, setSelectedRole] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState<string>('');
const [dialogOpen, setDialogOpen] = useState<boolean>(false);

useEffect(() => {
const fetchUsers = async () => {
try {
const usersData = await getUsers();
setUsers(usersData);
} catch (error) {
console.error('Error fetching users:', error);
const response = await getUsers();
setUsers(response);
setFilteredUsers(response);
} catch (err) {
setError('Failed to fetch users');
} finally {
setLoading(false);
}
};

fetchUsers();
}, []);

const handleRoleChange = (user: User, newRole: string) => {
setUsers(
users.map((u) => (u.id === user.id ? { ...u, role: newRole } : u)),
useEffect(() => {
const lowercasedQuery = searchQuery.toLowerCase();
const newFilteredUsers = users.filter(user =>
user.username.toLowerCase().includes(lowercasedQuery) ||
user.email.toLowerCase().includes(lowercasedQuery)
);
};
setFilteredUsers(newFilteredUsers);
}, [searchQuery, users]);

const handleSearchChange = (query: string) => {
const handleSearch = (query: string) => {
setSearchQuery(query);
};

const filteredUsers = users.filter(
(user) =>
user.username.toLowerCase().includes(searchQuery.toLowerCase()) ||
user.email.toLowerCase().includes(searchQuery.toLowerCase()),
);
const handleMenuClick = (event: React.MouseEvent<HTMLButtonElement>, user: User) => {
setAnchorEl(event.currentTarget);
setSelectedUser(user);
};

const handleMenuClose = () => {
setAnchorEl(null);
setSelectedUser(null);
setSelectedRole(null);
};

const handleRoleSelect = (role: string) => {
setSelectedRole(role);
setDialogOpen(true);
};

const confirmRoleChange = async () => {
if (selectedUser && selectedRole) {
try {
await updateUserRole(selectedUser._id, selectedRole);
const updatedUsers = users.map(user =>
user._id === selectedUser._id ? { ...user, role: selectedRole } : user
);
setUsers(updatedUsers);
setFilteredUsers(updatedUsers);
handleMenuClose();
} catch (err) {
setError('Failed to update user role');
} finally {
setDialogOpen(false);
}
}
};

const cancelRoleChange = () => {
setDialogOpen(false);
};

if (loading) return <CircularProgress />;
if (error) return <div>{error}</div>;

return (
<Box className="flex min-h-screen">
<Box
className="flex-1 p-4"
sx={{ maxWidth: '800px', width: '100%', margin: '0 auto' }}
>
<Box sx={{ mb: 4 }}>
<SearchBar value={searchQuery} onChange={handleSearchChange} />
</Box>
<UserTable users={filteredUsers} onRoleChange={handleRoleChange} />
<Box sx={{ padding: 2, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Box sx={{ width: '100%', maxWidth: 800, marginBottom: 2 }}>
<SearchBar value={searchQuery} onChange={handleSearch} />
</Box>
<UserTable
users={filteredUsers}
anchorEl={anchorEl}
onMenuClick={handleMenuClick}
onMenuClose={handleMenuClose}
onRoleSelect={handleRoleSelect}
/>

{/* Dialog de confirmação */}
<Dialog open={dialogOpen} onClose={cancelRoleChange}>
<DialogTitle>Confirmar Alteração de Role</DialogTitle>
<DialogContent>
{selectedUser && selectedRole && (
<Typography variant="h6">
{`Tornar ${selectedUser.username} ${selectedRole}`}
</Typography>
)}
</DialogContent>
<DialogActions>
<Button onClick={cancelRoleChange} color="error">
Cancelar
</Button>
<Button onClick={confirmRoleChange} color="primary">
Confirmar
</Button>
</DialogActions>
</Dialog>
</Box>
);
}
};

export default Admin;
1 change: 0 additions & 1 deletion src/app/components/admin/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ interface SearchBarProps {
const SearchBar: React.FC<SearchBarProps> = ({ value, onChange }) => {
return (
<TextField
label=""
variant="outlined"
fullWidth
value={value}
Expand Down
132 changes: 54 additions & 78 deletions src/app/components/admin/UserTable.tsx
Original file line number Diff line number Diff line change
@@ -1,95 +1,71 @@
'use client';

import React, { useState, MouseEvent } from 'react';
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
IconButton,
Menu,
MenuItem,
} from '@mui/material';
import React from 'react';
import { Box, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, IconButton, Menu, MenuItem, Typography } from '@mui/material';
import MoreVertIcon from '@mui/icons-material/MoreVert';

type User = {
id: number;
_id: string;
username: string;
email: string;
role: string;
};

interface UserTableProps {
users: User[];
onRoleChange: (user: User, newRole: string) => void;
anchorEl: null | HTMLElement;
onMenuClick: (event: React.MouseEvent<HTMLButtonElement>, user: User) => void;
onMenuClose: () => void;
onRoleSelect: (role: string) => void;
}

const UserTable: React.FC<UserTableProps> = ({ users, onRoleChange }) => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [selectedUser, setSelectedUser] = useState<null | User>(null);

const handleMenuClick = (event: MouseEvent<HTMLElement>, user: User) => {
setAnchorEl(event.currentTarget);
setSelectedUser(user);
};

const handleMenuClose = () => {
setAnchorEl(null);
setSelectedUser(null);
};

const handleRoleChange = (newRole: string) => {
if (selectedUser) {
onRoleChange(selectedUser, newRole);
handleMenuClose();
}
};

const UserTable: React.FC<UserTableProps> = ({ users, anchorEl, onMenuClick, onMenuClose, onRoleSelect }) => {
return (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Username</TableCell>
<TableCell>Email</TableCell>
<TableCell>Role</TableCell>
<TableCell>Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{users.map((user) => (
<TableRow key={user.id}>
<TableCell>{user.username}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>{user.role}</TableCell>
<TableCell>
<IconButton onClick={(event) => handleMenuClick(event, user)}>
<MoreVertIcon />
</IconButton>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl) && selectedUser?.id === user.id}
onClose={handleMenuClose}
>
<MenuItem onClick={() => handleRoleChange('admin')}>
Admin
</MenuItem>
<MenuItem onClick={() => handleRoleChange('professor')}>
Professor
</MenuItem>
<MenuItem onClick={() => handleRoleChange('aluno')}>
Aluno
</MenuItem>
</Menu>
</TableCell>
<Box sx={{ width: '100%', maxWidth: 800 }}>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell sx={{ fontWeight: 'bold', textTransform: 'lowercase' }}>username</TableCell>
<TableCell sx={{ fontWeight: 'bold', textTransform: 'lowercase' }}>email</TableCell>
<TableCell sx={{ fontWeight: 'bold', textTransform: 'lowercase' }}>role</TableCell>
<TableCell></TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</TableHead>
<TableBody>
{users.map((user) => (
<TableRow key={user._id}>
<TableCell>{user.username}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>{user.role}</TableCell>
<TableCell>
<IconButton
onClick={(e) => onMenuClick(e, user)}
color="primary"
>
<MoreVertIcon />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>

<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={onMenuClose}
>
<MenuItem onClick={() => onRoleSelect('admin')}>
<Typography>Tornar Admin</Typography>
</MenuItem>
<MenuItem onClick={() => onRoleSelect('aluno')}>
<Typography>Tornar Aluno</Typography>
</MenuItem>
<MenuItem onClick={() => onRoleSelect('professor')}>
<Typography>Tornar Professor</Typography>
</MenuItem>
</Menu>
</Box>
);
};

Expand Down
23 changes: 23 additions & 0 deletions src/services/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,26 @@ export const googleCallback = async () => {
const response = await api.get('auth/google/callback');
return response;
};

export const getUsers = async () => {
try {
const response = await api.get('/users');
console.log('Users:', response.data);
return response.data;
} catch (error) {
console.error('Failed to fetch users:', error);
throw error;
}
};

export const updateUserRole = async (userId: string, newRole: string) => {
try {
const response = await api.patch(`/users/${userId}/role`, {
role: newRole,
});
return response.data;
} catch (error) {
console.error('Failed to update user role:', error);
throw error;
}
};

0 comments on commit 262011f

Please sign in to comment.