Community 93 Premium feature complete
This commit is contained in:
236
frontend/src/features/admin/pages/AdminCommunityStationsPage.tsx
Normal file
236
frontend/src/features/admin/pages/AdminCommunityStationsPage.tsx
Normal file
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* @ai-summary Admin desktop page for managing community station submissions
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Paper,
|
||||
Tabs,
|
||||
Tab,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
Grid,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { AdminSectionHeader } from '../components/AdminSectionHeader';
|
||||
import { CommunityStationReviewQueue } from '../components/CommunityStationReviewQueue';
|
||||
import { CommunityStationsList } from '../../stations/components/CommunityStationsList';
|
||||
import {
|
||||
usePendingSubmissions,
|
||||
useAllCommunitySubmissions,
|
||||
useReviewStation,
|
||||
} from '../../stations/hooks/useCommunityStations';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
index: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
const TabPanel: React.FC<TabPanelProps> = ({ children, value, index }) => {
|
||||
if (value !== index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div role="tabpanel">
|
||||
<Box sx={{ padding: 2 }}>{children}</Box>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Admin page for reviewing and managing community station submissions
|
||||
* Desktop layout with tab navigation and status filtering
|
||||
*/
|
||||
export const AdminCommunityStationsPage: React.FC = () => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
const [statusFilter, setStatusFilter] = useState<string>('');
|
||||
const [page, setPage] = useState(0);
|
||||
|
||||
// Hooks
|
||||
const pendingSubmissions = usePendingSubmissions(page, 50);
|
||||
const allSubmissions = useAllCommunitySubmissions(statusFilter || undefined, page, 50);
|
||||
const reviewMutation = useReviewStation();
|
||||
|
||||
// Determine which data to display
|
||||
const displayData = tabValue === 0 ? pendingSubmissions : allSubmissions;
|
||||
const stations = displayData.data?.stations || [];
|
||||
const totalPages = displayData.data?.total
|
||||
? Math.ceil(displayData.data.total / 50)
|
||||
: 1;
|
||||
|
||||
// Handle approval
|
||||
const handleApprove = useCallback(
|
||||
async (id: string) => {
|
||||
try {
|
||||
await reviewMutation.mutateAsync({
|
||||
id,
|
||||
decision: { status: 'approved' },
|
||||
});
|
||||
toast.success('Station approved');
|
||||
} catch (error: any) {
|
||||
toast.error(error?.response?.data?.message || 'Failed to approve station');
|
||||
}
|
||||
},
|
||||
[reviewMutation]
|
||||
);
|
||||
|
||||
// Handle rejection
|
||||
const handleReject = useCallback(
|
||||
async (id: string, reason: string) => {
|
||||
try {
|
||||
await reviewMutation.mutateAsync({
|
||||
id,
|
||||
decision: { status: 'rejected', rejectionReason: reason },
|
||||
});
|
||||
toast.success('Station rejected');
|
||||
} catch (error: any) {
|
||||
toast.error(error?.response?.data?.message || 'Failed to reject station');
|
||||
}
|
||||
},
|
||||
[reviewMutation]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/* Header */}
|
||||
<AdminSectionHeader
|
||||
title="Community Gas Station Reviews"
|
||||
stats={[
|
||||
{ label: 'Pending', value: pendingSubmissions.data?.total || 0 },
|
||||
{ label: 'Total', value: allSubmissions.data?.total || 0 }
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Main content */}
|
||||
<Paper sx={{ m: 2 }}>
|
||||
<Tabs
|
||||
value={tabValue}
|
||||
onChange={(_, newValue) => setTabValue(newValue)}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
aria-label="admin community stations tabs"
|
||||
variant={isMobile ? 'scrollable' : 'standard'}
|
||||
>
|
||||
<Tab label={`Pending (${pendingSubmissions.data?.total || 0})`} id="tab-0" />
|
||||
<Tab label="All Submissions" id="tab-1" />
|
||||
</Tabs>
|
||||
|
||||
{/* Pending tab */}
|
||||
<TabPanel value={tabValue} index={0}>
|
||||
<CommunityStationReviewQueue
|
||||
stations={stations}
|
||||
loading={displayData.isLoading}
|
||||
error={displayData.error ? 'Failed to load submissions' : null}
|
||||
onApprove={handleApprove}
|
||||
onReject={handleReject}
|
||||
page={page}
|
||||
onPageChange={setPage}
|
||||
totalPages={totalPages}
|
||||
isReviewLoading={reviewMutation.isPending}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
{/* All submissions tab */}
|
||||
<TabPanel value={tabValue} index={1}>
|
||||
<Grid container spacing={2} sx={{ mb: 2 }}>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel>Filter by Status</InputLabel>
|
||||
<Select
|
||||
value={statusFilter}
|
||||
label="Filter by Status"
|
||||
onChange={(e) => {
|
||||
setStatusFilter(e.target.value);
|
||||
setPage(0);
|
||||
}}
|
||||
>
|
||||
<MenuItem value="">All</MenuItem>
|
||||
<MenuItem value="pending">Pending</MenuItem>
|
||||
<MenuItem value="approved">Approved</MenuItem>
|
||||
<MenuItem value="rejected">Rejected</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<CommunityStationsList
|
||||
stations={stations}
|
||||
loading={displayData.isLoading}
|
||||
error={displayData.error ? 'Failed to load submissions' : null}
|
||||
isAdmin={true}
|
||||
onApprove={handleApprove}
|
||||
onReject={handleReject}
|
||||
page={page}
|
||||
onPageChange={setPage}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Paper>
|
||||
|
||||
{/* Stats card */}
|
||||
{pendingSubmissions.data && (
|
||||
<Paper sx={{ m: 2, p: 2 }}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<Box>
|
||||
<Typography variant="h4" color="primary">
|
||||
{pendingSubmissions.data.total}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Pending Review
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
{allSubmissions.data && (
|
||||
<>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<Box>
|
||||
<Typography variant="h4" color="success.main">
|
||||
{allSubmissions.data.stations.filter((s) => s.status === 'approved').length}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Approved
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<Box>
|
||||
<Typography variant="h4" color="error.main">
|
||||
{allSubmissions.data.stations.filter((s) => s.status === 'rejected').length}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Rejected
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<Box>
|
||||
<Typography variant="h4">
|
||||
{allSubmissions.data.total}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Total Submitted
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
</Grid>
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminCommunityStationsPage;
|
||||
Reference in New Issue
Block a user