-
P
+
ParkTrack
{isRegister ? 'Регистрация' : 'Вход'}
diff --git a/src/pages/DashboardPage.tsx b/src/pages/DashboardPage.tsx
index 3abe571..b39a07e 100644
--- a/src/pages/DashboardPage.tsx
+++ b/src/pages/DashboardPage.tsx
@@ -3,13 +3,11 @@ import { api } from '@/api/client';
import { Button } from '@/components/UiKit';
import { navigate } from '@/router/routes';
import { useSessionStore } from '@/auth/sessionStore';
-import type { Camera, HealthResponse, VersionResponse } from '@/api/client';
+import type { Camera } from '@/api/client';
import type { ParkingZone } from '@/types';
type DashboardState = {
loading: boolean;
- health?: HealthResponse;
- version?: VersionResponse;
cameras: Camera[];
zones: ParkingZone[];
error?: string;
@@ -53,7 +51,6 @@ function formatDashboardLoadError(results: Array<{
}
export default function DashboardPage() {
- const session = useSessionStore();
const currentPartnerId = useSessionStore(state => state.currentPartnerId);
const [state, setState] = useState
({
loading: false,
@@ -67,9 +64,7 @@ export default function DashboardPage() {
async function load() {
setState(prev => ({ ...prev, loading: true, error: undefined }));
try {
- const [health, version, cameras, zones] = await Promise.allSettled([
- api.health(),
- api.version(),
+ const [cameras, zones] = await Promise.allSettled([
api.listCameras({ partner_id: currentPartnerId }),
api.listZones({ partner_id: currentPartnerId })
]);
@@ -78,13 +73,9 @@ export default function DashboardPage() {
setState({
loading: false,
- health: health.status === 'fulfilled' ? health.value : undefined,
- version: version.status === 'fulfilled' ? version.value : undefined,
cameras: cameras.status === 'fulfilled' ? cameras.value : [],
zones: zones.status === 'fulfilled' ? zones.value : [],
error: formatDashboardLoadError([
- { label: 'статус API', result: health },
- { label: 'версию API', result: version },
{ label: 'камеры', result: cameras, ignoreStatuses: [401, 403] },
{ label: 'зоны', result: zones }
])
@@ -185,56 +176,45 @@ export default function DashboardPage() {
{state.error && {state.error}
}
-
-
-
-
+
+
-
+
-
-
-
Сессия
-
-
- Пользователь
- {session.user?.full_name || session.user?.email || '—'}
-
-
- Глобальная роль
- {session.user?.global_role || '—'}
-
-
- Партнёры
- {session.user?.partner_memberships.filter(m => m.is_active !== false).length ?? 0}
-
-
- Permissions
- {session.user?.permissions.length ?? 0}
-
+
+
+
+
Зоны внимания
+ navigate('zones')}>Все зоны
-
+
+ {zoneWatchlist.map(zone => {
+ const freeCount = typeof zone.free_count === 'number'
+ ? zone.free_count
+ : typeof zone.occupied === 'number'
+ ? Math.max(0, zone.capacity - zone.occupied)
+ : '—';
-
-
Система
-
-
- API status
- {state.health?.status ?? 'unknown'}
-
-
- Database
- {state.health?.database ?? 'unknown'}
-
-
- API version
- {state.version?.api_version ?? state.version?.version ?? 'unknown'}
-
-
- Текущий partner
- {currentPartnerId ?? (session.isAdmin() ? 'Все партнёры' : '—')}
-
+ return (
+
navigate('zones')}
+ >
+
+
Зона #{String(zone.id)}
+
Камера #{zone.camera_id} · {zone.zone_type}
+
+
+ {freeCount} свободно
+ Уверенность: {formatConfidence(zone.confidence)}
+
+
+ );
+ })}
+ {!zoneWatchlist.length &&
Зоны пока не загружены.
}
@@ -243,19 +223,15 @@ export default function DashboardPage() {
navigate('cameras')}>
Проверить камеры
- Редактирование настроек, snapshot и позиции на карте.
navigate('zones')}>
Проверить зоны
- Список зон, geometry entrypoint’ы и правка свойств.
navigate('profile')}>
Открыть профиль
- Посмотреть текущую сессию и роль администратора.
navigate('users')}>
- Подготовить пользователей
- Контрактный раздел для следующего этапа admin-панели.
+ Управлять пользователями
@@ -276,10 +252,7 @@ export default function DashboardPage() {
{camera.title}
#{camera.camera_id}
-
- lat: {camera.latitude ?? '—'}
- lng: {camera.longitude ?? '—'}
-
+
Координаты не заданы
))}
{!camerasWithoutMap.length &&
Все камеры уже привязаны к карте.
}
@@ -298,11 +271,11 @@ export default function DashboardPage() {
>
Зона #{String(zone.id)}
-
Camera #{zone.camera_id}
+
Камера #{zone.camera_id}
- image: {(zone.image_polygon ?? zone.image_quad)?.length ?? 0}/4
- map: {zone.points.filter(point => point.latitude !== null && point.longitude !== null).length}/4
+ Кадр: {(zone.image_polygon ?? zone.image_quad)?.length ?? 0}/4
+ Карта: {zone.points.filter(point => point.latitude !== null && point.longitude !== null).length}/4
))}
@@ -311,7 +284,7 @@ export default function DashboardPage() {
-
Камеры под рукой
+
Недавно обновлённые камеры
{staleCameras.map(camera => (
- {camera.is_active === false ? 'paused' : 'active'}
+ {camera.is_active === false ? 'Неактивна' : 'Активна'}
{formatDate(camera.updated_at)}
@@ -335,38 +308,6 @@ export default function DashboardPage() {
{!staleCameras.length && Камеры пока не загружены.
}
-
-
-
Зоны внимания
-
- {zoneWatchlist.map(zone => {
- const freeCount = typeof zone.free_count === 'number'
- ? zone.free_count
- : typeof zone.occupied === 'number'
- ? Math.max(0, zone.capacity - zone.occupied)
- : '—';
-
- return (
-
navigate('zones')}
- >
-
-
Зона #{String(zone.id)}
-
Camera #{zone.camera_id} • {zone.zone_type}
-
-
- free: {freeCount}
- confidence: {formatConfidence(zone.confidence)}
-
-
- );
- })}
- {!zoneWatchlist.length &&
Зоны пока не загружены.
}
-
-
);
diff --git a/src/pages/PartnersAdminPage.tsx b/src/pages/PartnersAdminPage.tsx
index 80fc4f4..eca5d2a 100644
--- a/src/pages/PartnersAdminPage.tsx
+++ b/src/pages/PartnersAdminPage.tsx
@@ -5,6 +5,7 @@ import { Button, Field, Input, Select } from '@/components/UiKit';
import { BulkActionBar, BulkSelectionCheckbox } from '@/components/BulkActionBar';
import { useFeedbackStore } from '@/feedback/feedbackStore';
import { validateOptionalPhone } from '@/utils/phone';
+import { formatAccessScope, formatPartnerRole } from '@/utils/accessLabels';
import type { AccessScope } from '@/types';
type AdminPartner = {
@@ -414,7 +415,7 @@ export default function PartnersAdminPage() {
const confirmed = await confirmAction({
title: 'Удалить партнёра?',
- message: `Организация "${selectedPartner.name}" будет удалена из backend.`,
+ message: `Организация "${selectedPartner.name}" будет удалена без возможности восстановления.`,
confirmLabel: 'Удалить',
cancelLabel: 'Отмена',
tone: 'danger'
@@ -611,10 +612,6 @@ export default function PartnersAdminPage() {
Сотрудников
{members.length}
-
-
Current context
-
{currentPartnerId ?? 'all'}
-
@@ -717,7 +714,7 @@ export default function PartnersAdminPage() {
{partner.name}
{partner.slug}
- {partner.is_active ? 'active' : 'inactive'}
+ {partner.is_active ? 'Активен' : 'Неактивен'}
))}
@@ -839,11 +836,11 @@ export default function PartnersAdminPage() {
{!membersLoading && canViewMembers && (
- User
- Role
- Read
- Write
- Delete
+ Сотрудник
+ Роль
+ Чтение
+ Изменение
+ Удаление
{members.map(member => (
@@ -854,10 +851,10 @@ export default function PartnersAdminPage() {
onClick={() => setSelectedMemberUserId(member.user_id)}
>
{member.email}
-
{member.user_role}
-
{member.read_scope}
-
{member.write_scope}
-
{member.delete_scope}
+
{formatPartnerRole(member.user_role)}
+
{formatAccessScope(member.read_scope)}
+
{formatAccessScope(member.write_scope)}
+
{formatAccessScope(member.delete_scope)}
))}
{!members.length &&
У партнёра пока нет сотрудников.
}
@@ -871,7 +868,7 @@ export default function PartnersAdminPage() {
Доступ сотрудника
{selectedMember.email}
-
+
prev ? ({ ...prev, userRole: e.target.value }) : prev);
}}
>
- {memberRoleOptions.map(role => {role} )}
+ {memberRoleOptions.map(role => {formatPartnerRole(role)} )}
-
+
prev ? ({ ...prev, readScope: e.target.value }) : prev);
}}
>
- {scopeOptions.map(scope => {scope} )}
+ {scopeOptions.map(scope => {formatAccessScope(scope)} )}
-
+
prev ? ({ ...prev, writeScope: e.target.value }) : prev);
}}
>
- {scopeOptions.map(scope => {scope} )}
+ {scopeOptions.map(scope => {formatAccessScope(scope)} )}
-
+
prev ? ({ ...prev, deleteScope: e.target.value }) : prev);
}}
>
- {scopeOptions.map(scope => {scope} )}
+ {scopeOptions.map(scope => {formatAccessScope(scope)} )}
@@ -964,40 +961,40 @@ export default function PartnersAdminPage() {
))}
-
+
setInviteForm(prev => ({ ...prev, userRole: e.target.value }))}
>
- {memberRoleOptions.map(role => {role} )}
+ {memberRoleOptions.map(role => {formatPartnerRole(role)} )}
-
+
setInviteForm(prev => ({ ...prev, readScope: e.target.value }))}
>
- {scopeOptions.map(scope => {scope} )}
+ {scopeOptions.map(scope => {formatAccessScope(scope)} )}
-
+
setInviteForm(prev => ({ ...prev, writeScope: e.target.value }))}
>
- {scopeOptions.map(scope => {scope} )}
+ {scopeOptions.map(scope => {formatAccessScope(scope)} )}
-
+
setInviteForm(prev => ({ ...prev, deleteScope: e.target.value }))}
>
- {scopeOptions.map(scope => {scope} )}
+ {scopeOptions.map(scope => {formatAccessScope(scope)} )}
diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx
index fd1871c..78d520f 100644
--- a/src/pages/ProfilePage.tsx
+++ b/src/pages/ProfilePage.tsx
@@ -4,6 +4,7 @@ import { api } from '@/api/client';
import { useFeedbackStore } from '@/feedback/feedbackStore';
import { Button, Field, Input } from '@/components/UiKit';
import { validateOptionalPhone } from '@/utils/phone';
+import { formatAccessScope, formatGlobalRole, formatPartnerRole } from '@/utils/accessLabels';
export default function ProfilePage() {
const user = useSessionStore(s => s.user);
@@ -24,7 +25,7 @@ export default function ProfilePage() {
const [profileError, setProfileError] = useState
();
const [passwordSaving, setPasswordSaving] = useState(false);
const [passwordError, setPasswordError] = useState();
- const role = user?.global_role ?? '—';
+ const role = formatGlobalRole(user?.global_role);
const memberships = user?.partner_memberships ?? [];
const hasProfileChanges = useMemo(() => (
profileForm.fullName.trim() !== (user?.full_name ?? '')
@@ -153,15 +154,11 @@ export default function ProfilePage() {
-
-
-
+
-
-
@@ -305,35 +302,3 @@ function Detail({ label, value }: { label: string; value: string | number }) {
);
}
-
-function formatDate(dateStr?: string) {
- if (!dateStr) return '—';
- try {
- return new Date(dateStr).toLocaleString('ru-RU');
- } catch {
- return dateStr;
- }
-}
-
-function formatAccessScope(scope?: string) {
- const labels: Record
= {
- none: 'Нет доступа',
- own: 'Свои',
- assigned: 'Назначенные',
- own_or_assigned: 'Свои и назначенные',
- partner_all: 'Весь партнёр',
- global_all: 'Все партнёры'
- };
- return labels[scope ?? ''] ?? scope ?? '—';
-}
-
-function formatPartnerRole(role?: string) {
- const labels: Record = {
- partner_owner: 'Владелец партнёра',
- partner_admin: 'Администратор партнёра',
- partner_manager: 'Менеджер партнёра',
- partner_analyst: 'Аналитик партнёра',
- partner_viewer: 'Наблюдатель партнёра'
- };
- return labels[role ?? ''] ?? role ?? '—';
-}
diff --git a/src/pages/ResourcePlaceholderPage.tsx b/src/pages/ResourcePlaceholderPage.tsx
index 762a4de..a494b7a 100644
--- a/src/pages/ResourcePlaceholderPage.tsx
+++ b/src/pages/ResourcePlaceholderPage.tsx
@@ -28,7 +28,7 @@ export default function ResourcePlaceholderPage({
{endpoints.map(endpoint => (
{endpoint}
-
Ожидает backend
+
В разработке
))}
diff --git a/src/pages/SourcesPage.tsx b/src/pages/SourcesPage.tsx
index ff95b3b..099ee67 100644
--- a/src/pages/SourcesPage.tsx
+++ b/src/pages/SourcesPage.tsx
@@ -19,6 +19,19 @@ function normalizeStatus(source: DataSource) {
return source.status || 'active';
}
+function formatStatus(source: DataSource) {
+ const status = normalizeStatus(source);
+ const labels: Record