refactor: implement user management API and update dashboard users page with new data table

This commit is contained in:
fredy.siswanto 2025-03-09 23:06:07 +07:00
parent a9a350fdb2
commit de92703fbe
5 changed files with 139 additions and 153 deletions

View File

@ -0,0 +1,41 @@
import { z } from 'zod'
import { HttpServer, type THttpServer } from '~/libs/http-server'
const subscribePlanResponseSchema = z.object({
id: z.string(),
code: z.string(),
name: z.string(),
})
const subscribeResponseSchema = z.object({
id: z.string(),
subscribe_plan_id: z.string(),
start_date: z.string(),
end_date: z.string().nullable(),
status: z.string(),
auto_renew: z.boolean(),
subscribe_plan: subscribePlanResponseSchema,
})
const userResponseSchema = z.object({
id: z.string(),
email: z.string().email(),
phone: z.string(),
subscribe: subscribeResponseSchema,
})
const usersResponseSchema = z.object({
data: z.array(userResponseSchema),
})
export type TSubscribePlanRespon = z.infer<typeof subscribePlanResponseSchema>
export type TUserResponse = z.infer<typeof userResponseSchema>
export type TSubscribeResponse = z.infer<typeof subscribeResponseSchema>
export const getUsers = async (parameters: THttpServer) => {
try {
const { data } = await HttpServer(parameters).get(`/api/staff/users`)
return usersResponseSchema.parse(data)
} catch (error) {
// eslint-disable-next-line unicorn/no-useless-promise-resolve-reject
return Promise.reject(error)
}
}

View File

@ -1,56 +0,0 @@
type TUsers = {
id: number
idTransaction: number
date: string
name: string
email: string
category: string
status: string
}
export const USERS: TUsers[] = [
{
id: 1,
idTransaction: 5_512_446_588,
date: '24/10/2024',
name: 'Ainun Wijaya',
email: 'ainun@gmail.com',
category: 'Pribadi',
status: 'Baru',
},
{
id: 2,
idTransaction: 5_512_446_588,
date: '24/10/2024',
name: 'Ainun Wijaya',
email: 'ainun@gmail.com',
category: 'Pribadi',
status: 'Premium',
},
{
id: 3,
idTransaction: 5_512_446_588,
date: '24/10/2024',
name: 'Ainun Wijaya',
email: 'ainun@gmail.com',
category: 'Pribadi',
status: 'Pembayaran',
},
{
id: 4,
idTransaction: 5_512_446_588,
date: '24/10/2024',
name: 'Ainun Wijaya',
email: 'ainun@gmail.com',
category: 'Pribadi',
status: 'Premium',
},
{
id: 5,
idTransaction: 5_512_446_588,
date: '24/10/2024',
name: 'Ainun Wijaya',
email: 'ainun@gmail.com',
category: 'Pribadi',
status: 'Baru',
},
]

View File

