feat(users): add company selection and status to create user modal
This commit is contained in:
parent
6ab7e357fb
commit
43c0719664
4 changed files with 80 additions and 9 deletions
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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})
|
||||||
|
|
|
||||||
|
|
@ -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 })}>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue