diff --git a/apps/ui-sharethrift/src/components/layouts/home/account/admin-dashboard/components/admin-appeals-table/admin-appeals-table.container.graphql b/apps/ui-sharethrift/src/components/layouts/home/account/admin-dashboard/components/admin-appeals-table/admin-appeals-table.container.graphql new file mode 100644 index 000000000..e0cf9d93c --- /dev/null +++ b/apps/ui-sharethrift/src/components/layouts/home/account/admin-dashboard/components/admin-appeals-table/admin-appeals-table.container.graphql @@ -0,0 +1,38 @@ +query AdminAppealsTableContainerGetAllUserAppealRequests( + $input: GetAllUserAppealRequestsInput! +) { + getAllUserAppealRequests(input: $input) { + items { + id + reason + state + type + createdAt + updatedAt + user { + id + account { + username + email + profile { + firstName + lastName + } + } + } + } + total + page + pageSize + } +} + +mutation AdminAppealsTableContainerUpdateUserAppealRequestState( + $input: UpdateUserAppealRequestStateInput! +) { + updateUserAppealRequestState(input: $input) { + id + state + updatedAt + } +} diff --git a/apps/ui-sharethrift/src/components/layouts/home/account/admin-dashboard/components/admin-appeals-table/admin-appeals-table.container.tsx b/apps/ui-sharethrift/src/components/layouts/home/account/admin-dashboard/components/admin-appeals-table/admin-appeals-table.container.tsx new file mode 100644 index 000000000..b1a8201b8 --- /dev/null +++ b/apps/ui-sharethrift/src/components/layouts/home/account/admin-dashboard/components/admin-appeals-table/admin-appeals-table.container.tsx @@ -0,0 +1,172 @@ +import { useQuery, useMutation } from '@apollo/client/react'; +import { AdminAppealsTable } from './admin-appeals-table.tsx'; +import { ComponentQueryLoader } from '@sthrift/ui-components'; +import { useState } from 'react'; +import { message } from 'antd'; +import { + AdminAppealsTableContainerGetAllUserAppealRequestsDocument, + AdminAppealsTableContainerUpdateUserAppealRequestStateDocument, + type AdminAppealsTableContainerGetAllUserAppealRequestsQuery, +} from '../../../../../../../generated.tsx'; +import type { AdminAppealData } from './admin-appeals-table.types.ts'; + +export const AdminAppealsTableContainer: React.FC = () => { + const [searchText, setSearchText] = useState(''); + const [statusFilters, setStatusFilters] = useState([]); + const [sorter, setSorter] = useState<{ + field: string; + order: 'ascend' | 'descend'; + } | null>(null); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + + const { data, loading, error, refetch } = useQuery< + AdminAppealsTableContainerGetAllUserAppealRequestsQuery + >(AdminAppealsTableContainerGetAllUserAppealRequestsDocument, { + variables: { + input: { + page: currentPage, + pageSize: pageSize, + stateFilters: statusFilters.length > 0 ? statusFilters : undefined, + sorter: sorter + ? { + field: sorter.field, + direction: sorter.order === 'ascend' ? 'ASC' : 'DESC', + } + : undefined, + }, + }, + }); + + const [updateAppealState] = useMutation( + AdminAppealsTableContainerUpdateUserAppealRequestStateDocument, + ); + + const handleSearch = (text: string) => { + setSearchText(text); + setCurrentPage(1); + }; + + const handleStatusFilter = (filters: string[]) => { + setStatusFilters(filters); + setCurrentPage(1); + }; + + const handleTableChange = ( + _pagination: unknown, + _filters: unknown, + sorter: unknown, + ) => { + // Handle sorting + if (sorter && typeof sorter === 'object' && 'field' in sorter) { + const { field, order } = sorter as { + field: string; + order?: 'ascend' | 'descend'; + }; + if (order) { + setSorter({ field, order }); + } else { + setSorter(null); + } + } + }; + + const handlePageChange = (page: number, pageSize: number) => { + setCurrentPage(page); + setPageSize(pageSize); + }; + + const handleAction = async ( + action: 'accept' | 'deny' | 'view-user', + appealId: string, + ) => { + // TODO: Implement navigation to user profile + // The view-user action should navigate to the user's profile page + // Example: navigate(`/admin/users/${appealId}`) + if (action === 'view-user') { + console.log('Navigate to user:', appealId); + message.info('User profile navigation not yet implemented'); + return; + } + + try { + const newState = + action === 'accept' ? ('accepted' as const) : ('denied' as const); + + await updateAppealState({ + variables: { + input: { + id: appealId, + state: newState, + }, + }, + }); + + message.success( + `Appeal ${action === 'accept' ? 'accepted' : 'denied'} successfully`, + ); + refetch(); + } catch (error) { + console.error(`Failed to ${action} appeal:`, error); + message.error(`Failed to ${action} appeal. Please try again.`); + } + }; + + const appealsList = data?.getAllUserAppealRequests?.items || []; + + // TODO: PERFORMANCE - Move search filtering to server-side + // Current implementation filters on the client which won't scale well + // The GraphQL schema needs to be extended to support searchText parameter + // Issue: https://github.com/simnova/sharethrift/issues/XXX + const filteredAppeals = searchText + ? appealsList.filter( + (appeal) => + appeal.user.account?.profile?.firstName + ?.toLowerCase() + .includes(searchText.toLowerCase()) || + appeal.user.account?.profile?.lastName + ?.toLowerCase() + .includes(searchText.toLowerCase()) || + appeal.user.account?.email + ?.toLowerCase() + .includes(searchText.toLowerCase()), + ) + : appealsList; + + const appealsData: AdminAppealData[] = filteredAppeals.map((appeal) => ({ + id: appeal.id, + userId: appeal.user.id, + userName: `${appeal.user.account?.profile?.firstName || ''} ${appeal.user.account?.profile?.lastName || ''}`.trim(), + userEmail: appeal.user.account?.email || '', + reason: appeal.reason, + state: appeal.state as 'requested' | 'accepted' | 'denied', + type: appeal.type as 'user' | 'listing', + createdAt: appeal.createdAt, + updatedAt: appeal.updatedAt, + })); + + return ( + + } + /> + ); +}; diff --git a/apps/ui-sharethrift/src/components/layouts/home/account/admin-dashboard/components/admin-appeals-table/admin-appeals-table.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/account/admin-dashboard/components/admin-appeals-table/admin-appeals-table.stories.tsx new file mode 100644 index 000000000..cd2a64e66 --- /dev/null +++ b/apps/ui-sharethrift/src/components/layouts/home/account/admin-dashboard/components/admin-appeals-table/admin-appeals-table.stories.tsx @@ -0,0 +1,117 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { AdminAppealsTable } from './admin-appeals-table'; +import type { AdminAppealData } from './admin-appeals-table.types'; + +const meta = { + title: + 'Layouts/Home/Account/AdminDashboard/Components/AdminAppealsTable', + component: AdminAppealsTable, + parameters: { + layout: 'padded', + }, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const mockAppeals: AdminAppealData[] = [ + { + id: '1', + userId: 'user1', + userName: 'John Doe', + userEmail: 'john.doe@example.com', + reason: + 'I believe my account was blocked by mistake. I have always followed the community guidelines.', + state: 'requested', + type: 'user', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + { + id: '2', + userId: 'user2', + userName: 'Jane Smith', + userEmail: 'jane.smith@example.com', + reason: + 'I apologize for the late return. There was a family emergency.', + state: 'accepted', + type: 'user', + createdAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), + updatedAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), + }, + { + id: '3', + userId: 'user3', + userName: 'Bob Johnson', + userEmail: 'bob.johnson@example.com', + reason: 'I disagree with the block decision.', + state: 'denied', + type: 'user', + createdAt: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString(), + updatedAt: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString(), + }, + { + id: '4', + userId: 'user4', + userName: 'Alice Williams', + userEmail: 'alice.williams@example.com', + reason: + 'My listing was blocked unfairly. I have updated it according to the guidelines.', + state: 'requested', + type: 'listing', + createdAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), + updatedAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), + }, +]; + +export const Default: Story = { + args: { + data: mockAppeals, + searchText: '', + statusFilters: [], + currentPage: 1, + pageSize: 10, + total: mockAppeals.length, + loading: false, + onSearch: (text: string) => console.log('Search:', text), + onStatusFilter: (filters: string[]) => console.log('Filter:', filters), + onTableChange: (pagination, filters, sorter) => + console.log('Table change:', { pagination, filters, sorter }), + onPageChange: (page, pageSize) => + console.log('Page change:', { page, pageSize }), + onAction: (action, appealId) => + console.log('Action:', { action, appealId }), + }, +}; + +export const WithPendingAppeals: Story = { + args: { + ...Default.args, + data: mockAppeals.filter((a) => a.state === 'requested'), + statusFilters: ['requested'], + }, +}; + +export const WithAcceptedAppeals: Story = { + args: { + ...Default.args, + data: mockAppeals.filter((a) => a.state === 'accepted'), + statusFilters: ['accepted'], + }, +}; + +export const Empty: Story = { + args: { + ...Default.args, + data: [], + total: 0, + }, +}; + +export const Loading: Story = { + args: { + ...Default.args, + loading: true, + }, +}; diff --git a/apps/ui-sharethrift/src/components/layouts/home/account/admin-dashboard/components/admin-appeals-table/admin-appeals-table.tsx b/apps/ui-sharethrift/src/components/layouts/home/account/admin-dashboard/components/admin-appeals-table/admin-appeals-table.tsx new file mode 100644 index 000000000..4e0bd7eeb --- /dev/null +++ b/apps/ui-sharethrift/src/components/layouts/home/account/admin-dashboard/components/admin-appeals-table/admin-appeals-table.tsx @@ -0,0 +1,355 @@ +import { Input, Checkbox, Button, Tag, Modal, Space } from 'antd'; +import type { TableProps } from 'antd'; +import { + SearchOutlined, + FilterOutlined, + CheckCircleOutlined, + CloseCircleOutlined, + ExclamationCircleOutlined, +} from '@ant-design/icons'; +import { Dashboard } from '@sthrift/ui-components'; +import type { + AdminAppealData, + AdminAppealsTableProps, +} from './admin-appeals-table.types.ts'; +import { useState } from 'react'; + +const { Search } = Input; + +const STATUS_OPTIONS = [ + { label: 'Requested', value: 'requested' }, + { label: 'Accepted', value: 'accepted' }, + { label: 'Denied', value: 'denied' }, +]; + +export const AdminAppealsTable: React.FC< + Readonly +> = ({ + data, + searchText, + statusFilters, + sorter, + currentPage, + pageSize, + total, + loading = false, + onSearch, + onStatusFilter, + onTableChange, + onPageChange, + onAction, +}) => { + const [acceptModalVisible, setAcceptModalVisible] = useState(false); + const [denyModalVisible, setDenyModalVisible] = useState(false); + const [selectedAppeal, setSelectedAppeal] = useState( + null, + ); + const [viewModalVisible, setViewModalVisible] = useState(false); + + const handleAcceptAppeal = (appeal: AdminAppealData) => { + setSelectedAppeal(appeal); + setAcceptModalVisible(true); + }; + + const handleDenyAppeal = (appeal: AdminAppealData) => { + setSelectedAppeal(appeal); + setDenyModalVisible(true); + }; + + const handleViewDetails = (appeal: AdminAppealData) => { + setSelectedAppeal(appeal); + setViewModalVisible(true); + }; + + const handleAcceptConfirm = () => { + if (selectedAppeal) { + onAction('accept', selectedAppeal.id); + } + setAcceptModalVisible(false); + }; + + const handleDenyConfirm = () => { + if (selectedAppeal) { + onAction('deny', selectedAppeal.id); + } + setDenyModalVisible(false); + }; + + const getStateTag = (state: string) => { + switch (state) { + case 'requested': + return ( + } color="warning"> + Pending + + ); + case 'accepted': + return ( + } color="success"> + Accepted + + ); + case 'denied': + return ( + } color="error"> + Denied + + ); + default: + return null; + } + }; + + const getActionButtons = (record: AdminAppealData) => { + const commonActions = [ + , + , + ]; + + if (record.state === 'requested') { + return [ + ...commonActions, + , + , + ]; + } + + return commonActions; + }; + + const columns: TableProps['columns'] = [ + { + title: 'User', + dataIndex: 'userName', + key: 'userName', + sorter: true, + sortOrder: sorter?.field === 'userName' ? sorter.order : undefined, + filterDropdown: ({ setSelectedKeys, selectedKeys, confirm }) => ( +
+ { + setSelectedKeys(e.target.value ? [e.target.value] : []); + }} + onSearch={(value) => { + confirm(); + onSearch(value); + }} + style={{ width: 250 }} + allowClear + /> +
+ ), + filterIcon: (filtered: boolean) => ( + + ), + }, + { + title: 'Email', + dataIndex: 'userEmail', + key: 'userEmail', + }, + { + title: 'Type', + dataIndex: 'type', + key: 'type', + render: (type: string) => ( + {type} + ), + }, + { + title: 'Status', + dataIndex: 'state', + key: 'state', + render: (state: string) => getStateTag(state), + filters: STATUS_OPTIONS.map((opt) => ({ + text: opt.label, + value: opt.value, + })), + filteredValue: statusFilters, + filterDropdown: ({ setSelectedKeys, selectedKeys, confirm }) => ( +
+ { + setSelectedKeys(values); + onStatusFilter(values as string[]); + confirm(); + }} + /> +
+ ), + filterIcon: (filtered: boolean) => ( + + ), + }, + { + title: 'Submitted', + dataIndex: 'createdAt', + key: 'createdAt', + sorter: true, + sortOrder: sorter?.field === 'createdAt' ? sorter.order : undefined, + render: (date: string) => + new Date(date).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + }), + }, + { + title: 'Actions', + key: 'actions', + render: (_: unknown, record: AdminAppealData) => ( + {getActionButtons(record)} + ), + }, + ]; + + return ( +
+ onPageChange(page, pageSize)} + showPagination={true} + onChange={onTableChange} + /> + + {/* Accept Appeal Modal */} + setAcceptModalVisible(false)} + okText="Accept Appeal" + okButtonProps={{ style: { backgroundColor: 'green' } }} + > +

+ Are you sure you want to accept this appeal? The user's account will + be unblocked. +

+ {selectedAppeal && ( +
+ User: {selectedAppeal.userName} +
+ Email: {selectedAppeal.userEmail} +
+ )} +
+ + {/* Deny Appeal Modal */} + setDenyModalVisible(false)} + okText="Deny Appeal" + okButtonProps={{ danger: true }} + > +

+ Are you sure you want to deny this appeal? The user will remain + blocked. +

+ {selectedAppeal && ( +
+ User: {selectedAppeal.userName} +
+ Email: {selectedAppeal.userEmail} +
+ )} +
+ + {/* View Details Modal */} + setViewModalVisible(false)} + footer={[ + , + ]} + width={600} + > + {selectedAppeal && ( +
+
+ User: {selectedAppeal.userName} +
+ Email: {selectedAppeal.userEmail} +
+ Status: {getStateTag(selectedAppeal.state)} +
+ Type:{' '} + + {selectedAppeal.type} + +
+ Submitted:{' '} + {new Date(selectedAppeal.createdAt).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + })} +
+
+ Appeal Reason: +

+ {selectedAppeal.reason} +

+
+
+ )} +
+
+ ); +}; diff --git a/apps/ui-sharethrift/src/components/layouts/home/account/admin-dashboard/components/admin-appeals-table/admin-appeals-table.types.ts b/apps/ui-sharethrift/src/components/layouts/home/account/admin-dashboard/components/admin-appeals-table/admin-appeals-table.types.ts new file mode 100644 index 000000000..305bbb681 --- /dev/null +++ b/apps/ui-sharethrift/src/components/layouts/home/account/admin-dashboard/components/admin-appeals-table/admin-appeals-table.types.ts @@ -0,0 +1,30 @@ +export interface AdminAppealData { + id: string; + userId: string; + userName: string; + userEmail: string; + reason: string; + state: 'requested' | 'accepted' | 'denied'; + type: 'user' | 'listing'; + createdAt: string; + updatedAt: string; +} + +export interface AdminAppealsTableProps { + data: AdminAppealData[]; + searchText: string; + statusFilters: string[]; + sorter?: { + field: string; + order: 'ascend' | 'descend'; + }; + currentPage: number; + pageSize: number; + total: number; + loading?: boolean; + onSearch: (text: string) => void; + onStatusFilter: (filters: string[]) => void; + onTableChange: (pagination: unknown, filters: unknown, sorter: unknown) => void; + onPageChange: (page: number, pageSize: number) => void; + onAction: (action: 'accept' | 'deny' | 'view-user', appealId: string) => void; +} diff --git a/apps/ui-sharethrift/src/components/layouts/home/account/admin-dashboard/components/admin-appeals-table/index.ts b/apps/ui-sharethrift/src/components/layouts/home/account/admin-dashboard/components/admin-appeals-table/index.ts new file mode 100644 index 000000000..d82f4b68f --- /dev/null +++ b/apps/ui-sharethrift/src/components/layouts/home/account/admin-dashboard/components/admin-appeals-table/index.ts @@ -0,0 +1,6 @@ +export { AdminAppealsTableContainer as AdminAppeals } from './admin-appeals-table.container.tsx'; +export { AdminAppealsTable } from './admin-appeals-table.tsx'; +export type { + AdminAppealData, + AdminAppealsTableProps, +} from './admin-appeals-table.types.ts'; diff --git a/apps/ui-sharethrift/src/components/layouts/home/account/admin-dashboard/components/admin-users-table/admin-users-table.container.tsx b/apps/ui-sharethrift/src/components/layouts/home/account/admin-dashboard/components/admin-users-table/admin-users-table.container.tsx index bdcf9b161..28854078e 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/account/admin-dashboard/components/admin-users-table/admin-users-table.container.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/account/admin-dashboard/components/admin-users-table/admin-users-table.container.tsx @@ -4,165 +4,181 @@ import { ComponentQueryLoader } from "@sthrift/ui-components"; import { message } from "antd"; import { useQuery, useMutation } from "@apollo/client/react"; import { - AdminUsersTableContainerAllUsersDocument, - BlockUserDocument, - UnblockUserDocument, + AdminUsersTableContainerAllUsersDocument, + BlockUserDocument, + UnblockUserDocument, } from "../../../../../../../generated.tsx"; +import { useNavigate } from 'react-router-dom'; interface AdminUsersTableContainerProps { - currentPage: number; - onPageChange: (page: number) => void; + currentPage: number; + onPageChange: (page: number) => void; } export const AdminUsersTableContainer: React.FC> = ({ - currentPage, - onPageChange, + currentPage, + onPageChange, }) => { - const [searchText, setSearchText] = useState(""); - const [statusFilters, setStatusFilters] = useState([]); - const [sorter, setSorter] = useState<{ - field: string | null; - order: "ascend" | "descend" | null; - }>({ field: null, order: null }); - const pageSize = 50; // in BRD - - const { data, loading, error, refetch } = useQuery( - AdminUsersTableContainerAllUsersDocument, - { - variables: { - page: currentPage, - pageSize: pageSize, - searchText: searchText, - statusFilters: statusFilters, - sorter: - sorter.field && sorter.order - ? { field: sorter.field, order: sorter.order } - : undefined, - }, - fetchPolicy: "network-only", - } - ); - - const [blockUser] = useMutation(BlockUserDocument, { - onCompleted: () => { - message.success("User blocked successfully"); - refetch(); - }, - onError: (err) => { - message.error(`Failed to block user: ${err.message}`); - }, - }); - - const [unblockUser] = useMutation(UnblockUserDocument, { - onCompleted: () => { - message.success("User unblocked successfully"); - refetch(); - }, - onError: (err) => { - message.error(`Failed to unblock user: ${err.message}`); - }, - }); - - // Transform GraphQL data to match AdminUserData structure - const users = (data?.allUsers?.items ?? []).map((user) => ({ - id: user.id, - username: user.account?.username ?? "N/A", - firstName: user.account?.profile?.firstName ?? "N/A", - lastName: user.account?.profile?.lastName ?? "N/A", - email: user.account?.email ?? "N/A", - accountCreated: user.createdAt ?? "Unknown", - status: user.isBlocked ? ("Blocked" as const) : ("Active" as const), - isBlocked: user.isBlocked ?? false, - userType: user.userType ?? "unknown", - reportCount: 0, // TODO: Add reportCount to GraphQL query once available - })); - const total = data?.allUsers?.total ?? 0; - - const handleSearch = (value: string) => { - setSearchText(value); - onPageChange(1); // Reset to first page on search - }; - - const handleStatusFilter = (checkedValues: string[]) => { - setStatusFilters(checkedValues); - onPageChange(1); // Reset to first page on filter change - }; - - const handleTableChange = ( - _pagination: unknown, - _filters: unknown, - sorterParam: unknown - ) => { - // Type guard: ensure sorterParam matches expected shape - const sorter = sorterParam as { - field?: string | string[]; - order?: "ascend" | "descend"; - }; + const navigate = useNavigate(); + + const [searchText, setSearchText] = useState(""); + const [statusFilters, setStatusFilters] = useState([]); + const [sorter, setSorter] = useState<{ + field: string | null; + order: "ascend" | "descend" | null; + }>({ field: null, order: null }); + const pageSize = 50; // in BRD - setSorter({ - field: Array.isArray(sorter.field) - ? sorter.field[0] ?? null - : sorter.field ?? null, - order: sorter.order ?? null, + const { data, loading, error, refetch } = useQuery( + AdminUsersTableContainerAllUsersDocument, + { + variables: { + page: currentPage, + pageSize: pageSize, + searchText: searchText, + statusFilters: statusFilters, + sorter: + sorter.field && sorter.order + ? { field: sorter.field, order: sorter.order } + : undefined, + }, + fetchPolicy: "network-only", + } + ); + + const [blockUser, { loading: blockLoading }] = useMutation(BlockUserDocument, { + onCompleted: () => { + message.success("User blocked successfully"); + refetch(); + }, + onError: (err) => { + message.error(`Failed to block user: ${err.message}`); + }, }); - }; - const handleAction = async ( - action: "block" | "unblock" | "view-profile" | "view-report", - userId: string - ) => { - console.log(`Action: ${action}, User ID: ${userId}`); + const [unblockUser, { loading: unblockLoading }] = useMutation(UnblockUserDocument, { + onCompleted: () => { + message.success("User unblocked successfully"); + refetch(); + }, + onError: (err) => { + message.error(`Failed to unblock user: ${err.message}`); + }, + }); + + // Transform GraphQL data to match AdminUserData structure + const users = (data?.allUsers?.items ?? []).map((user) => ({ + id: user.id, + username: user.account?.username ?? "N/A", + firstName: user.account?.profile?.firstName ?? "N/A", + lastName: user.account?.profile?.lastName ?? "N/A", + email: user.account?.email ?? "N/A", + accountCreated: user.createdAt ?? "Unknown", + status: user.isBlocked ? ("Blocked" as const) : ("Active" as const), + isBlocked: user.isBlocked ?? false, + userType: user.userType ?? "unknown", + reportCount: 0, // TODO: Add reportCount to GraphQL query once available + })); + const total = data?.allUsers?.total ?? 0; + + const handleSearch = (value: string) => { + setSearchText(value); + onPageChange(1); // Reset to first page on search + }; + + const handleStatusFilter = (checkedValues: string[]) => { + setStatusFilters(checkedValues); + onPageChange(1); // Reset to first page on filter change + }; + + const handleTableChange = ( + _pagination: unknown, + _filters: unknown, + sorterParam: unknown + ) => { + // Type guard: ensure sorterParam matches expected shape + const sorter = sorterParam as { + field?: string | string[]; + order?: "ascend" | "descend"; + }; - switch (action) { - case "block": + setSorter({ + field: Array.isArray(sorter.field) + ? sorter.field[0] ?? null + : sorter.field ?? null, + order: sorter.order ?? null, + }); + }; + + const handleBlockUser = async (userId: string) => { try { - await blockUser({ variables: { userId } }); + await blockUser({ variables: { userId } }); } catch (err) { - // Error handled by mutation onError callback - console.error("Block user error:", err); + console.error("Block user error:", err); } - break; - case "unblock": + }; + + const handleUnblockUser = async (userId: string) => { try { - await unblockUser({ variables: { userId } }); + await unblockUser({ variables: { userId } }); } catch (err) { - // Error handled by mutation onError callback - console.error("Unblock user error:", err); + console.error("Unblock user error:", err); } - break; - case "view-profile": - message.info(`TODO: Navigate to user profile for user ${userId}`); - // TODO: Navigate to user profile page - break; - case "view-report": + }; + + const handleViewProfile = (userId: string) => { + navigate(`/account/profile/${userId}`); + }; + + const handleViewReport = (userId: string) => { message.info(`TODO: Navigate to user reports for user ${userId}`); // TODO: Navigate to user reports page - break; - } - }; - - return ( - { + console.log(`Action: ${action}, User ID: ${userId}`); + switch (action) { + case "block": + await handleBlockUser(userId); + break; + case "unblock": + await handleUnblockUser(userId); + break; + case "view-profile": + handleViewProfile(userId); + break; + case "view-report": + handleViewReport(userId); + break; + } + }; + const isLoading = loading || blockLoading || unblockLoading; + + return ( + + } /> - } - /> - ); + ); } diff --git a/apps/ui-sharethrift/src/components/layouts/home/account/admin-dashboard/components/admin-users-table/admin-users-table.tsx b/apps/ui-sharethrift/src/components/layouts/home/account/admin-dashboard/components/admin-users-table/admin-users-table.tsx index 5bfad49c5..520f1d804 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/account/admin-dashboard/components/admin-users-table/admin-users-table.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/account/admin-dashboard/components/admin-users-table/admin-users-table.tsx @@ -1,355 +1,279 @@ -import { Input, Checkbox, Button, Tag, Modal, Form, Select } from 'antd'; -import type { TableProps } from 'antd'; -import { SearchOutlined, FilterOutlined } from '@ant-design/icons'; -import { Dashboard } from '@sthrift/ui-components'; +import { Input, Checkbox, Button, Tag } from "antd"; +import type { TableProps } from "antd"; +import { SearchOutlined, FilterOutlined } from "@ant-design/icons"; +import { Dashboard } from "@sthrift/ui-components"; import type { - AdminUserData, - AdminUsersTableProps, -} from './admin-users-table.types.ts'; -import { AdminUsersCard } from './admin-users-card.tsx'; -import { useState } from 'react'; + AdminUserData, + AdminUsersTableProps, +} from "./admin-users-table.types.ts"; +import { AdminUsersCard } from "./admin-users-card.tsx"; +import { useState } from "react"; +import { type BlockUserFormValues, + BlockUserModal } from "../../../../../../shared/user-modals/block-user-modal.tsx"; +import { UnblockUserModal } from "../../../../../../shared/user-modals/unblock-user-modal.tsx"; +import { getUserDisplayName } from "../../../../../../shared/user-display-name.ts"; -const { Search, TextArea } = Input; +const { Search } = Input; const STATUS_OPTIONS = [ - { label: 'Active', value: 'Active' }, - { label: 'Blocked', value: 'Blocked' }, -]; - -const BLOCK_REASONS = [ - 'Late Return', - 'Item Damage', - 'Policy Violation', - 'Inappropriate Behavior', - 'Other', -]; - -const BLOCK_DURATIONS = [ - { label: '7 Days', value: '7' }, - { label: '30 Days', value: '30' }, - { label: 'Indefinite', value: 'indefinite' }, + { label: "Active", value: "Active" }, + { label: "Blocked", value: "Blocked" }, ]; export const AdminUsersTable: React.FC> = ({ - data, - searchText, - statusFilters, - sorter, - currentPage, - pageSize, - total, - loading = false, - onSearch, - onStatusFilter, - onTableChange, - onPageChange, - onAction, + data, + searchText, + statusFilters, + sorter, + currentPage, + pageSize, + total, + loading = false, + onSearch, + onStatusFilter, + onTableChange, + onPageChange, + onAction }) => { - const [blockModalVisible, setBlockModalVisible] = useState(false); - const [unblockModalVisible, setUnblockModalVisible] = useState(false); - const [selectedUser, setSelectedUser] = useState(null); - const [blockForm] = Form.useForm(); + const [blockModalVisible, setBlockModalVisible] = useState(false); + const [unblockModalVisible, setUnblockModalVisible] = useState(false); + const [selectedUser, setSelectedUser] = useState(null); - const handleBlockUser = (user: AdminUserData) => { - setSelectedUser(user); - setBlockModalVisible(true); - }; + const handleBlockUser = (user: AdminUserData) => { + setSelectedUser(user); + setBlockModalVisible(true); + }; - const handleUnblockUser = (user: AdminUserData) => { - setSelectedUser(user); - setUnblockModalVisible(true); - }; + const handleUnblockUser = (user: AdminUserData) => { + setSelectedUser(user); + setUnblockModalVisible(true); + }; - const handleBlockConfirm = async () => { - try { - const values = await blockForm.validateFields(); - console.log('Block user with:', values); - // Mutation is handled by the container via onAction - onAction('block', selectedUser?.id ?? ''); - setBlockModalVisible(false); - blockForm.resetFields(); - } catch (error) { - console.error('Block validation failed:', error); - } - }; + const handleBlockConfirm = (_blockUserFormValues: BlockUserFormValues) => { + // TODO: wire _blockUserFormValues's values through to the backend when supported + onAction("block", selectedUser?.id ?? ""); + setBlockModalVisible(false); + }; - const handleUnblockConfirm = () => { - onAction('unblock', selectedUser?.id ?? ''); - setUnblockModalVisible(false); - }; + const handleUnblockConfirm = () => { + onAction("unblock", selectedUser?.id ?? ""); + setUnblockModalVisible(false); + }; - const getActionButtons = (record: AdminUserData) => { - const commonActions = [ - , - , - ]; + const getActionButtons = (record: AdminUserData) => { + const commonActions = [ + , + , + ]; - const statusAction = - record.status === "Blocked" ? ( - - ) : ( - - ); + const statusAction = + record.status === "Blocked" ? ( + + ) : ( + + ); - return [...commonActions, statusAction]; - }; + return [...commonActions, statusAction]; + }; - const columns: TableProps['columns'] = [ - { - title: 'Username', - dataIndex: 'username', - key: 'username', - width: 150, - filterDropdown: ({ setSelectedKeys, selectedKeys, confirm }) => ( -
- { - setSelectedKeys(e.target.value ? [e.target.value] : []); - }} - onSearch={(value) => { - confirm(); - onSearch(value); - }} - style={{ width: 200 }} - allowClear - /> -
- ), - filterIcon: (filtered: boolean) => ( - - ), - render: (username: string) => ( - {username || 'N/A'} - ), - }, - { - title: 'First Name', - dataIndex: 'firstName', - key: 'firstName', - sorter: true, - sortOrder: sorter.field === 'firstName' ? sorter.order : null, - }, - { - title: 'Last Name', - dataIndex: 'lastName', - key: 'lastName', - sorter: true, - sortOrder: sorter.field === 'lastName' ? sorter.order : null, - }, - { - title: 'Account Creation', - dataIndex: 'accountCreated', - key: 'accountCreated', - sorter: true, - sortOrder: sorter.field === 'accountCreated' ? sorter.order : null, - render: (date?: string | null) => { - // Guard: handle missing/invalid dates gracefully - if (!date) return N/A; + const columns: TableProps["columns"] = [ + { + title: "Username", + dataIndex: "username", + key: "username", + width: 150, + filterDropdown: ({ setSelectedKeys, selectedKeys, confirm }) => ( +
+ { + setSelectedKeys(e.target.value ? [e.target.value] : []); + }} + onSearch={(value) => { + confirm(); + onSearch(value); + }} + style={{ width: 200 }} + allowClear + /> +
+ ), + filterIcon: (filtered: boolean) => ( + + ), + render: (username: string) => ( + {username || "N/A"} + ), + }, + { + title: "First Name", + dataIndex: "firstName", + key: "firstName", + sorter: true, + sortOrder: sorter.field === "firstName" ? sorter.order : null, + }, + { + title: "Last Name", + dataIndex: "lastName", + key: "lastName", + sorter: true, + sortOrder: sorter.field === "lastName" ? sorter.order : null, + }, + { + title: "Account Creation", + dataIndex: "accountCreated", + key: "accountCreated", + sorter: true, + sortOrder: sorter.field === "accountCreated" ? sorter.order : null, + render: (date?: string | null) => { + // Guard: handle missing/invalid dates gracefully + if (!date) return N/A; - const d = new Date(date); - if (Number.isNaN(d.getTime())) { - return N/A; - } + const d = new Date(date); + if (Number.isNaN(d.getTime())) { + return N/A; + } - const yyyy = d.getFullYear(); - const mm = String(d.getMonth() + 1).padStart(2, '0'); - const dd = String(d.getDate()).padStart(2, '0'); - return ( - - {`${yyyy}-${mm}-${dd}`} - - ); - }, - }, - { - title: 'Status', - dataIndex: 'status', - key: 'status', - filterDropdown: ({ confirm }) => ( -
-
- Filter by Status -
- { - onStatusFilter(checkedValues); - confirm(); - }} - style={{ display: 'flex', flexDirection: 'column', gap: 8 }} - /> -
- ), - filterIcon: (filtered: boolean) => ( - - ), - render: (status: string) => ( - {status} - ), - }, - { - title: 'Actions', - key: 'actions', - width: 300, - render: (_: unknown, record: AdminUserData) => { - const actions = getActionButtons(record); - return ( -
- {actions} -
- ); - }, - }, - ]; + const yyyy = d.getFullYear(); + const mm = String(d.getMonth() + 1).padStart(2, "0"); + const dd = String(d.getDate()).padStart(2, "0"); + return ( + + {`${yyyy}-${mm}-${dd}`} + + ); + }, + }, + { + title: "Status", + dataIndex: "status", + key: "status", + filterDropdown: ({ confirm }) => ( +
+
+ Filter by Status +
+ { + onStatusFilter(checkedValues); + confirm(); + }} + style={{ display: "flex", flexDirection: "column", gap: 8 }} + /> +
+ ), + filterIcon: (filtered: boolean) => ( + + ), + render: (status: string) => ( + {status} + ), + }, + { + title: "Actions", + key: "actions", + width: 300, + render: (_: unknown, record: AdminUserData) => { + const actions = getActionButtons(record); + return ( +
+ {actions} +
+ ); + }, + }, + ]; - return ( - <> - ( - { - if (action === 'block') { - handleBlockUser(item); - } else if (action === 'unblock') { - handleUnblockUser(item); - } else { - onAction(action, item.id); - } - }} - /> - )} - /> + return ( + <> + ( + { + if (action === "block") { + handleBlockUser(item); + } else if (action === "unblock") { + handleUnblockUser(item); + } else { + onAction(action, item.id); + } + }} + /> + )} + /> - {/* Block User Modal */} - { - setBlockModalVisible(false); - blockForm.resetFields(); - }} - okText="Block User" - okButtonProps={{ danger: true }} - > -

- You are about to block {selectedUser?.username}. This - will prevent them from creating listings or making reservations. -

-
- - - - - - - -