@ -1,110 +1,98 @@
import { Field, Input, Label, Select } from '@headlessui/react'
import DT, { type ConfigColumns } from 'datatables.net-dt'
import DataTable, { type DataTableSlots } from 'datatables.net-react'
import { useRouteLoaderData } from 'react-router'
import { SearchIcon } from '~/components/icons/search'
import { Pagination } from '~/components/ui/pagination'
import type { TUserResponse } from '~/apis/admin/get-users'
import { UiTable } from '~/components/ui/table'
import { TitleDashboard } from '~/components/ui/title-dashboard'
import { USERS } from './data'
import type { loader } from '~/routes/_admin.lg-admin._dashboard.users._index'
import { formatDate } from '~/utils/formatter'
type TColorBadge = 'Baru' | 'Premium' | 'Pembayaran'
const getStatusBadge = (status: TColorBadge) => {
const statusColors = {
Baru: 'bg-[#DFE5FF] text-[#4C5CA0]',
Premium: 'bg-[#FFFCAF] text-[#DBCA6E]',
Pembayaran: 'bg-[#FEC4FF] text-[#CC6EDB]',
}
return statusColors[status] || 'bg-gray-200 text-gray-700'
}
export const UsersPage = () => {
const getStatusBadge = (status: TColorBadge) => {
const statusColors = {
Baru: 'bg-[#DFE5FF] text-[#4C5CA0]',
Premium: 'bg-[#FFFCAF] text-[#DBCA6E]',
Pembayaran: 'bg-[#FEC4FF] text-[#CC6EDB]',
}
return statusColors[status] || 'bg-gray-200 text-gray-700'
const loaderData = useRouteLoaderData<typeof loader>(
'routes/_admin.lg-admin._dashboard.users._index',
)
DataTable.use(DT)
const dataTable =
loaderData?.usersData?.sort(
(a, b) =>
new Date(b.subscribe.start_date).getTime() -
new Date(a.subscribe.start_date).getTime(),
) || []
const dataColumns: ConfigColumns[] = [
{
title: 'No',
render: (
_data: unknown,
_type: unknown,
_row: unknown,
meta: { row: number },
) => {
return meta.row + 1
},
},
{
title: 'Tanggal Daftar',
data: 'subscribe.start_date',
},
{
title: 'Nama User',
},
{
title: 'Email',
data: 'email',
},
{
title: 'Kategori',
data: 'subscribe.status',
},
{
title: 'Status',
data: 'subscribe.subscribe_plan.name',
},
]
const dataSlot: DataTableSlots = {
1: (value: string) => formatDate(value),
2: (_value: unknown, _type: unknown, data: TUserResponse) => (
<div>
<div>{data.phone}</div>
<div className="text-sm text-[#7C7C7C]">ID: {data.id.slice(0, 8)}</div>
</div>
),
4: (_value: string) => <span className="text-sm">Pribadi</span>,
5: (value: TColorBadge) => (
<span className={`rounded-lg px-2 text-sm ${getStatusBadge(value)}`}>
{value}
</span>
),
}
return (
<div className="relative">
<TitleDashboard title="Users" />
{/* filter section */}
<div className="mb-8 flex items-center gap-5 rounded-lg bg-gray-50 text-[#363636]">
<div className="w-[400px]">
<Field>
<Label className="mb-2 block text-sm font-medium">Cari User</Label>
<div className="relative">
<Input
type="text"
placeholder="Cari Nama"
className="w-full rounded-lg bg-white p-2 pr-10 pl-4 shadow focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none"
/>
<div className="absolute inset-y-0 right-0 flex items-center pr-3">
<SearchIcon className="h-5 w-5" />
</div>
</div>
</Field>
</div>
<div className="w-[235px]">
<Field>
<Label className="mb-2 block text-sm font-medium">Status</Label>
<Select className="w-full rounded-lg bg-white p-2 shadow focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none">
<option>Pilih Status</option>
<option>Aktif</option>
<option>Nonaktif</option>
</Select>
</Field>
</div>
<div className="mb-8 flex items-end justify-between gap-5">
<div className="flex-1">{/* TODO: Filter */}</div>
</div>
{/* table */}
<div className="overflow-x-auto">
<div className="rounded-lg bg-white px-6 py-8 shadow-sm">
<h2 className="text-xl font-bold text-[#4C5CA0]">Daftar User</h2>
<table className="min-w-full p-3">
<thead className="my-5 border-b-3 border-[#C2C2C2]">
<tr>
<th className="p-3">No</th>
<th>Tanggal Daftar</th>
<th>Nama User</th>
<th>Email</th>
<th>Kategori</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{USERS.map((user, index) => (
<tr
key={user.id}
className="border-b-1 border-gray-200 text-center"
>
<td className="p-4">{index + 1}</td>
<td>{user.date}</td>
<td>
{user.name}
<div className="text-sm text-gray-500">
id: {user.idTransaction}
</div>
</td>
<td>{user.email}</td>
<td>{user.category}</td>
<td className="">
<span
className={`inline-block min-w-[100px] rounded-full px-2 py-1 text-sm ${getStatusBadge(user.status as TColorBadge)}`}
>
{user.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* pagination */}
<div className="float-end mt-6">
<Pagination
currentPage={1}
totalPages={5}
onPageChange={() => {}}
/>
</div>
</div>
<UiTable
data={dataTable || []}
columns={dataColumns}
slots={dataSlot}
title="Daftar Users"
/>
</div>
)
}

View File

@ -0,0 +1,17 @@
import { getUsers } from '~/apis/admin/get-users'
import { handleCookie } from '~/libs/cookies'
import { UsersPage } from '~/pages/dashboard-users'
import type { Route } from './+types/_admin.lg-admin._dashboard.users._index'
export const loader = async ({ request }: Route.LoaderArgs) => {
const { staffToken } = await handleCookie(request)
const { data: usersData } = await getUsers({
accessToken: staffToken,
})
return { usersData }
}
const DashboardUsersLayout = () => <UsersPage />
export default DashboardUsersLayout

View File

@ -1,4 +0,0 @@
import { UsersPage } from '~/pages/dashboard-users'
const DashboardUsersLayout = () => <UsersPage />
export default DashboardUsersLayout