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]
func (h *CoreHandlers) CreateUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
tenantID, ok := ctx.Value(middleware.ContextTenantID).(string)
if !ok || tenantID == "" {
tenantID, _ := ctx.Value(middleware.ContextTenantID).(string)
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)
return
}

View file

@ -16,7 +16,9 @@ type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
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 {

View file

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

View file

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