feat(users): add company selection and status to create user modal

This commit is contained in:
Tiago Yamamoto 2025-12-26 01:18:14 -03:00
parent 6ab7e357fb
commit 43c0719664
4 changed files with 80 additions and 9 deletions

View file

@ -242,8 +242,18 @@ func (h *CoreHandlers) ListCompanies(w http.ResponseWriter, r *http.Request) {
// @Router /api/v1/users [post] // @Router /api/v1/users [post]
func (h *CoreHandlers) CreateUser(w http.ResponseWriter, r *http.Request) { func (h *CoreHandlers) CreateUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
tenantID, ok := ctx.Value(middleware.ContextTenantID).(string) tenantID, _ := ctx.Value(middleware.ContextTenantID).(string)
if !ok || tenantID == "" {
userRoles := middleware.ExtractRoles(ctx.Value(middleware.ContextRoles))
isAdmin := false
for _, role := range userRoles {
if role == "ADMIN" || role == "SUPERADMIN" || role == "admin" || role == "superadmin" {
isAdmin = true
break
}
}
if !isAdmin && tenantID == "" {
http.Error(w, "Tenant ID not found in context", http.StatusForbidden) http.Error(w, "Tenant ID not found in context", http.StatusForbidden)
return return
} }

View file

@ -16,7 +16,9 @@ type CreateUserRequest struct {
Name string `json:"name"` Name string `json:"name"`
Email string `json:"email"` Email string `json:"email"`
Password string `json:"password"` Password string `json:"password"`
Roles []string `json:"roles"` // e.g. ["RECRUITER"] Roles []string `json:"roles"`
Status *string `json:"status,omitempty"`
TenantID *string `json:"companyId,omitempty"` // Optional, mainly for superads to assign user to company
} }
type UpdateUserRequest struct { type UpdateUserRequest struct {

View file

@ -36,10 +36,19 @@ func (uc *CreateUserUseCase) Execute(ctx context.Context, input dto.CreateUserRe
} }
// 3. Create Entity // 3. Create Entity
// Note: We enforce currentTenantID to ensure isolation. // Note: We enforce currentTenantID unless it's empty (SuperAdmin context) and input provides one.
user := entity.NewUser("", currentTenantID, input.Name, input.Email) tenantID := currentTenantID
if tenantID == "" && input.TenantID != nil {
tenantID = *input.TenantID
}
user := entity.NewUser("", tenantID, input.Name, input.Email)
user.PasswordHash = hashed user.PasswordHash = hashed
if input.Status != nil {
user.Status = *input.Status
}
// Assign roles // Assign roles
for _, r := range input.Roles { for _, r := range input.Roles {
user.AssignRole(entity.Role{Name: r}) user.AssignRole(entity.Role{Name: r})

View file

@ -19,7 +19,7 @@ import {
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Plus, Search, Trash2, Loader2, RefreshCw, Pencil } from "lucide-react" import { Plus, Search, Trash2, Loader2, RefreshCw, Pencil } from "lucide-react"
import { usersApi, type ApiUser } from "@/lib/api" import { usersApi, adminCompaniesApi, type ApiUser, type AdminCompany } from "@/lib/api"
import { getCurrentUser, isAdminUser } from "@/lib/auth" import { getCurrentUser, isAdminUser } from "@/lib/auth"
import { toast } from "sonner" import { toast } from "sonner"
import { Skeleton } from "@/components/ui/skeleton" import { Skeleton } from "@/components/ui/skeleton"
@ -41,11 +41,15 @@ export default function AdminUsersPage() {
const [creating, setCreating] = useState(false) const [creating, setCreating] = useState(false)
const [updating, setUpdating] = useState(false) const [updating, setUpdating] = useState(false)
const [selectedUser, setSelectedUser] = useState<ApiUser | null>(null) const [selectedUser, setSelectedUser] = useState<ApiUser | null>(null)
const [companies, setCompanies] = useState<AdminCompany[]>([])
const [currentUser, setCurrentUser] = useState<ApiUser | null>(null)
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: "", name: "",
email: "", email: "",
password: "", password: "",
role: "candidate", role: "candidate",
status: "active",
companyId: "",
}) })
const [editFormData, setEditFormData] = useState({ const [editFormData, setEditFormData] = useState({
name: "", name: "",
@ -60,9 +64,23 @@ export default function AdminUsersPage() {
router.push("/dashboard") router.push("/dashboard")
return return
} }
setCurrentUser(user as ApiUser) // Casting safe here due to check
loadUsers() loadUsers()
if (user?.role === 'superadmin') {
loadCompanies()
}
}, [router]) }, [router])
const loadCompanies = async () => {
try {
const data = await adminCompaniesApi.list(undefined, 1, 100)
setCompanies(data.data || [])
} catch (error) {
console.error("Error loading companies:", error)
}
}
const limit = 10 const limit = 10
const totalPages = Math.max(1, Math.ceil(totalUsers / limit)) const totalPages = Math.max(1, Math.ceil(totalUsers / limit))
@ -84,10 +102,14 @@ export default function AdminUsersPage() {
const handleCreate = async () => { const handleCreate = async () => {
try { try {
setCreating(true) setCreating(true)
await usersApi.create(formData) const payload = {
...formData,
roles: [formData.role], // Backend expects array
}
await usersApi.create(payload)
toast.success("User created successfully!") toast.success("User created successfully!")
setIsDialogOpen(false) setIsDialogOpen(false)
setFormData({ name: "", email: "", password: "", role: "candidate" }) setFormData({ name: "", email: "", password: "", role: "candidate", status: "active", companyId: "" })
setPage(1) setPage(1)
loadUsers(1) loadUsers(1)
} catch (error) { } catch (error) {
@ -228,7 +250,6 @@ export default function AdminUsersPage() {
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="password">Password</Label>
<Input <Input
id="password" id="password"
type="password" type="password"
@ -237,6 +258,35 @@ export default function AdminUsersPage() {
placeholder="Secure password" placeholder="Secure password"
/> />
</div> </div>
{currentUser?.role === 'superadmin' && (
<div className="grid gap-2">
<Label htmlFor="company">Company</Label>
<Select value={formData.companyId} onValueChange={(v) => setFormData({ ...formData, companyId: v })}>
<SelectTrigger>
<SelectValue placeholder="Select a company" />
</SelectTrigger>
<SelectContent>
{companies.map((company) => (
<SelectItem key={company.id} value={company.id}>
{company.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
<div className="grid gap-2">
<Label htmlFor="status">Status</Label>
<Select value={formData.status} onValueChange={(v) => setFormData({ ...formData, status: v })}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="inactive">Inactive</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="role">Role</Label> <Label htmlFor="role">Role</Label>
<Select value={formData.role} onValueChange={(v) => setFormData({ ...formData, role: v })}> <Select value={formData.role} onValueChange={(v) => setFormData({ ...formData, role: v })}>