Add docker
This commit is contained in:
160
src/modules/admin/components/leads/dashboard-leads.tsx
Normal file
160
src/modules/admin/components/leads/dashboard-leads.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user