Community 93 Premium feature complete
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* @ai-summary Community station review card for admin approval workflow
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardActions,
|
||||
Typography,
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField,
|
||||
Chip,
|
||||
Grid,
|
||||
} from '@mui/material';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import { CommunityStation } from '../../stations/types/community-stations.types';
|
||||
|
||||
interface CommunityStationReviewCardProps {
|
||||
station: CommunityStation;
|
||||
onApprove: (id: string) => void;
|
||||
onReject: (id: string, reason: string) => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Card showing full station details for admin review
|
||||
* Responsive design with 44px minimum touch targets
|
||||
*/
|
||||
export const CommunityStationReviewCard: React.FC<CommunityStationReviewCardProps> = ({
|
||||
station,
|
||||
onApprove,
|
||||
onReject,
|
||||
isLoading = false,
|
||||
}) => {
|
||||
const [openRejectDialog, setOpenRejectDialog] = useState(false);
|
||||
const [rejectionReason, setRejectionReason] = useState('');
|
||||
|
||||
const handleApprove = () => {
|
||||
onApprove(station.id);
|
||||
};
|
||||
|
||||
const handleRejectClick = () => {
|
||||
setOpenRejectDialog(true);
|
||||
};
|
||||
|
||||
const handleRejectConfirm = () => {
|
||||
if (rejectionReason.trim()) {
|
||||
onReject(station.id, rejectionReason);
|
||||
setOpenRejectDialog(false);
|
||||
setRejectionReason('');
|
||||
}
|
||||
};
|
||||
|
||||
const octaneLabel = station.has93Octane
|
||||
? station.has93OctaneEthanolFree
|
||||
? '93 Octane · Ethanol Free'
|
||||
: '93 Octane · w/ Ethanol'
|
||||
: 'No 93 Octane';
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
sx={{
|
||||
transition: 'transform 0.2s, box-shadow 0.2s',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: 3,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
{/* Station name and location */}
|
||||
<Typography variant="h6" sx={{ mb: 1 }}>
|
||||
{station.name}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ mb: 2, display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{station.address}
|
||||
</Typography>
|
||||
{(station.city || station.state || station.zipCode) && (
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{[station.city, station.state, station.zipCode].filter(Boolean).join(', ')}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Details grid */}
|
||||
<Grid container spacing={1} sx={{ mb: 2 }}>
|
||||
{station.brand && (
|
||||
<Grid item xs={6}>
|
||||
<Typography variant="caption" color="textSecondary" display="block">
|
||||
Brand
|
||||
</Typography>
|
||||
<Typography variant="body2">{station.brand}</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={station.brand ? 6 : 12}>
|
||||
<Typography variant="caption" color="textSecondary" display="block">
|
||||
93 Octane Status
|
||||
</Typography>
|
||||
<Chip label={octaneLabel} size="small" color="success" />
|
||||
</Grid>
|
||||
{station.price93 && (
|
||||
<Grid item xs={6}>
|
||||
<Typography variant="caption" color="textSecondary" display="block">
|
||||
Price
|
||||
</Typography>
|
||||
<Typography variant="body2">${station.price93.toFixed(3)}</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
{/* Coordinates */}
|
||||
<Box sx={{ mb: 2, p: 1, bgcolor: 'grey.100', borderRadius: 1 }}>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
Latitude: {station.latitude.toFixed(8)}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="textSecondary" display="block">
|
||||
Longitude: {station.longitude.toFixed(8)}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Notes */}
|
||||
{station.notes && (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
Notes:
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ p: 1, bgcolor: 'grey.50', borderRadius: 1, mt: 0.5 }}>
|
||||
{station.notes}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Submission info */}
|
||||
<Box sx={{ pt: 1, borderTop: '1px solid #e0e0e0' }}>
|
||||
<Typography variant="caption" color="textSecondary" display="block">
|
||||
Submitted by: {station.submittedBy}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="textSecondary" display="block">
|
||||
Date: {new Date(station.createdAt).toLocaleDateString()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</CardContent>
|
||||
|
||||
{/* Action buttons */}
|
||||
<CardActions sx={{ gap: 1, minHeight: '44px', justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={handleRejectClick}
|
||||
disabled={isLoading}
|
||||
startIcon={<CancelIcon />}
|
||||
sx={{ minHeight: '44px' }}
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="success"
|
||||
onClick={handleApprove}
|
||||
disabled={isLoading}
|
||||
startIcon={<CheckCircleIcon />}
|
||||
sx={{ minHeight: '44px' }}
|
||||
>
|
||||
Approve
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
|
||||
{/* Rejection dialog */}
|
||||
<Dialog open={openRejectDialog} onClose={() => setOpenRejectDialog(false)} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Reject Station Submission</DialogTitle>
|
||||
<DialogContent sx={{ pt: 2 }}>
|
||||
<TextField
|
||||
autoFocus
|
||||
fullWidth
|
||||
multiline
|
||||
rows={4}
|
||||
label="Rejection Reason"
|
||||
placeholder="e.g., Incorrect location, duplicate entry, invalid address..."
|
||||
value={rejectionReason}
|
||||
onChange={(e) => setRejectionReason(e.target.value)}
|
||||
variant="outlined"
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setOpenRejectDialog(false)}>Cancel</Button>
|
||||
<Button
|
||||
onClick={handleRejectConfirm}
|
||||
variant="contained"
|
||||
color="error"
|
||||
disabled={!rejectionReason.trim() || isLoading}
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommunityStationReviewCard;
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* @ai-summary Queue of pending community station submissions for admin review
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
CircularProgress,
|
||||
Alert,
|
||||
Grid,
|
||||
Pagination,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { CommunityStation } from '../../stations/types/community-stations.types';
|
||||
import { CommunityStationReviewCard } from './CommunityStationReviewCard';
|
||||
|
||||
interface CommunityStationReviewQueueProps {
|
||||
stations: CommunityStation[];
|
||||
loading?: boolean;
|
||||
error?: string | null;
|
||||
onApprove: (id: string) => void;
|
||||
onReject: (id: string, reason: string) => void;
|
||||
page?: number;
|
||||
onPageChange?: (page: number) => void;
|
||||
totalPages?: number;
|
||||
isReviewLoading?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Review queue for pending community station submissions
|
||||
* Responsive grid layout with pagination
|
||||
*/
|
||||
export const CommunityStationReviewQueue: React.FC<CommunityStationReviewQueueProps> = ({
|
||||
stations,
|
||||
loading = false,
|
||||
error = null,
|
||||
onApprove,
|
||||
onReject,
|
||||
page = 0,
|
||||
onPageChange,
|
||||
totalPages = 1,
|
||||
isReviewLoading = false,
|
||||
}) => {
|
||||
if (loading) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '300px' }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert severity="error">
|
||||
{error}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (stations.length === 0) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: '300px',
|
||||
flexDirection: 'column',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" color="textSecondary">
|
||||
No pending submissions
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
All community stations have been reviewed
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/* Stats header */}
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Pending review: {stations.length} submission{stations.length !== 1 ? 's' : ''}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Grid of cards */}
|
||||
<Grid container spacing={2}>
|
||||
{stations.map((station) => (
|
||||
<Grid item xs={12} sm={6} md={4} key={station.id}>
|
||||
<CommunityStationReviewCard
|
||||
station={station}
|
||||
onApprove={onApprove}
|
||||
onReject={onReject}
|
||||
isLoading={isReviewLoading}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
{/* Pagination */}
|
||||
{onPageChange && totalPages > 1 && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}>
|
||||
<Pagination
|
||||
count={totalPages}
|
||||
page={page + 1}
|
||||
onChange={(_, newPage) => onPageChange(newPage - 1)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommunityStationReviewQueue;
|
||||
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* @ai-summary Mobile admin screen for community station reviews
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import {
|
||||
Box,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
} from '@mui/material';
|
||||
import { MobileContainer } from '../../../shared-minimal/components/mobile/MobileContainer';
|
||||
import { useAdminAccess } from '../../../core/auth/useAdminAccess';
|
||||
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';
|
||||
|
||||
type TabType = 'pending' | 'all';
|
||||
|
||||
/**
|
||||
* Mobile admin screen for reviewing community station submissions
|
||||
* Touch-friendly layout with tab navigation
|
||||
*/
|
||||
export const AdminCommunityStationsMobileScreen: React.FC = () => {
|
||||
const { isAdmin, loading } = useAdminAccess();
|
||||
const [activeTab, setActiveTab] = useState<TabType>('pending');
|
||||
const [statusFilter, setStatusFilter] = useState<string>('');
|
||||
const [page, setPage] = useState(0);
|
||||
|
||||
// Hooks
|
||||
const pendingSubmissions = usePendingSubmissions(page, 20);
|
||||
const allSubmissions = useAllCommunitySubmissions(statusFilter || undefined, page, 20);
|
||||
const reviewMutation = useReviewStation();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<MobileContainer>
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-center">
|
||||
<div className="text-slate-500 mb-2">Loading admin access...</div>
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
</MobileContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAdmin) {
|
||||
return <Navigate to="/garage/settings" replace />;
|
||||
}
|
||||
|
||||
// Handle approval
|
||||
const handleApprove = 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');
|
||||
}
|
||||
};
|
||||
|
||||
// Handle rejection
|
||||
const handleReject = 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');
|
||||
}
|
||||
};
|
||||
|
||||
const displayData = activeTab === 'pending' ? pendingSubmissions : allSubmissions;
|
||||
const stations = displayData.data?.stations || [];
|
||||
const totalPages = displayData.data?.total ? Math.ceil(displayData.data.total / 20) : 1;
|
||||
|
||||
return (
|
||||
<MobileContainer>
|
||||
<div className="space-y-4 pb-20 p-4">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-6">
|
||||
<h1 className="text-2xl font-bold text-slate-800">Community Station Reviews</h1>
|
||||
<p className="text-slate-500 mt-1">Review user-submitted gas stations</p>
|
||||
</div>
|
||||
|
||||
{/* Tab navigation */}
|
||||
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveTab('pending');
|
||||
setPage(0);
|
||||
}}
|
||||
className={`flex-1 py-2 px-3 rounded-lg font-medium transition ${
|
||||
activeTab === 'pending'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-slate-100 text-slate-600'
|
||||
}`}
|
||||
>
|
||||
Pending ({pendingSubmissions.data?.total || 0})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveTab('all');
|
||||
setPage(0);
|
||||
}}
|
||||
className={`flex-1 py-2 px-3 rounded-lg font-medium transition ${
|
||||
activeTab === 'all'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-slate-100 text-slate-600'
|
||||
}`}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
</Box>
|
||||
|
||||
{/* Status filter for all tab */}
|
||||
{activeTab === 'all' && (
|
||||
<FormControl fullWidth size="small" sx={{ mb: 2 }}>
|
||||
<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>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
{activeTab === 'pending' ? (
|
||||
<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}
|
||||
/>
|
||||
) : (
|
||||
<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}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</MobileContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminCommunityStationsMobileScreen;
|
||||
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