Add docker

This commit is contained in:
Evgenii Saenko
2025-12-17 11:51:25 +03:00
parent a01b46182e
commit 4f6700e0e2
110 changed files with 8838 additions and 181 deletions

View 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>
);
};