Doc fetch

This commit is contained in:
Mikan
2025-06-02 19:39:54 +03:00
parent 054cb63d71
commit 546192b96d
11 changed files with 244 additions and 27 deletions

View File

@@ -1,8 +1,8 @@
import {PageControl} from "../types/common.ts"; import {FieldInfo, PageControl} from "../types/common.ts";
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {ApiResponse, PageResponse} from "../types/api.ts"; import {ApiResponse, PageResponse} from "../types/api.ts";
import {getFetch} from "./common.ts"; import {getFetch} from "./common.ts";
import {createQueryString} from "../utils/common.ts"; import {createQueryString, hostUrl} from "../utils/common.ts";
export function useDataPage<T>(application:string, endpoint: string, params:{[key:string]:string|number}={}, needLoad:boolean=true):PageControl<T> { export function useDataPage<T>(application:string, endpoint: string, params:{[key:string]:string|number}={}, needLoad:boolean=true):PageControl<T> {
const [data, setData] = useState<T[]|null>(null); const [data, setData] = useState<T[]|null>(null);
@@ -137,3 +137,91 @@ export function useCachedData<T>(application: string, endpoint: string): CachedD
} }
} }
/**
* Хук для получения документации swagger JSON и извлечения структур.
* @param applicationName Название модуля/приложения.
* @param endpointName Название эндпоинта.
* @returns Массив структур без ID с полями: название, тип, title, format.
*/
export function useApiDocumentation(
applicationName: string,
endpointName: string
): { schema: FieldInfo[] | null; loading: boolean; error: Error | null } {
const [schema, setSchema] = useState<FieldInfo[] | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
async function fetchDocumentation() {
try {
setLoading(true);
const response = await fetch(hostUrl('/docs/swaggerjson/'));
if (!response.ok) {
throw new Error(`Failed to fetch documentation: ${response.statusText}`);
}
const data = await response.json();
// Найти путь по applicationName и endpointName
const pathKey = `/${applicationName}/${endpointName}/`;
const pathItem = data.paths?.[pathKey];
if (!pathItem) {
throw new Error(`Path ${pathKey} not found`);
}
const getMethod = pathItem.get;
if (!getMethod || !getMethod.responses?.['200']) {
throw new Error(`GET method or response 200 not found for ${pathKey}`);
}
const schemaRef = getMethod.responses["200"].schema?.properties?.results?.items?.['$ref'];
if (!schemaRef) {
throw new Error(`$ref not found in response schema for ${pathKey}`);
}
console.log(schemaRef.match(/#\/definitions\/(\w+)/))
// Получить название определения
const defNameMatch = schemaRef.match(/#\/definitions\/(\w+)/);
if (!defNameMatch || defNameMatch.length < 2) {
throw new Error(`Invalid $ref format: ${schemaRef}`);
}
const defName = defNameMatch[1];
const definitions = data.definitions;
if (!definitions || !definitions[defName]) {
throw new Error(`Definition ${defName} not found`);
}
const defProps = definitions[defName].properties;
if (!defProps) {
throw new Error(`Properties for ${defName} not found`);
}
// Собрать поля, исключая id
const result: FieldInfo[] = Object.entries(defProps)
.filter((prop) => prop[0] !== 'id')
.map(([name, prop]) => {
return {
name,
type: prop.type || '',
title: prop.title || '',
format: prop.format || undefined,
};
});
setSchema(result);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
}
fetchDocumentation().then();
}, [applicationName, endpointName]);
return { schema, loading, error };
}

View File

@@ -1,6 +1,7 @@
import { TokenObtainPair, TokenRefresh, TokenVerify, JwtTokenResponse } from '../types/users'; import { TokenObtainPair, TokenRefresh, TokenVerify, JwtTokenResponse } from '../types/users';
import {apiUrl} from "../utils/common.ts"; import {apiUrl} from "../utils/common.ts";
import {postFetch} from "./common.ts";
export async function login(data: TokenObtainPair): Promise<JwtTokenResponse | null> { export async function login(data: TokenObtainPair): Promise<JwtTokenResponse | null> {
@@ -44,3 +45,10 @@ export async function verifyToken(data: TokenVerify): Promise<boolean> {
return response.ok; return response.ok;
} }
export async function changePassword(data: { currentPassword: string; newPassword: string }) {
const response = await postFetch("/users/auth/users/set_password/", {new_password: data.newPassword, re_new_password: data.newPassword, current_password: data.currentPassword});
if (!response.success) {
throw new Error('Не удалось изменить пароль');
}
}

View File

@@ -1,15 +1,16 @@
// App.tsx // App.tsx
import {BrowserRouter as Router, Routes, Route, Navigate} from 'react-router-dom'; import {BrowserRouter as Router, Routes, Route, Navigate} from 'react-router-dom';
import {MainPage} from "./pages/MainPage.tsx"; import {MainPage} from "./pages/MainPage.tsx";
import {SurveyPage} from "./pages/SurveyPage.tsx"; import {SurveyPage} from "./pages/surveys/SurveyPage.tsx";
import {DashboardPage} from "./pages/DashboardPage.tsx"; import {DashboardPage} from "./pages/dashboards/DashboardPage.tsx";
import { SurveyResultPage } from './pages/SurveyResultPage.tsx'; import { SurveyResultPage } from './pages/surveys/SurveyResultPage.tsx';
import { ProfessionPage } from './pages/ProfessionPage.tsx'; import { ProfessionPage } from './pages/surveys/ProfessionPage.tsx';
import React from "react"; import React from "react";
import {LoginPage} from "./pages/LoginPage.tsx"; import {LoginPage} from "./pages/LoginPage.tsx";
import {UserProvider} from "./utils/users/UserProvider.tsx"; import {UserProvider} from "./utils/users/UserProvider.tsx";
import {DashboardLayout} from "./layouts/DashboardLayout.tsx"; import {DashboardLayout} from "./layouts/DashboardLayout.tsx";
import {MainLayout} from "./layouts/MainLayout.tsx"; import {MainLayout} from "./layouts/MainLayout.tsx";
import {DataPage} from "./pages/dashboards/DataPage.tsx";
export default function App() { export default function App() {
@@ -23,6 +24,7 @@ export default function App() {
<Route path="/dashboard/*" element={<DashboardLayout />}> <Route path="/dashboard/*" element={<DashboardLayout />}>
<Route index element={<DashboardPage />} /> <Route index element={<DashboardPage />} />
<Route path=":applicationName/:endpointName/" element={<DataPage />} />
</Route> </Route>

View File

@@ -28,12 +28,11 @@ export function DashboardLayout() {
<h1 className="logo">Pro-Fi</h1> <h1 className="logo">Pro-Fi</h1>
<p className="role">{user.roleTitle || "Роль не опознана"}</p> {/* Отображаем текущую роль пользователя */} <p className="role">{user.roleTitle || "Роль не опознана"}</p> {/* Отображаем текущую роль пользователя */}
<nav className="navigation"> <nav className="navigation">
<Link to="/dashboard/staff" className={isActive('/dashboard/staff')}>Штат</Link> <Link to="/dashboard/employees/employees/" className={isActive('/dashboard/staff')}>Штат</Link>
<Link to="/dashboard/schools" className={isActive('/dashboard/schools')}>Школы</Link> <Link to="/dashboard/outreach/schools/" className={isActive('/dashboard/schools')}>Школы</Link>
<Link to="/dashboard/events" className={isActive('/dashboard/events')}>Мероприятия</Link> <Link to="/dashboard/events/events/" className={isActive('/dashboard/events')}>Мероприятия</Link>
<Link to="/dashboard/students" className={isActive('/dashboard/students')}>Студенты</Link> <Link to="/dashboard/education/students/" className={isActive('/dashboard/students')}>Студенты</Link>
<Link to="/dashboard/reports" className={isActive('/dashboard/reports')}>Отчеты</Link> <Link to="/dashboard/outreach/partners/" className={isActive('/dashboard/partners')}>Партнеры</Link>
<Link to="/dashboard/partners" className={isActive('/dashboard/partners')}>Партнеры</Link>
<Link to="/admin" className={isActive('/admin')}>Админ-панель</Link> <Link to="/admin" className={isActive('/admin')}>Админ-панель</Link>
<button onClick={logout} className="logout">Выйти</button> <button onClick={logout} className="logout">Выйти</button>
</nav> </nav>

View File

@@ -1,3 +0,0 @@
export function DashboardPage() {
return null;
}

View File

@@ -0,0 +1,70 @@
// pages/DashboardPage.tsx
import React, { useState } from 'react';
import {useUser} from "../../utils/users/UseUser.ts";
import {changePassword} from "../../API/users.ts";
export function DashboardPage() {
// Получаем данные о пользователе
const userContext = useUser();
const user = userContext?.user;
// Состояние для управления модальным окном
const [isModalOpen, setModalOpen] = useState(false);
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
// Обработчик смены пароля
const handleChangePassword = async () => {
try {
await changePassword({ currentPassword, newPassword });
alert('Пароль успешно изменен!');
setModalOpen(false); // Закрыть модальное окно после успешной смены пароля
} catch (error) {
alert('Ошибка при смене пароля: ' + error.message);
}
};
return (
<div className="dashboard">
<div className="welcome-message">
С возвращением, {user?.last_name} {user?.first_name} {user?.middle_name}!
</div>
<div className="user-role">
Вы вошли как: {user?.roleTitle}
</div>
{
!isModalOpen &&
<button className="change-password-button" onClick={() => setModalOpen(true)}>
Поменять пароль
</button>
}
{isModalOpen && (
<div className="change-password">
<div className="modal-content">
<label>
Текущий пароль:
<input
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
/>
</label>
<label>
Новый пароль:
<input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
/>
</label>
<button onClick={handleChangePassword}>Сменить пароль</button>
<button onClick={() => setModalOpen(false)}>Закрыть</button>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,47 @@
import { useParams } from 'react-router-dom';
import {useApiDocumentation} from "../../API/hooks.ts";
export function DataPage() {
const { applicationName, endpointName } = useParams<{ applicationName: string; endpointName: string }>();
const {schema} = useApiDocumentation(applicationName || "", endpointName || "");
console.log(schema)
const handleExport = () => {
// Пока пусто
};
const handleAdd = () => {
// Пока пусто
};
const handleFilters = () => {
// Пока пусто
};
return (
<div className="data-page">
<div className="buttons-container">
<button className="export-excel-button" onClick={handleExport}>Экспорт в Excel</button>
<button className="add-button" onClick={handleAdd}>Добавление</button>
<button className="filters-button" onClick={handleFilters}>Фильтры</button>
</div>
<div className="table-container">
<table className="data-table" style={{ overflowX: 'auto', display: 'block' }}>
<thead>
<tr>
<th className="column-header">Header 1</th>
<th className="column-header">Header 2</th>
<th className="column-header">Header 3</th>
<th className="column-header">Header 4</th>
</tr>
</thead>
<tbody>
{/* Тут могут быть строки данных */}
</tbody>
</table>
</div>
</div>
);
}

View File

@@ -1,9 +1,9 @@
import {Link, useParams} from 'react-router-dom'; import {Link, useParams} from 'react-router-dom';
import { Institution, Profession, Specialty } from '../types/survey'; import { Institution, Profession, Specialty } from '../../types/survey.ts';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import { LoadingData } from '../components/LoadingData'; import { LoadingData } from '../../components/LoadingData.tsx';
import { LoadingList } from '../components/LoadingList'; import { LoadingList } from '../../components/LoadingList.tsx';
import {useCachedData, useData, useDataPage} from "../API/hooks.ts"; import {useCachedData, useData, useDataPage} from "../../API/hooks.ts";

View File

@@ -7,10 +7,10 @@ import {
AgreementQuestion, AgreementQuestion,
QuestionType, QuestionType,
} from "../types/survey"; } from "../../types/survey.ts";
import { LoadingData } from "../components/LoadingData"; import { LoadingData } from "../../components/LoadingData.tsx";
import { LoadingList } from "../components/LoadingList"; import { LoadingList } from "../../components/LoadingList.tsx";
import {useData, useDataPage} from "../API/hooks.ts"; import {useData, useDataPage} from "../../API/hooks.ts";
export function SurveyPage() { export function SurveyPage() {
const { surveyId } = useParams<{ surveyId: string }>(); const { surveyId } = useParams<{ surveyId: string }>();

View File

@@ -1,9 +1,9 @@
import { Link, useParams } from 'react-router-dom'; import { Link, useParams } from 'react-router-dom';
import { Profession, ScoreVariable, Survey } from '../types/survey'; import { Profession, ScoreVariable, Survey } from '../../types/survey.ts';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import { LoadingData } from '../components/LoadingData'; import { LoadingData } from '../../components/LoadingData.tsx';
import { LoadingList } from '../components/LoadingList'; import { LoadingList } from '../../components/LoadingList.tsx';
import {useData, useDataPage} from "../API/hooks.ts"; import {useData, useDataPage} from "../../API/hooks.ts";
export function SurveyResultPage() { export function SurveyResultPage() {
const { scoreVariableId } = useParams<{ scoreVariableId: string }>(); const { scoreVariableId } = useParams<{ scoreVariableId: string }>();

View File

@@ -11,3 +11,9 @@ export interface PageControl<T>{
previousPage(): void; previousPage(): void;
} }
export interface FieldInfo {
name: string;
type: string;
title: string;
format?: string;
}