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]
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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 })}>
|
||||
|
|
|
|||
Loading…
Reference in a new issue