161 lines
5.1 KiB
TypeScript
161 lines
5.1 KiB
TypeScript
import React, { useEffect, useMemo, useState } from 'react';
|
||
|
||
import Alert from '@mui/material/Alert';
|
||
import Box from '@mui/material/Box';
|
||
import CircularProgress from '@mui/material/CircularProgress';
|
||
import Pagination from '@mui/material/Pagination';
|
||
import Stack from '@mui/material/Stack';
|
||
import Table from '@mui/material/Table';
|
||
import TableBody from '@mui/material/TableBody';
|
||
import TableCell from '@mui/material/TableCell';
|
||
import TableHead from '@mui/material/TableHead';
|
||
import TableRow from '@mui/material/TableRow';
|
||
import Typography from '@mui/material/Typography';
|
||
|
||
import { leadsApi, type LeadListItem } from '../../../../api/index.ts';
|
||
import { ApiError } from '../../../../api/httpClient.ts';
|
||
|
||
const pageSize = 10;
|
||
|
||
const formatDateTime = (value: string) => {
|
||
const timestamp = new Date(value);
|
||
if (Number.isNaN(timestamp.getTime())) {
|
||
return '—';
|
||
}
|
||
|
||
return new Intl.DateTimeFormat('ru-RU', {
|
||
day: '2-digit',
|
||
month: 'short',
|
||
year: 'numeric',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
}).format(timestamp);
|
||
};
|
||
|
||
export const AdminDashboardLeads: React.FC = () => {
|
||
const [items, setItems] = useState<LeadListItem[]>([]);
|
||
const [page, setPage] = useState(1);
|
||
const [total, setTotal] = useState(0);
|
||
const [limit, setLimit] = useState(pageSize);
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
useEffect(() => {
|
||
let isMounted = true;
|
||
const load = async () => {
|
||
setIsLoading(true);
|
||
setError(null);
|
||
try {
|
||
const response = await leadsApi.list({
|
||
limit: pageSize,
|
||
page,
|
||
});
|
||
if (!isMounted) {
|
||
return;
|
||
}
|
||
setItems(response.items);
|
||
setTotal(response.total);
|
||
setLimit(response.limit || pageSize);
|
||
} catch (err) {
|
||
if (!isMounted) {
|
||
return;
|
||
}
|
||
const message = err instanceof ApiError ? err.message : 'Не удалось загрузить лиды. Попробуйте позже.';
|
||
setError(message);
|
||
} finally {
|
||
if (isMounted) {
|
||
setIsLoading(false);
|
||
}
|
||
}
|
||
};
|
||
|
||
void load();
|
||
|
||
return () => {
|
||
isMounted = false;
|
||
};
|
||
}, [page]);
|
||
|
||
const totalPages = useMemo(() => {
|
||
if (total > 0 && limit > 0) {
|
||
return Math.max(Math.ceil(total / limit), 1);
|
||
}
|
||
return items.length > 0 ? page : 0;
|
||
}, [items.length, limit, page, total]);
|
||
|
||
useEffect(() => {
|
||
if (!isLoading && totalPages > 0 && page > totalPages) {
|
||
setPage(totalPages);
|
||
}
|
||
}, [isLoading, page, totalPages]);
|
||
|
||
return (
|
||
<Stack spacing={3}>
|
||
<div>
|
||
<Typography variant="h5">Лиды и заявки</Typography>
|
||
<Typography color="text.secondary">Просматривайте обращения клиентов и назначайте ответственных.</Typography>
|
||
</div>
|
||
|
||
{isLoading && (
|
||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
||
<CircularProgress size={28} />
|
||
</Box>
|
||
)}
|
||
|
||
{error && <Alert severity="error">{error}</Alert>}
|
||
|
||
{!isLoading && !error && items.length === 0 && (
|
||
<Typography color="text.secondary" textAlign="center">
|
||
Пока нет новых заявок. Как только клиенты оставят контакты, они появятся здесь.
|
||
</Typography>
|
||
)}
|
||
|
||
{!isLoading && !error && items.length > 0 && (
|
||
<Table
|
||
size="small"
|
||
sx={{
|
||
border: 1,
|
||
borderColor: 'divider',
|
||
borderRadius: 1,
|
||
overflow: 'hidden',
|
||
'& th, & td': {
|
||
borderRight: '1px solid',
|
||
borderColor: 'divider',
|
||
'&:last-of-type': {
|
||
borderRight: 'none',
|
||
},
|
||
},
|
||
}}
|
||
>
|
||
<TableHead>
|
||
<TableRow sx={{ backgroundColor: 'action.hover' }}>
|
||
<TableCell sx={{ fontWeight: 600 }}>Дата и время</TableCell>
|
||
<TableCell sx={{ fontWeight: 600 }}>Имя</TableCell>
|
||
<TableCell sx={{ fontWeight: 600 }}>Email</TableCell>
|
||
<TableCell sx={{ fontWeight: 600 }}>Телефон</TableCell>
|
||
</TableRow>
|
||
</TableHead>
|
||
<TableBody>
|
||
{items.map((item) => (
|
||
<TableRow key={item.id} hover sx={{ '& td': { borderBottom: '1px solid', borderColor: 'divider' } }}>
|
||
<TableCell>{formatDateTime(item.createdAt ?? '')}</TableCell>
|
||
<TableCell>
|
||
<Typography fontWeight={600}>{item.fullName}</Typography>
|
||
</TableCell>
|
||
<TableCell>{item.email}</TableCell>
|
||
<TableCell>{item.phone ?? '—'}</TableCell>
|
||
</TableRow>
|
||
))}
|
||
</TableBody>
|
||
</Table>
|
||
)}
|
||
|
||
{!isLoading && !error && totalPages > 1 && (
|
||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||
<Pagination page={page} onChange={(_, value) => setPage(value)} count={totalPages} color="primary" showFirstButton showLastButton />
|
||
</Box>
|
||
)}
|
||
</Stack>
|
||
);
|
||
};
|