diff --git a/src/App.tsx b/src/App.tsx
index 2698743..1c03c6b 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,82 +1,39 @@
-import { Admin, Resource, ListGuesser, fetchUtils } from 'react-admin';
-import simpleRestProvider from 'ra-data-simple-rest';
-
-// Кастомный httpClient, добавляющий X-Total-Count, если его нет
-const httpClient = (url: string, options: any = {}) => {
- // Добавляем JWT токен
- const token = localStorage.getItem('token');
- if (token) {
- options.headers = new Headers({
- ...options.headers,
- Authorization: `Bearer ${token}`,
- });
- }
-
- return fetchUtils.fetchJson(url, options).then((response) => {
- const { headers, json } = response;
- // Если это GET-запрос и ответ - массив, добавляем X-Total-Count
- if (
- !options.method || options.method === 'GET'
- ) {
- if (Array.isArray(json)) {
- // Создаём новый объект Response с добавленным заголовком
- const newHeaders = new Headers(headers);
- if (!newHeaders.has('X-Total-Count')) {
- newHeaders.set('X-Total-Count', json.length.toString());
- }
- return Promise.resolve({
- status: response.status,
- headers: newHeaders,
- body: json,
- json: json,
- });
- }
- }
- return response;
- });
-};
-
-const dataProvider = simpleRestProvider('/api', httpClient);
-
-const authProvider = {
- login: ({ username, password }: any) => {
- return fetch('/v1/admin/login', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ email: username, password }),
- })
- .then((res) => {
- if (res.ok) return res.json();
- throw new Error(res.statusText);
- })
- .then((data) => {
- localStorage.setItem('token', data.token);
- return Promise.resolve();
- });
- },
- logout: () => {
- localStorage.removeItem('token');
- return Promise.resolve();
- },
- checkAuth: () =>
- localStorage.getItem('token') ? Promise.resolve() : Promise.reject(),
- checkError: (error: any) => {
- if (error.status === 401) {
- localStorage.removeItem('token');
- return Promise.reject();
- }
- return Promise.resolve();
- },
- getPermissions: () => Promise.resolve(),
-};
+import React from 'react';
+import { Admin, Resource } from 'react-admin';
+import { authProvider } from './authProvider';
+import { dataProvider } from './dataProvider';
+import Dashboard from './resources/dashboard/Dashboard';
+import { UserList, UserEdit } from './resources/users';
+import { ReportList, ReportEdit } from './resources/reports';
+import { TicketList, TicketEdit } from './resources/tickets';
+import { AdminList, AdminEdit } from './resources/admins';
+import { BannedWordList, BannedWordEdit } from './resources/banned-words';
+import { SubscriptionList, SubscriptionEdit } from './resources/subscriptions';
+import { ReviewList, ReviewEdit } from './resources/reviews';
+import { AuditList } from './resources/audit';
const App = () => (
-
-
-
-
-
-
+
+ {(permissions) => [
+ // Доступно всем администраторам
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ // Только superadmin
+ permissions === 'superadmin' ? (
+
+ ) : null,
+ ]}
+
);
export default App;
\ No newline at end of file
diff --git a/src/authProvider.ts b/src/authProvider.ts
new file mode 100644
index 0000000..fb71495
--- /dev/null
+++ b/src/authProvider.ts
@@ -0,0 +1,44 @@
+const base = import.meta.env.VITE_ADMIN_API_BASE_URL || '';
+
+export const authProvider = {
+ login: async ({ username, password }: { username: string; password: string }) => {
+ const req = new Request(`${base}/v1/admin/login`, {
+ method: 'POST',
+ body: JSON.stringify({ email: username, password }),
+ headers: new Headers({ 'Content-Type': 'application/json' }),
+ });
+ const res = await fetch(req);
+ if (res.status < 200 || res.status >= 300) {
+ throw new Error(res.statusText);
+ }
+ const auth = await res.json();
+ localStorage.setItem('auth', JSON.stringify(auth));
+ return Promise.resolve();
+ },
+ logout: () => {
+ localStorage.removeItem('auth');
+ return Promise.resolve();
+ },
+ checkAuth: () =>
+ localStorage.getItem('auth')
+ ? Promise.resolve()
+ : Promise.reject(new Error('Not authenticated')),
+ checkError: (error: any) => {
+ const status = error.status;
+ if (status === 401 || status === 403) {
+ localStorage.removeItem('auth');
+ return Promise.reject();
+ }
+ return Promise.resolve();
+ },
+ getPermissions: () => {
+ const auth = JSON.parse(localStorage.getItem('auth') || '{}');
+ // Используем роль из вложенного объекта user, если есть
+ return Promise.resolve(auth.user?.role || auth.role);
+ },
+ getIdentity: () => {
+ const auth = JSON.parse(localStorage.getItem('auth') || '{}');
+ const user = auth.user || {};
+ return Promise.resolve({ id: user.id || '', fullName: user.email || auth.email });
+ },
+};
\ No newline at end of file
diff --git a/src/dataProvider.ts b/src/dataProvider.ts
new file mode 100644
index 0000000..ca87fc0
--- /dev/null
+++ b/src/dataProvider.ts
@@ -0,0 +1,80 @@
+// src/dataProvider.ts
+import { fetchUtils } from 'react-admin';
+
+const base = import.meta.env.VITE_ADMIN_API_BASE_URL || '';
+
+const httpClient = (url: string, options: any = {}) => {
+ if (!options.headers) {
+ options.headers = new Headers({ Accept: 'application/json' });
+ }
+ const auth = JSON.parse(localStorage.getItem('auth') || '{}');
+ if (auth.token) {
+ (options.headers as Headers).set('Authorization', `Bearer ${auth.token}`);
+ }
+ return fetchUtils.fetchJson(url, options);
+};
+
+export const dataProvider = {
+ getList: async (resource, params) => {
+ const url = `${base}/v1/admin/${resource}`;
+ const { json } = await httpClient(url);
+ const data = Array.isArray(json) ? json : json.data || json.items || [];
+ const total = data.length;
+ return { data, total };
+ },
+ getOne: async (resource, params) => {
+ const url = `${base}/v1/admin/${resource}/${params.id}`;
+ const { json } = await httpClient(url);
+ return { data: json };
+ },
+ getMany: async (resource, params) => {
+ const url = `${base}/v1/admin/${resource}`;
+ const { json } = await httpClient(url);
+ const data = (Array.isArray(json) ? json : json.data || []).filter((item: any) =>
+ params.ids.includes(item.id)
+ );
+ return { data };
+ },
+ getManyReference: async (resource, params) => {
+ const url = `${base}/v1/admin/${resource}`;
+ const { json } = await httpClient(url);
+ return { data: (Array.isArray(json) ? json : []), total: (Array.isArray(json) ? json : []).length };
+ },
+ update: async (resource, params) => {
+ const url = `${base}/v1/admin/${resource}/${params.id}`;
+ const { json } = await httpClient(url, {
+ method: 'PUT',
+ body: JSON.stringify(params.data),
+ });
+ return { data: json };
+ },
+ updateMany: async (resource, params) => {
+ const url = `${base}/v1/admin/${resource}`;
+ await httpClient(url, {
+ method: 'PUT',
+ body: JSON.stringify({ ids: params.ids, data: params.data }),
+ });
+ return { data: [] };
+ },
+ create: async (resource, params) => {
+ const url = `${base}/v1/admin/${resource}`;
+ const { json } = await httpClient(url, {
+ method: 'POST',
+ body: JSON.stringify(params.data),
+ });
+ return { data: json };
+ },
+ delete: async (resource, params) => {
+ const url = `${base}/v1/admin/${resource}/${params.id}`;
+ await httpClient(url, { method: 'DELETE' });
+ return { data: { id: params.id } };
+ },
+ deleteMany: async (resource, params) => {
+ const url = `${base}/v1/admin/${resource}`;
+ await httpClient(url, {
+ method: 'DELETE',
+ body: JSON.stringify({ ids: params.ids }),
+ });
+ return { data: [] };
+ },
+};
\ No newline at end of file
diff --git a/src/resources/admins/AdminEdit.tsx b/src/resources/admins/AdminEdit.tsx
new file mode 100644
index 0000000..d7eaa74
--- /dev/null
+++ b/src/resources/admins/AdminEdit.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import { Edit, SimpleForm, SelectInput, required } from 'react-admin';
+
+export const AdminEdit = () => (
+
+
+
+
+
+);
\ No newline at end of file
diff --git a/src/resources/admins/AdminList.tsx b/src/resources/admins/AdminList.tsx
new file mode 100644
index 0000000..c6bfd1d
--- /dev/null
+++ b/src/resources/admins/AdminList.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import { List, Datagrid, TextField, EmailField, DateField, EditButton, SelectInput } from 'react-admin';
+
+export const AdminList = () => (
+
,
+ ]}>
+
+
+
+
+
+
+
+
+
+);
\ No newline at end of file
diff --git a/src/resources/admins/index.ts b/src/resources/admins/index.ts
new file mode 100644
index 0000000..6938233
--- /dev/null
+++ b/src/resources/admins/index.ts
@@ -0,0 +1,2 @@
+export { AdminList } from './AdminList';
+export { AdminEdit } from './AdminEdit';
\ No newline at end of file
diff --git a/src/resources/audit/AuditList.tsx b/src/resources/audit/AuditList.tsx
new file mode 100644
index 0000000..d0b300f
--- /dev/null
+++ b/src/resources/audit/AuditList.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import { List, Datagrid, TextField, DateField, TextInput } from 'react-admin';
+
+export const AuditList = () => (
+
]}>
+
+
+
+
+
+
+
+
+
+);
\ No newline at end of file
diff --git a/src/resources/audit/index.ts b/src/resources/audit/index.ts
new file mode 100644
index 0000000..f094527
--- /dev/null
+++ b/src/resources/audit/index.ts
@@ -0,0 +1 @@
+export { AuditList } from './AuditList';
\ No newline at end of file
diff --git a/src/resources/banned-words/BannedWordEdit.tsx b/src/resources/banned-words/BannedWordEdit.tsx
new file mode 100644
index 0000000..357d377
--- /dev/null
+++ b/src/resources/banned-words/BannedWordEdit.tsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import { Edit, SimpleForm, TextInput } from 'react-admin';
+
+export const BannedWordEdit = () => (
+
+
+
+
+
+);
\ No newline at end of file
diff --git a/src/resources/banned-words/BannedWordList.tsx b/src/resources/banned-words/BannedWordList.tsx
new file mode 100644
index 0000000..c4f0540
--- /dev/null
+++ b/src/resources/banned-words/BannedWordList.tsx
@@ -0,0 +1,13 @@
+import React from 'react';
+import { List, Datagrid, TextField, DateField, EditButton, TextInput } from 'react-admin';
+
+export const BannedWordList = () => (
+
]}>
+
+
+
+
+
+
+
+);
\ No newline at end of file
diff --git a/src/resources/banned-words/index.ts b/src/resources/banned-words/index.ts
new file mode 100644
index 0000000..f199435
--- /dev/null
+++ b/src/resources/banned-words/index.ts
@@ -0,0 +1,2 @@
+export { BannedWordList } from './BannedWordList';
+export { BannedWordEdit } from './BannedWordEdit';
\ No newline at end of file
diff --git a/src/resources/dashboard/Dashboard.tsx b/src/resources/dashboard/Dashboard.tsx
new file mode 100644
index 0000000..5209b66
--- /dev/null
+++ b/src/resources/dashboard/Dashboard.tsx
@@ -0,0 +1,64 @@
+// src/resources/dashboard/Dashboard.tsx
+import React, { useEffect, useState } from 'react';
+import { Card, CardContent, Typography, Box, CircularProgress } from '@mui/material';
+import { useDataProvider } from 'react-admin';
+
+const Dashboard = () => {
+ const [stats, setStats] = useState | null>(null);
+ const [ticketsStats, setTicketsStats] = useState<{ total: number } | null>(null);
+ const dataProvider = useDataProvider();
+
+ const fetchJson = async (url: string) => {
+ const auth = JSON.parse(localStorage.getItem('auth') || '{}');
+ const headers: HeadersInit = { 'Content-Type': 'application/json' };
+ if (auth.token) {
+ headers['Authorization'] = `Bearer ${auth.token}`;
+ }
+ const res = await fetch(url, { headers });
+ if (!res.ok) throw new Error(res.statusText);
+ return res.json();
+ };
+
+ useEffect(() => {
+ Promise.all([
+ fetchJson('/v1/admin/stats'),
+ fetchJson('/v1/admin/tickets/stats'),
+ ])
+ .then(([statsData, ticketsData]) => {
+ setStats(statsData);
+ setTicketsStats(ticketsData);
+ })
+ .catch(console.error);
+ }, []);
+
+ if (!stats || !ticketsStats) return ;
+
+ const cards = [
+ { label: 'Users', value: stats.users },
+ { label: 'Calendars', value: stats.calendars },
+ { label: 'Events', value: stats.events },
+ { label: 'Bookings', value: stats.bookings },
+ { label: 'Reviews', value: stats.reviews },
+ { label: 'Subscriptions', value: stats.subscriptions },
+ { label: 'Tickets', value: ticketsStats.total },
+ ];
+
+ return (
+
+ {cards.map(({ label, value }) => (
+
+
+
+
+ {label}
+
+ {value}
+
+
+
+ ))}
+
+ );
+};
+
+export default Dashboard;
\ No newline at end of file
diff --git a/src/resources/events/EventEdit.tsx b/src/resources/events/EventEdit.tsx
new file mode 100644
index 0000000..0a50c1a
--- /dev/null
+++ b/src/resources/events/EventEdit.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import { Edit, SimpleForm, SelectInput, required } from 'react-admin';
+
+export const EventEdit = () => (
+
+
+
+
+
+);
\ No newline at end of file
diff --git a/src/resources/events/EventList.tsx b/src/resources/events/EventList.tsx
new file mode 100644
index 0000000..39ffb98
--- /dev/null
+++ b/src/resources/events/EventList.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import { List, Datagrid, TextField, DateField, EditButton, TextInput, SelectInput } from 'react-admin';
+
+export const EventList = () => (
+
,
+ ,
+ ]}>
+
+
+
+
+
+
+
+
+);
\ No newline at end of file
diff --git a/src/resources/events/index.ts b/src/resources/events/index.ts
new file mode 100644
index 0000000..45db43b
--- /dev/null
+++ b/src/resources/events/index.ts
@@ -0,0 +1,2 @@
+export { EventList } from './EventList';
+export { EventEdit } from './EventEdit';
\ No newline at end of file
diff --git a/src/resources/reports/ReportEdit.tsx b/src/resources/reports/ReportEdit.tsx
new file mode 100644
index 0000000..f1b3082
--- /dev/null
+++ b/src/resources/reports/ReportEdit.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import { Edit, SimpleForm, SelectInput, required } from 'react-admin';
+
+export const ReportEdit = () => (
+
+
+
+
+
+);
\ No newline at end of file
diff --git a/src/resources/reports/ReportList.tsx b/src/resources/reports/ReportList.tsx
new file mode 100644
index 0000000..a844e21
--- /dev/null
+++ b/src/resources/reports/ReportList.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import { List, Datagrid, TextField, DateField, EditButton, SelectInput } from 'react-admin';
+
+export const ReportList = () => (
+
,
+ ]}>
+
+
+
+
+
+
+
+
+
+
+);
\ No newline at end of file
diff --git a/src/resources/reports/index.ts b/src/resources/reports/index.ts
new file mode 100644
index 0000000..1591533
--- /dev/null
+++ b/src/resources/reports/index.ts
@@ -0,0 +1,2 @@
+export { ReportList } from './ReportList';
+export { ReportEdit } from './ReportEdit';
\ No newline at end of file
diff --git a/src/resources/reviews/ReviewEdit.tsx b/src/resources/reviews/ReviewEdit.tsx
new file mode 100644
index 0000000..b7e286a
--- /dev/null
+++ b/src/resources/reviews/ReviewEdit.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import { Edit, SimpleForm, SelectInput } from 'react-admin';
+
+export const ReviewEdit = () => (
+
+
+
+
+
+);
\ No newline at end of file
diff --git a/src/resources/reviews/ReviewList.tsx b/src/resources/reviews/ReviewList.tsx
new file mode 100644
index 0000000..ea031d5
--- /dev/null
+++ b/src/resources/reviews/ReviewList.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import { List, Datagrid, TextField, DateField, EditButton, TextInput } from 'react-admin';
+
+export const ReviewList = () => (
+
]}>
+
+
+
+
+
+
+
+
+
+
+
+);
\ No newline at end of file
diff --git a/src/resources/reviews/index.ts b/src/resources/reviews/index.ts
new file mode 100644
index 0000000..608e925
--- /dev/null
+++ b/src/resources/reviews/index.ts
@@ -0,0 +1,2 @@
+export { ReviewList } from './ReviewList';
+export { ReviewEdit } from './ReviewEdit';
\ No newline at end of file
diff --git a/src/resources/subscriptions/SubscriptionEdit.tsx b/src/resources/subscriptions/SubscriptionEdit.tsx
new file mode 100644
index 0000000..59c0548
--- /dev/null
+++ b/src/resources/subscriptions/SubscriptionEdit.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import { Edit, SimpleForm, TextInput, SelectInput } from 'react-admin';
+
+export const SubscriptionEdit = () => (
+
+
+
+
+
+
+);
\ No newline at end of file
diff --git a/src/resources/subscriptions/SubscriptionList.tsx b/src/resources/subscriptions/SubscriptionList.tsx
new file mode 100644
index 0000000..ce94fd5
--- /dev/null
+++ b/src/resources/subscriptions/SubscriptionList.tsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import { List, Datagrid, TextField, DateField, EditButton, SelectInput } from 'react-admin';
+
+export const SubscriptionList = () => (
+
]}>
+
+
+
+
+
+
+
+
+
+);
\ No newline at end of file
diff --git a/src/resources/subscriptions/index.ts b/src/resources/subscriptions/index.ts
new file mode 100644
index 0000000..2f82ae9
--- /dev/null
+++ b/src/resources/subscriptions/index.ts
@@ -0,0 +1,2 @@
+export { SubscriptionList } from './SubscriptionList';
+export { SubscriptionEdit } from './SubscriptionEdit';
\ No newline at end of file
diff --git a/src/resources/tickets/TicketEdit.tsx b/src/resources/tickets/TicketEdit.tsx
new file mode 100644
index 0000000..6bc56db
--- /dev/null
+++ b/src/resources/tickets/TicketEdit.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import { Edit, SimpleForm, TextInput, SelectInput, required } from 'react-admin';
+
+export const TicketEdit = () => (
+
+
+
+
+
+
+
+
+);
\ No newline at end of file
diff --git a/src/resources/tickets/TicketList.tsx b/src/resources/tickets/TicketList.tsx
new file mode 100644
index 0000000..43a7994
--- /dev/null
+++ b/src/resources/tickets/TicketList.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import { List, Datagrid, TextField, NumberField, DateField, EditButton, SelectInput } from 'react-admin';
+
+export const TicketList = () => (
+
,
+ ]}>
+
+
+
+
+
+
+
+
+
+);
\ No newline at end of file
diff --git a/src/resources/tickets/index.ts b/src/resources/tickets/index.ts
new file mode 100644
index 0000000..2cc5f07
--- /dev/null
+++ b/src/resources/tickets/index.ts
@@ -0,0 +1,2 @@
+export { TicketList } from './TicketList';
+export { TicketEdit } from './TicketEdit';
\ No newline at end of file
diff --git a/src/resources/users/UserEdit.tsx b/src/resources/users/UserEdit.tsx
new file mode 100644
index 0000000..429343d
--- /dev/null
+++ b/src/resources/users/UserEdit.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import { Edit, SimpleForm, TextInput, SelectInput, required } from 'react-admin';
+
+export const UserEdit = () => (
+
+
+
+
+
+
+);
\ No newline at end of file
diff --git a/src/resources/users/UserList.tsx b/src/resources/users/UserList.tsx
new file mode 100644
index 0000000..bbaa259
--- /dev/null
+++ b/src/resources/users/UserList.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import { List, Datagrid, TextField, EmailField, DateField, EditButton, TextInput, SelectInput } from 'react-admin';
+
+const userFilters = [
+ ,
+ ,
+];
+
+export const UserList = () => (
+
+
+
+
+
+
+
+
+
+
+);
\ No newline at end of file
diff --git a/src/resources/users/index.ts b/src/resources/users/index.ts
new file mode 100644
index 0000000..44b6de8
--- /dev/null
+++ b/src/resources/users/index.ts
@@ -0,0 +1,2 @@
+export { UserList } from './UserList';
+export { UserEdit } from './UserEdit';
\ No newline at end of file