diff --git a/src/api/users.ts b/src/api/users.ts index a8bfb86..f2f1eab 100644 --- a/src/api/users.ts +++ b/src/api/users.ts @@ -8,8 +8,8 @@ import { apiClient } from './client'; export interface User { id: string; email: string; - first_name: string; - last_name: string; + name: string; + avatar_url: string | null; role_id: string; role?: { id: string; @@ -26,17 +26,15 @@ export interface User { export interface CreateUserInput { email: string; password: string; - first_name: string; - last_name: string; - role_id: number; + name: string; + role_id: string; is_active?: boolean; } export interface UpdateUserInput { email?: string; - first_name?: string; - last_name?: string; - role_id?: number; + name?: string; + role_id?: string; is_active?: boolean; } diff --git a/src/pages/UsersPage.tsx b/src/pages/UsersPage.tsx index c9df8b5..fc92999 100644 --- a/src/pages/UsersPage.tsx +++ b/src/pages/UsersPage.tsx @@ -1,8 +1,13 @@ import { useState, useEffect } from "react"; import { Plus, Trash2, Pencil, RefreshCcw } from "lucide-react"; import MaterialTable from "@material-table/core"; +import { createUser, updateUser, deleteUser, getAllUsers, CreateUserInput, UpdateUserInput, User as ApiUser } from "../api/users"; import { getAllRoles, Role as ApiRole } from "../api/roles"; -import { createUser, getAllUsers, CreateUserInput, User as ApiUser } from "../api/users"; + +interface RoleOption { + id: string; + name: string; +} interface User { id: string; @@ -15,8 +20,7 @@ interface User { } interface UserForm { - firstName: string; - lastName: string; + name: string; email: string; password?: string; roleId: string; @@ -28,69 +32,66 @@ export default function UsersPage() { const [users, setUsers] = useState([]); const [activeUser, setActiveUser] = useState(null); const [search, setSearch] = useState(""); + const [selectedRoleFilter, setSelectedRoleFilter] = useState(""); // Filter state const [showModal, setShowModal] = useState(false); const [editingId, setEditingId] = useState(null); - const [roles, setRoles] = useState([]); - const [loadingRoles, setLoadingRoles] = useState(true); + const [roles, setRoles] = useState([]); + const [modalRoles, setModalRoles] = useState([]); const [loadingUsers, setLoadingUsers] = useState(true); + const [loadingModalRoles, setLoadingModalRoles] = useState(false); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); - const emptyUser: UserForm = { firstName: "", lastName: "", email: "", roleId: "", password: "", status: "ACTIVE", createdAt: new Date().toISOString().slice(0,10) }; + const emptyUser: UserForm = { name: "", email: "", roleId: "", password: "", status: "ACTIVE", createdAt: new Date().toISOString().slice(0,10) }; const [form, setForm] = useState(emptyUser); - // Fetch roles and users from API on component mount useEffect(() => { - const fetchRoles = async () => { - try { - setLoadingRoles(true); - const rolesData = await getAllRoles(); - console.log('Roles fetched:', rolesData); - setRoles(rolesData); - } catch (error) { - console.error('Failed to fetch roles:', error); - } finally { - setLoadingRoles(false); - } - }; - - const fetchUsers = async () => { - try { - setLoadingUsers(true); - const usersResponse = await getAllUsers(); - console.log('Users API response:', usersResponse); - - // Map API users to UI format - const mappedUsers: User[] = usersResponse.data.map((apiUser: ApiUser) => ({ - id: apiUser.id, - name: `${apiUser.first_name} ${apiUser.last_name}`, - email: apiUser.email, - roleId: apiUser.role_id, - roleName: apiUser.role?.name || '', - status: apiUser.is_active ? "ACTIVE" : "INACTIVE", - createdAt: new Date(apiUser.created_at).toISOString().slice(0, 10) - })); - - console.log('Mapped users:', mappedUsers); - setUsers(mappedUsers); - } catch (error) { - console.error('Failed to fetch users:', error); - // Keep empty array on error - setUsers([]); - } finally { - setLoadingUsers(false); - } - }; - - fetchRoles(); fetchUsers(); }, []); + const fetchUsers = async () => { + try { + setLoadingUsers(true); + const usersResponse = await getAllUsers(); + console.log('Users API response:', usersResponse); + + const mappedUsers: User[] = usersResponse.data.map((apiUser: ApiUser) => ({ + id: apiUser.id, + name: apiUser.name, + email: apiUser.email, + roleId: apiUser.role_id, + roleName: apiUser.role?.name || '', + status: apiUser.is_active ? "ACTIVE" : "INACTIVE", + createdAt: new Date(apiUser.created_at).toISOString().slice(0, 10) + })); + + setUsers(mappedUsers); + + const uniqueRolesMap = new Map(); + usersResponse.data.forEach((apiUser: ApiUser) => { + if (apiUser.role) { + uniqueRolesMap.set(apiUser.role.id, { + id: apiUser.role.id, + name: apiUser.role.name + }); + } + }); + const uniqueRoles = Array.from(uniqueRolesMap.values()); + console.log('Unique roles extracted:', uniqueRoles); + setRoles(uniqueRoles); + } catch (error) { + console.error('Failed to fetch users:', error); + setUsers([]); + setRoles([]); + } finally { + setLoadingUsers(false); + } + }; + const handleSave = async () => { setError(null); - // Validation - if (!form.firstName || !form.lastName || !form.email || !form.roleId) { + if (!form.name || !form.email || !form.roleId) { setError("Please fill in all required fields"); return; } @@ -109,52 +110,28 @@ export default function UsersPage() { setSaving(true); if (editingId) { - // TODO: Implement update user - const roleName = roles.find(r => r.id === form.roleId)?.name || ""; - const fullName = `${form.firstName} ${form.lastName}`; - setUsers(prev => prev.map(u => u.id === editingId ? { - id: editingId, - name: fullName, + const updateData: UpdateUserInput = { email: form.email, - roleId: form.roleId, - roleName, - status: form.status, - createdAt: form.createdAt - } : u)); - } else { - // Create new user - const roleIdNum = parseInt(form.roleId, 10); - - if (isNaN(roleIdNum)) { - setError("Please select a valid role"); - return; - } - - const createData: CreateUserInput = { - email: form.email, - password: form.password!, - first_name: form.firstName, - last_name: form.lastName, - role_id: roleIdNum, + name: form.name.trim(), + role_id: form.roleId, is_active: form.status === "ACTIVE", }; - const newUser = await createUser(createData); - const roleName = roles.find(r => r.id === form.roleId)?.name || ""; - - // Add the new user to the list - const apiUser: ApiUser = newUser; - setUsers(prev => [...prev, { - id: apiUser.id, - name: `${apiUser.first_name} ${apiUser.last_name}`, - email: apiUser.email, - roleId: apiUser.role_id, - roleName: roleName, - status: apiUser.is_active ? "ACTIVE" : "INACTIVE", - createdAt: new Date(apiUser.created_at).toISOString().slice(0, 10) - }]); + await updateUser(editingId, updateData); + } else { + const createData: CreateUserInput = { + email: form.email, + password: form.password!, + name: form.name.trim(), + role_id: form.roleId, + is_active: form.status === "ACTIVE", + }; + + await createUser(createData); } + await handleRefresh(); + setShowModal(false); setEditingId(null); setForm(emptyUser); @@ -167,44 +144,100 @@ export default function UsersPage() { }; const handleRefresh = async () => { + await fetchUsers(); + }; + + const handleDelete = async () => { + if (!activeUser) return; + + if (!window.confirm(`Are you sure you want to delete user "${activeUser.name}"?`)) { + return; + } + try { - setLoadingUsers(true); - const usersResponse = await getAllUsers(); - const mappedUsers: User[] = usersResponse.data.map((apiUser: ApiUser) => ({ - id: apiUser.id, - name: `${apiUser.first_name} ${apiUser.last_name}`, - email: apiUser.email, - roleId: apiUser.role_id, - roleName: apiUser.role?.name || '', - status: apiUser.is_active ? "ACTIVE" : "INACTIVE", - createdAt: new Date(apiUser.created_at).toISOString().slice(0, 10) - })); - setUsers(mappedUsers); + setSaving(true); + await deleteUser(activeUser.id); + + await handleRefresh(); + setActiveUser(null); } catch (error) { - console.error('Failed to refresh users:', error); + console.error('Failed to delete user:', error); + alert('Failed to delete user. Please try again.'); } finally { - setLoadingUsers(false); + setSaving(false); } }; - const handleDelete = () => { - if (!activeUser) return; - setUsers(prev => prev.filter(u => u.id !== activeUser.id)); - setActiveUser(null); + const fetchModalRoles = async () => { + try { + setLoadingModalRoles(true); + const rolesData = await getAllRoles(); + console.log('Modal roles fetched:', rolesData); + setModalRoles(rolesData); + } catch (error) { + console.error('Failed to fetch modal roles:', error); + } finally { + setLoadingModalRoles(false); + } }; - const filtered = users.filter(u => u.name.toLowerCase().includes(search.toLowerCase()) || u.email.toLowerCase().includes(search.toLowerCase())); + const handleOpenAddModal = () => { + setForm(emptyUser); + setEditingId(null); + setError(null); + setShowModal(true); + fetchModalRoles(); + }; + + const handleOpenEditModal = (user: User) => { + setEditingId(user.id); + setForm({ + name: user.name, + email: user.email, + roleId: user.roleId, + status: user.status, + createdAt: user.createdAt, + password: "" + }); + setError(null); + setShowModal(true); + fetchModalRoles(); + }; + + // Filter users by search and selected role + const filtered = users.filter(u => { + const matchesSearch = u.name.toLowerCase().includes(search.toLowerCase()) || + u.email.toLowerCase().includes(search.toLowerCase()); + const matchesRole = !selectedRoleFilter || u.roleId === selectedRoleFilter; + return matchesSearch && matchesRole; + }); return (
{/* LEFT INFO SIDEBAR */}
-

Project Information

-

Usuarios disponibles y sus roles.

- setSelectedRoleFilter(e.target.value)} + className="w-full border px-3 py-2 rounded" + disabled={loadingUsers} + > + {roles.map(r => )} + + {selectedRoleFilter && ( + + )}
{/* MAIN */} @@ -217,23 +250,12 @@ export default function UsersPage() {

Usuarios registrados

- + - +
@@ -277,17 +299,9 @@ export default function UsersPage() { setForm({...form, firstName: e.target.value})} - disabled={saving} - /> - - setForm({...form, lastName: e.target.value})} + placeholder="Full Name *" + value={form.name} + onChange={e => setForm({...form, name: e.target.value})} disabled={saving} /> @@ -315,10 +329,10 @@ export default function UsersPage() { value={form.roleId} onChange={e => setForm({...form, roleId: e.target.value})} className="w-full border px-3 py-2 rounded" - disabled={loadingRoles || saving} + disabled={loadingModalRoles || saving} > - - {roles.map(r => )} + + {modalRoles.map(r => )} diff --git a/water-api/src/controllers/user.controller.ts b/water-api/src/controllers/user.controller.ts index 0e10953..a60f703 100644 --- a/water-api/src/controllers/user.controller.ts +++ b/water-api/src/controllers/user.controller.ts @@ -67,9 +67,9 @@ export async function getUserById( res: Response ): Promise { try { - const userId = parseInt(req.params.id, 10); + const userId = req.params.id; - if (isNaN(userId)) { + if (!userId) { res.status(400).json({ success: false, error: 'Invalid user ID', @@ -80,7 +80,7 @@ export async function getUserById( // Check if user is admin or requesting their own data const requestingUser = req.user; const isAdmin = requestingUser?.role === 'ADMIN'; - const isSelf = requestingUser?.id === userId.toString(); + const isSelf = requestingUser?.id === userId; if (!isAdmin && !isSelf) { res.status(403).json({ @@ -166,9 +166,9 @@ export async function updateUser( res: Response ): Promise { try { - const userId = parseInt(req.params.id, 10); + const userId = req.params.id; - if (isNaN(userId)) { + if (!userId) { res.status(400).json({ success: false, error: 'Invalid user ID', @@ -178,7 +178,7 @@ export async function updateUser( const requestingUser = req.user; const isAdmin = requestingUser?.role === 'ADMIN'; - const isSelf = requestingUser?.id === userId.toString(); + const isSelf = requestingUser?.id === userId; if (!isAdmin && !isSelf) { res.status(403).json({ @@ -243,9 +243,9 @@ export async function deleteUser( res: Response ): Promise { try { - const userId = parseInt(req.params.id, 10); + const userId = req.params.id; - if (isNaN(userId)) { + if (!userId) { res.status(400).json({ success: false, error: 'Invalid user ID', @@ -254,7 +254,7 @@ export async function deleteUser( } // Prevent admin from deleting themselves - if (req.user?.id === userId.toString()) { + if (req.user?.id === userId) { res.status(400).json({ success: false, error: 'Cannot deactivate your own account', @@ -294,9 +294,9 @@ export async function changePassword( res: Response ): Promise { try { - const userId = parseInt(req.params.id, 10); + const userId = req.params.id; - if (isNaN(userId)) { + if (!userId) { res.status(400).json({ success: false, error: 'Invalid user ID', @@ -305,7 +305,7 @@ export async function changePassword( } // Only allow users to change their own password - if (req.user?.id !== userId.toString()) { + if (req.user?.id !== userId) { res.status(403).json({ success: false, error: 'You can only change your own password', diff --git a/water-api/src/services/user.service.ts b/water-api/src/services/user.service.ts index 2c4c8cd..1b7cf2d 100644 --- a/water-api/src/services/user.service.ts +++ b/water-api/src/services/user.service.ts @@ -148,7 +148,7 @@ export async function getAll( * @param id - User ID * @returns User without password_hash or null if not found */ -export async function getById(id: number): Promise { +export async function getById(id: string): Promise { const result = await query( ` SELECT @@ -234,7 +234,7 @@ export async function create(data: { password: string; name: string; avatar_url?: string | null; - role_id: number; + role_id: string; is_active?: boolean; }): Promise { // Check if email already exists @@ -275,12 +275,12 @@ export async function create(data: { * @returns Updated user without password_hash */ export async function update( - id: number, + id: string, data: { email?: string; name?: string; avatar_url?: string | null; - role_id?: number; + role_id?: string; is_active?: boolean; } ): Promise { @@ -357,7 +357,7 @@ export async function update( * @param id - User ID * @returns True if deleted, false if user not found */ -export async function deleteUser(id: number): Promise { +export async function deleteUser(id: string): Promise { const result = await query( ` UPDATE users @@ -379,7 +379,7 @@ export async function deleteUser(id: number): Promise { * @returns True if password changed, throws error if verification fails */ export async function changePassword( - id: number, + id: string, currentPassword: string, newPassword: string ): Promise { @@ -421,7 +421,7 @@ export async function changePassword( * @param id - User ID * @returns True if updated, false if user not found */ -export async function updateLastLogin(id: number): Promise { +export async function updateLastLogin(id: string): Promise { const result = await query( ` UPDATE users diff --git a/water-api/src/types/index.ts b/water-api/src/types/index.ts index 9c34d33..a8dc089 100644 --- a/water-api/src/types/index.ts +++ b/water-api/src/types/index.ts @@ -23,12 +23,12 @@ export interface Role { } export interface User { - id: number; + id: string; email: string; password_hash: string; name: string; avatar_url: string | null; - role_id: number; + role_id: string; role?: Role; is_active: boolean; last_login: Date | null; @@ -37,11 +37,11 @@ export interface User { } export interface UserPublic { - id: number; + id: string; email: string; name: string; avatar_url: string | null; - role_id: number; + role_id: string; role?: Role; is_active: boolean; last_login: Date | null; @@ -50,9 +50,9 @@ export interface UserPublic { } export interface JwtPayload { - userId: number; + userId: string; email: string; - roleId: number; + roleId: string; roleName: string; iat?: number; exp?: number; diff --git a/water-api/src/validators/user.validator.ts b/water-api/src/validators/user.validator.ts index da7c95d..8d059ae 100644 --- a/water-api/src/validators/user.validator.ts +++ b/water-api/src/validators/user.validator.ts @@ -28,9 +28,8 @@ export const createUserSchema = z.object({ .optional() .nullable(), role_id: z - .number({ required_error: 'Role ID is required' }) - .int('Role ID must be an integer') - .positive('Role ID must be a positive number'), + .string({ required_error: 'Role ID is required' }) + .uuid('Role ID must be a valid UUID'), is_active: z.boolean().default(true), }); @@ -56,9 +55,8 @@ export const updateUserSchema = z.object({ .optional() .nullable(), role_id: z - .number() - .int('Role ID must be an integer') - .positive('Role ID must be a positive number') + .string() + .uuid('Role ID must be a valid UUID') .optional(), is_active: z.boolean().optional(), });