From eb9bbed092153ef713b38a4732cccaac9ae0b9f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B9=20=D0=A1=D0=B0?= =?UTF-8?q?=D0=B1=D0=B8=D0=BB=D0=B8=D0=BD?= Date: Tue, 5 May 2026 20:45:13 +0300 Subject: [PATCH] prototype 1.0 --- src/App.tsx | 111 ++++++------------ src/authProvider.ts | 44 +++++++ src/dataProvider.ts | 80 +++++++++++++ src/resources/admins/AdminEdit.tsx | 14 +++ src/resources/admins/AdminList.tsx | 22 ++++ src/resources/admins/index.ts | 2 + src/resources/audit/AuditList.tsx | 15 +++ src/resources/audit/index.ts | 1 + src/resources/banned-words/BannedWordEdit.tsx | 10 ++ src/resources/banned-words/BannedWordList.tsx | 13 ++ src/resources/banned-words/index.ts | 2 + src/resources/dashboard/Dashboard.tsx | 64 ++++++++++ src/resources/events/EventEdit.tsx | 14 +++ src/resources/events/EventList.tsx | 21 ++++ src/resources/events/index.ts | 2 + src/resources/reports/ReportEdit.tsx | 14 +++ src/resources/reports/ReportList.tsx | 22 ++++ src/resources/reports/index.ts | 2 + src/resources/reviews/ReviewEdit.tsx | 14 +++ src/resources/reviews/ReviewList.tsx | 17 +++ src/resources/reviews/index.ts | 2 + .../subscriptions/SubscriptionEdit.tsx | 15 +++ .../subscriptions/SubscriptionList.tsx | 19 +++ src/resources/subscriptions/index.ts | 2 + src/resources/tickets/TicketEdit.tsx | 18 +++ src/resources/tickets/TicketList.tsx | 22 ++++ src/resources/tickets/index.ts | 2 + src/resources/users/UserEdit.tsx | 15 +++ src/resources/users/UserList.tsx | 24 ++++ src/resources/users/index.ts | 2 + 30 files changed, 528 insertions(+), 77 deletions(-) create mode 100644 src/authProvider.ts create mode 100644 src/dataProvider.ts create mode 100644 src/resources/admins/AdminEdit.tsx create mode 100644 src/resources/admins/AdminList.tsx create mode 100644 src/resources/admins/index.ts create mode 100644 src/resources/audit/AuditList.tsx create mode 100644 src/resources/audit/index.ts create mode 100644 src/resources/banned-words/BannedWordEdit.tsx create mode 100644 src/resources/banned-words/BannedWordList.tsx create mode 100644 src/resources/banned-words/index.ts create mode 100644 src/resources/dashboard/Dashboard.tsx create mode 100644 src/resources/events/EventEdit.tsx create mode 100644 src/resources/events/EventList.tsx create mode 100644 src/resources/events/index.ts create mode 100644 src/resources/reports/ReportEdit.tsx create mode 100644 src/resources/reports/ReportList.tsx create mode 100644 src/resources/reports/index.ts create mode 100644 src/resources/reviews/ReviewEdit.tsx create mode 100644 src/resources/reviews/ReviewList.tsx create mode 100644 src/resources/reviews/index.ts create mode 100644 src/resources/subscriptions/SubscriptionEdit.tsx create mode 100644 src/resources/subscriptions/SubscriptionList.tsx create mode 100644 src/resources/subscriptions/index.ts create mode 100644 src/resources/tickets/TicketEdit.tsx create mode 100644 src/resources/tickets/TicketList.tsx create mode 100644 src/resources/tickets/index.ts create mode 100644 src/resources/users/UserEdit.tsx create mode 100644 src/resources/users/UserList.tsx create mode 100644 src/resources/users/index.ts 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