Add paginated users listing
This commit is contained in:
parent
749a83efa4
commit
9c17a7a15a
9 changed files with 189 additions and 77 deletions
|
|
@ -717,6 +717,20 @@ const docTemplate = `{
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Page number (default: 1)",
|
||||||
|
"name": "page",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Items per page (default: 10, max: 100)",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
|
|
@ -728,10 +742,7 @@ const docTemplate = `{
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "array",
|
"$ref": "#/definitions/dto.PaginatedResponse"
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/dto.UserResponse"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"403": {
|
"403": {
|
||||||
|
|
|
||||||
|
|
@ -710,6 +710,20 @@
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Page number (default: 1)",
|
||||||
|
"name": "page",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Items per page (default: 10, max: 100)",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
|
|
@ -721,10 +735,7 @@
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "array",
|
"$ref": "#/definitions/dto.PaginatedResponse"
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/dto.UserResponse"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"403": {
|
"403": {
|
||||||
|
|
@ -1423,4 +1434,4 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -857,15 +857,22 @@ paths:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
description: Returns a list of users belonging to the authenticated tenant.
|
description: Returns a list of users belonging to the authenticated tenant.
|
||||||
|
parameters:
|
||||||
|
- description: Page number (default: 1)
|
||||||
|
in: query
|
||||||
|
name: page
|
||||||
|
type: integer
|
||||||
|
- description: Items per page (default: 10, max: 100)
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: OK
|
description: OK
|
||||||
schema:
|
schema:
|
||||||
items:
|
$ref: '#/definitions/dto.PaginatedResponse'
|
||||||
$ref: '#/definitions/dto.UserResponse'
|
|
||||||
type: array
|
|
||||||
"403":
|
"403":
|
||||||
description: Forbidden
|
description: Forbidden
|
||||||
schema:
|
schema:
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/rede5/gohorsejobs/backend/internal/api/middleware"
|
"github.com/rede5/gohorsejobs/backend/internal/api/middleware"
|
||||||
|
|
@ -173,7 +174,9 @@ func (h *CoreHandlers) CreateUser(w http.ResponseWriter, r *http.Request) {
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Success 200 {array} dto.UserResponse
|
// @Param page query int false "Page number (default: 1)"
|
||||||
|
// @Param limit query int false "Items per page (default: 10, max: 100)"
|
||||||
|
// @Success 200 {object} dto.PaginatedResponse
|
||||||
// @Failure 403 {string} string "Forbidden"
|
// @Failure 403 {string} string "Forbidden"
|
||||||
// @Failure 500 {string} string "Internal Server Error"
|
// @Failure 500 {string} string "Internal Server Error"
|
||||||
// @Router /api/v1/users [get]
|
// @Router /api/v1/users [get]
|
||||||
|
|
@ -185,7 +188,10 @@ func (h *CoreHandlers) ListUsers(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
users, err := h.listUsersUC.Execute(ctx, tenantID)
|
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
||||||
|
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||||
|
|
||||||
|
users, err := h.listUsersUC.Execute(ctx, tenantID, page, limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ type UserRepository interface {
|
||||||
FindByID(ctx context.Context, id string) (*entity.User, error)
|
FindByID(ctx context.Context, id string) (*entity.User, error)
|
||||||
FindByEmail(ctx context.Context, email string) (*entity.User, error)
|
FindByEmail(ctx context.Context, email string) (*entity.User, error)
|
||||||
// FindAllByTenant returns users strictly scoped to a tenant
|
// FindAllByTenant returns users strictly scoped to a tenant
|
||||||
FindAllByTenant(ctx context.Context, tenantID string) ([]*entity.User, error)
|
FindAllByTenant(ctx context.Context, tenantID string, limit, offset int) ([]*entity.User, int, error)
|
||||||
Update(ctx context.Context, user *entity.User) (*entity.User, error)
|
Update(ctx context.Context, user *entity.User) (*entity.User, error)
|
||||||
Delete(ctx context.Context, id string) error
|
Delete(ctx context.Context, id string) error
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,22 @@ func NewListUsersUseCase(uRepo ports.UserRepository) *ListUsersUseCase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *ListUsersUseCase) Execute(ctx context.Context, tenantID string) ([]dto.UserResponse, error) {
|
func (uc *ListUsersUseCase) Execute(ctx context.Context, tenantID string, page, limit int) (*dto.PaginatedResponse, error) {
|
||||||
users, err := uc.userRepo.FindAllByTenant(ctx, tenantID)
|
if page <= 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 10
|
||||||
|
}
|
||||||
|
if limit > 100 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
offset := (page - 1) * limit
|
||||||
|
if offset < 0 {
|
||||||
|
offset = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
users, total, err := uc.userRepo.FindAllByTenant(ctx, tenantID, limit, offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -40,5 +54,12 @@ func (uc *ListUsersUseCase) Execute(ctx context.Context, tenantID string) ([]dto
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return response, nil
|
return &dto.PaginatedResponse{
|
||||||
|
Data: response,
|
||||||
|
Pagination: dto.Pagination{
|
||||||
|
Page: page,
|
||||||
|
Limit: limit,
|
||||||
|
Total: total,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -96,11 +96,21 @@ func (r *UserRepository) FindByID(ctx context.Context, id string) (*entity.User,
|
||||||
return u, nil
|
return u, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *UserRepository) FindAllByTenant(ctx context.Context, tenantID string) ([]*entity.User, error) {
|
func (r *UserRepository) FindAllByTenant(ctx context.Context, tenantID string, limit, offset int) ([]*entity.User, int, error) {
|
||||||
query := `SELECT id, tenant_id, name, email, password_hash, status, created_at, updated_at FROM core_users WHERE tenant_id = $1`
|
var total int
|
||||||
rows, err := r.db.QueryContext(ctx, query, tenantID)
|
countQuery := `SELECT COUNT(*) FROM core_users WHERE tenant_id = $1`
|
||||||
|
if err := r.db.QueryRowContext(ctx, countQuery, tenantID).Scan(&total); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `SELECT id, tenant_id, name, email, password_hash, status, created_at, updated_at
|
||||||
|
FROM core_users
|
||||||
|
WHERE tenant_id = $1
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT $2 OFFSET $3`
|
||||||
|
rows, err := r.db.QueryContext(ctx, query, tenantID, limit, offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
|
|
@ -108,13 +118,13 @@ func (r *UserRepository) FindAllByTenant(ctx context.Context, tenantID string) (
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
u := &entity.User{}
|
u := &entity.User{}
|
||||||
if err := rows.Scan(&u.ID, &u.TenantID, &u.Name, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt, &u.UpdatedAt); err != nil {
|
if err := rows.Scan(&u.ID, &u.TenantID, &u.Name, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt, &u.UpdatedAt); err != nil {
|
||||||
return nil, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
// Populate roles N+1? Ideally join, but for now simple
|
// Populate roles N+1? Ideally join, but for now simple
|
||||||
u.Roles, _ = r.getRoles(ctx, u.ID)
|
u.Roles, _ = r.getRoles(ctx, u.ID)
|
||||||
users = append(users, u)
|
users = append(users, u)
|
||||||
}
|
}
|
||||||
return users, nil
|
return users, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *UserRepository) Update(ctx context.Context, user *entity.User) (*entity.User, error) {
|
func (r *UserRepository) Update(ctx context.Context, user *entity.User) (*entity.User, error) {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ export default function AdminUsersPage() {
|
||||||
const [users, setUsers] = useState<ApiUser[]>([])
|
const [users, setUsers] = useState<ApiUser[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [searchTerm, setSearchTerm] = useState("")
|
const [searchTerm, setSearchTerm] = useState("")
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [totalUsers, setTotalUsers] = useState(0)
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||||
const [creating, setCreating] = useState(false)
|
const [creating, setCreating] = useState(false)
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
|
|
@ -46,11 +48,16 @@ export default function AdminUsersPage() {
|
||||||
loadUsers()
|
loadUsers()
|
||||||
}, [router])
|
}, [router])
|
||||||
|
|
||||||
const loadUsers = async () => {
|
const limit = 10
|
||||||
|
const totalPages = Math.max(1, Math.ceil(totalUsers / limit))
|
||||||
|
|
||||||
|
const loadUsers = async (targetPage = page) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const data = await usersApi.list()
|
const data = await usersApi.list({ page: targetPage, limit })
|
||||||
setUsers(data || [])
|
setUsers(data?.data || [])
|
||||||
|
setTotalUsers(data?.pagination?.total || 0)
|
||||||
|
setPage(data?.pagination?.page || targetPage)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading users:", error)
|
console.error("Error loading users:", error)
|
||||||
toast.error("Failed to load users")
|
toast.error("Failed to load users")
|
||||||
|
|
@ -66,7 +73,8 @@ export default function AdminUsersPage() {
|
||||||
toast.success("User created successfully!")
|
toast.success("User created successfully!")
|
||||||
setIsDialogOpen(false)
|
setIsDialogOpen(false)
|
||||||
setFormData({ name: "", email: "", password: "", role: "jobSeeker" })
|
setFormData({ name: "", email: "", password: "", role: "jobSeeker" })
|
||||||
loadUsers()
|
setPage(1)
|
||||||
|
loadUsers(1)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating user:", error)
|
console.error("Error creating user:", error)
|
||||||
toast.error("Failed to create user")
|
toast.error("Failed to create user")
|
||||||
|
|
@ -80,7 +88,9 @@ export default function AdminUsersPage() {
|
||||||
try {
|
try {
|
||||||
await usersApi.delete(id)
|
await usersApi.delete(id)
|
||||||
toast.success("User deleted!")
|
toast.success("User deleted!")
|
||||||
loadUsers()
|
const nextPage = page > 1 && users.length === 1 ? page - 1 : page
|
||||||
|
setPage(nextPage)
|
||||||
|
loadUsers(nextPage)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error deleting user:", error)
|
console.error("Error deleting user:", error)
|
||||||
toast.error("Failed to delete user")
|
toast.error("Failed to delete user")
|
||||||
|
|
@ -201,12 +211,12 @@ export default function AdminUsersPage() {
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<CardDescription>Total users</CardDescription>
|
<CardDescription>Total users</CardDescription>
|
||||||
<CardTitle className="text-3xl">{users.length}</CardTitle>
|
<CardTitle className="text-3xl">{totalUsers}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<CardDescription>Admins</CardDescription>
|
<CardDescription>Admins (page)</CardDescription>
|
||||||
<CardTitle className="text-3xl">
|
<CardTitle className="text-3xl">
|
||||||
{users.filter((u) => u.role === "superadmin" || u.role === "companyAdmin").length}
|
{users.filter((u) => u.role === "superadmin" || u.role === "companyAdmin").length}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|
@ -214,13 +224,13 @@ export default function AdminUsersPage() {
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<CardDescription>Recruiters</CardDescription>
|
<CardDescription>Recruiters (page)</CardDescription>
|
||||||
<CardTitle className="text-3xl">{users.filter((u) => u.role === "recruiter").length}</CardTitle>
|
<CardTitle className="text-3xl">{users.filter((u) => u.role === "recruiter").length}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<CardDescription>Candidates</CardDescription>
|
<CardDescription>Candidates (page)</CardDescription>
|
||||||
<CardTitle className="text-3xl">{users.filter((u) => u.role === "jobSeeker").length}</CardTitle>
|
<CardTitle className="text-3xl">{users.filter((u) => u.role === "jobSeeker").length}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
@ -247,53 +257,83 @@ export default function AdminUsersPage() {
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Table>
|
<div className="space-y-4">
|
||||||
<TableHeader>
|
<Table>
|
||||||
<TableRow>
|
<TableHeader>
|
||||||
<TableHead>Name</TableHead>
|
|
||||||
<TableHead>Email</TableHead>
|
|
||||||
<TableHead>Role</TableHead>
|
|
||||||
<TableHead>Status</TableHead>
|
|
||||||
<TableHead>Created</TableHead>
|
|
||||||
<TableHead className="text-right">Actions</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{filteredUsers.length === 0 ? (
|
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={6} className="text-center text-muted-foreground py-8">
|
<TableHead>Name</TableHead>
|
||||||
No users found
|
<TableHead>Email</TableHead>
|
||||||
</TableCell>
|
<TableHead>Role</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead>Created</TableHead>
|
||||||
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
</TableHeader>
|
||||||
filteredUsers.map((user) => (
|
<TableBody>
|
||||||
<TableRow key={user.id}>
|
{filteredUsers.length === 0 ? (
|
||||||
<TableCell className="font-medium">{user.name}</TableCell>
|
<TableRow>
|
||||||
<TableCell>{user.email}</TableCell>
|
<TableCell colSpan={6} className="text-center text-muted-foreground py-8">
|
||||||
<TableCell>{getRoleBadge(user.role)}</TableCell>
|
No users found
|
||||||
<TableCell>
|
|
||||||
<Badge variant={user.status === "active" ? "default" : "secondary"}>
|
|
||||||
{user.status === "active" ? "Active" : user.status}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{user.created_at ? new Date(user.created_at).toLocaleDateString("en-US") : "-"}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => handleDelete(user.id)}
|
|
||||||
disabled={user.role === "superadmin"}
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
|
||||||
</Button>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
) : (
|
||||||
)}
|
filteredUsers.map((user) => (
|
||||||
</TableBody>
|
<TableRow key={user.id}>
|
||||||
</Table>
|
<TableCell className="font-medium">{user.name}</TableCell>
|
||||||
|
<TableCell>{user.email}</TableCell>
|
||||||
|
<TableCell>{getRoleBadge(user.role)}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant={user.status === "active" ? "default" : "secondary"}>
|
||||||
|
{user.status === "active" ? "Active" : user.status}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{user.created_at ? new Date(user.created_at).toLocaleDateString("en-US") : "-"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => handleDelete(user.id)}
|
||||||
|
disabled={user.role === "superadmin"}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-2 text-sm text-muted-foreground">
|
||||||
|
<span>
|
||||||
|
{totalUsers === 0
|
||||||
|
? "No users to display"
|
||||||
|
: `Showing ${(page - 1) * limit + 1}-${Math.min(page * limit, totalUsers)} of ${totalUsers}`}
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => loadUsers(page - 1)}
|
||||||
|
disabled={page <= 1 || loading}
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</Button>
|
||||||
|
<span>
|
||||||
|
Page {page} of {totalPages}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => loadUsers(page + 1)}
|
||||||
|
disabled={page >= totalPages || loading}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,13 @@ async function apiRequest<T>(
|
||||||
|
|
||||||
// Users API
|
// Users API
|
||||||
export const usersApi = {
|
export const usersApi = {
|
||||||
list: () => apiRequest<ApiUser[]>("/api/v1/users"),
|
list: (params?: { page?: number; limit?: number }) => {
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
if (params?.page) query.set("page", String(params.page));
|
||||||
|
if (params?.limit) query.set("limit", String(params.limit));
|
||||||
|
const queryStr = query.toString();
|
||||||
|
return apiRequest<PaginatedResponse<ApiUser>>(`/api/v1/users${queryStr ? `?${queryStr}` : ""}`);
|
||||||
|
},
|
||||||
|
|
||||||
create: (data: { name: string; email: string; password: string; role: string }) =>
|
create: (data: { name: string; email: string; password: string; role: string }) =>
|
||||||
apiRequest<ApiUser>("/api/v1/users", {
|
apiRequest<ApiUser>("/api/v1/users", {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue