Reports test

This commit is contained in:
cracklesparkle
2024-07-04 15:18:06 +09:00
parent 2c71e4f6af
commit 261196afef
17 changed files with 3390 additions and 172 deletions

View File

@ -12,6 +12,7 @@ import { initAuth, useAuthStore } from "./store/auth"
import { useEffect, useState } from "react"
import { Box, CircularProgress, Container } from "@mui/material"
import Documents from "./pages/Documents"
import Reports from "./pages/Reports"
function App() {
const auth = useAuthStore()
@ -50,6 +51,7 @@ function App() {
<Route path="/user" element={<Users />} />
<Route path="/role" element={<Roles />} />
<Route path="/documents" element={<Documents />} />
<Route path="/reports" element={<Reports />} />
<Route path="/api-test" element={<ApiTest />} />
<Route path="*" element={<NotFound />} />
</Route>

View File

@ -2,7 +2,8 @@ import { DataGrid } from '@mui/x-data-grid';
interface Props {
rows: any,
columns: any
columns: any,
checkboxSelection?: boolean
}
export default function DataTable(props: Props) {
@ -18,7 +19,7 @@ export default function DataTable(props: Props) {
},
}}
pageSizeOptions={[10, 20, 50, 100]}
checkboxSelection
checkboxSelection={props.checkboxSelection}
disableRowSelectionOnClick
processRowUpdate={(updatedRow, originalRow) => {

View File

@ -1,72 +1,67 @@
import { useDocuments, useFolders } from '../hooks/swrHooks'
import { IDocument, IDocumentFolder } from '../interfaces/documents'
import { Box, Breadcrumbs, Button, Card, CardActionArea, CircularProgress, Input, InputLabel, LinearProgress, Link } from '@mui/material'
import { Folder, InsertDriveFile, Upload, UploadFile } from '@mui/icons-material'
import { useRef, useState } from 'react'
import { Box, Breadcrumbs, Button, Card, CardActionArea, CircularProgress, Divider, Input, InputLabel, LinearProgress, Link, List, ListItem, ListItemButton } from '@mui/material'
import { Download, Folder, InsertDriveFile, Upload, UploadFile } from '@mui/icons-material'
import { useEffect, useRef, useState } from 'react'
import DocumentService from '../services/DocumentService'
import { mutate } from 'swr'
import FileViewer from './modals/FileViewer'
import { fileTypeFromBlob } from 'file-type/core'
interface FolderProps {
folder: IDocumentFolder;
handleFolderClick: (folder: IDocumentFolder) => void;
}
interface DocumentProps {
doc: IDocument;
index: number;
handleDocumentClick: (doc: IDocument, index: number) => void;
}
function ItemFolder({ folder, handleFolderClick, ...props }: FolderProps) {
return (
<Card
key={folder.id}
<ListItemButton
onClick={() => handleFolderClick(folder)}
>
<CardActionArea>
<Box
sx={{
cursor: 'pointer',
display: 'flex',
flexDirection: 'row',
gap: '8px',
alignItems: 'center',
padding: '8px'
}}
onClick={() => handleFolderClick(folder)}
{...props}
>
<Folder />
{folder.name}
</Box>
</CardActionArea>
</Card>
<Box
sx={{
cursor: 'pointer',
display: 'flex',
flexDirection: 'row',
gap: '8px',
alignItems: 'center',
padding: '8px'
}}
{...props}
>
<Folder />
{folder.name}
</Box>
</ListItemButton>
)
}
interface DocumentProps {
doc: IDocument;
handleDocumentClick: (doc: IDocument) => void;
}
function ItemDocument({ doc, handleDocumentClick, ...props }: DocumentProps) {
function ItemDocument({ doc, index, handleDocumentClick, ...props }: DocumentProps) {
return (
<Card
key={doc.id}
<ListItemButton
onClick={() => handleDocumentClick(doc, index)}
>
<CardActionArea>
<Box
sx={{
cursor: 'pointer',
display: 'flex',
flexDirection: 'row',
gap: '8px',
alignItems: 'center',
padding: '8px',
}}
onClick={() => {
handleDocumentClick(doc)
}}
{...props}
>
<InsertDriveFile />
{doc.name}
</Box>
</CardActionArea>
</Card>
<Box
sx={{
cursor: 'pointer',
display: 'flex',
flexDirection: 'row',
gap: '8px',
alignItems: 'center',
padding: '8px',
}}
{...props}
>
<InsertDriveFile />
{doc.name}
</Box>
</ListItemButton>
)
}
@ -85,24 +80,18 @@ export default function FolderViewer() {
const fileInputRef = useRef<HTMLInputElement>(null)
const [fileViewerModal, setFileViewerModal] = useState(false)
const [currentFileNo, setCurrentFileNo] = useState<number>(-1)
const handleFolderClick = (folder: IDocumentFolder) => {
setCurrentFolder(folder)
setBreadcrumbs((prev) => [...prev, folder])
}
const handleDocumentClick = async (doc: IDocument) => {
try {
const response = await DocumentService.downloadDoc(doc.document_folder_id, doc.id);
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', doc.name);
document.body.appendChild(link);
link.click();
link.remove();
} catch (error) {
console.error(error);
}
const handleDocumentClick = async (doc: IDocument, index: number) => {
setCurrentFileNo(index)
setFileViewerModal(true)
}
const handleBreadcrumbClick = (index: number) => {
@ -136,7 +125,6 @@ export default function FolderViewer() {
}
}
if (foldersLoading || documentsLoading) {
return (
<CircularProgress />
@ -149,6 +137,14 @@ export default function FolderViewer() {
flexDirection: 'column',
gap: '24px'
}}>
<FileViewer
open={fileViewerModal}
setOpen={setFileViewerModal}
currentFileNo={currentFileNo}
setCurrentFileNo={setCurrentFileNo}
docs={documents}
/>
<Breadcrumbs>
<Link
underline='hover'
@ -161,6 +157,7 @@ export default function FolderViewer() {
>
Главная
</Link>
{breadcrumbs.map((breadcrumb, index) => (
<Link
key={breadcrumb.id}
@ -197,31 +194,33 @@ export default function FolderViewer() {
}
</Box>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
flexWrap: 'wrap',
gap: '16px'
}}>
<List
dense
>
{currentFolder ? (
documents?.map((doc: IDocument) => (
<ItemDocument
key={`doc-${doc.id}`}
doc={doc}
handleDocumentClick={handleDocumentClick}
/>
documents?.map((doc: IDocument, index: number) => (
<div key={`${doc.id}-${doc.name}`}>
<ItemDocument
doc={doc}
index={index}
handleDocumentClick={handleDocumentClick}
/>
{index < documents.length - 1 && <Divider />}
</div>
))
) : (
folders?.map((folder: IDocumentFolder) => (
<ItemFolder
key={`folder-${folder.id}`}
folder={folder}
handleFolderClick={handleFolderClick}
/>
folders?.map((folder: IDocumentFolder, index: number) => (
<div key={`${folder.id}-${folder.name}`}>
<ItemFolder
folder={folder}
handleFolderClick={handleFolderClick}
/>
{index < folders.length - 1 && <Divider />}
</div>
))
)}
</Box>
</List>
</Box>
)
}

View File

@ -0,0 +1,121 @@
import React, { useEffect, useState } from 'react'
import { AppBar, Box, Button, CircularProgress, Dialog, IconButton, Toolbar, Typography } from '@mui/material';
import { ChevronLeft, ChevronRight, Close } from '@mui/icons-material';
import { useDownload } from '../../hooks/swrHooks';
import { fileTypeFromBlob } from 'file-type/core';
interface Props {
open: boolean;
setOpen: (state: boolean) => void;
docs: any;
currentFileNo: number;
setCurrentFileNo: (state: number) => void;
}
export default function FileViewer({
open,
setOpen,
docs,
currentFileNo,
setCurrentFileNo
}: Props) {
const { file, isError, isLoading } = useDownload(currentFileNo >= 0 ? docs[currentFileNo]?.document_folder_id : null, currentFileNo >= 0 ? docs[currentFileNo]?.id : null)
const [fileType, setFileType] = useState<any>("")
const getFileType = async (file: any) => {
try {
await fileTypeFromBlob(file).then(response => {
setFileType(response)
})
} catch (error) {
console.error(error)
}
}
useEffect(() => {
if (!isLoading && file) {
getFileType(file)
}
}, [isLoading])
const handleSave = async () => {
const url = window.URL.createObjectURL(file)
const link = document.createElement('a')
link.href = url
link.setAttribute('download', docs[currentFileNo].name)
document.body.appendChild(link)
link.click()
link.remove()
}
return (
<Dialog
fullScreen
open={open}
onClose={() => {
setOpen(false)
setCurrentFileNo(-1)
}}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<AppBar sx={{ position: 'relative' }}>
<Toolbar>
<IconButton
edge="start"
color="inherit"
onClick={() => {
setOpen(false)
setCurrentFileNo(-1)
}}
aria-label="close"
>
<Close />
</IconButton>
<Typography sx={{ ml: 2, flex: 1 }} variant="h6" component="div">
{currentFileNo != -1 && docs[currentFileNo].name}
</Typography>
<div>
<IconButton
color='inherit'
onClick={() => {
if (currentFileNo >= 0 && currentFileNo > 0) {
setCurrentFileNo(currentFileNo - 1)
}
}}
disabled={currentFileNo >= 0 && currentFileNo === 0}
>
<ChevronLeft />
</IconButton>
<IconButton
color='inherit'
onClick={() => {
if (currentFileNo >= 0 && currentFileNo < docs.length) {
setCurrentFileNo(currentFileNo + 1)
}
}}
disabled={currentFileNo >= 0 && currentFileNo >= docs.length - 1}
>
<ChevronRight />
</IconButton>
</div>
<Button
autoFocus
color="inherit"
onClick={handleSave}
>
Сохранить
</Button>
</Toolbar>
</AppBar>
<Box>
{file && <img src={window.URL.createObjectURL(file)} />}
</Box>
</Dialog>
)
}

View File

@ -1,7 +1,7 @@
import useSWR from "swr";
import RoleService from "../services/RoleService";
import UserService from "../services/UserService";
import { fetcher } from "../http/axiosInstance";
import { blobFetcher, fetcher } from "../http/axiosInstance";
export function useRoles() {
const { data, error, isLoading } = useSWR(`/auth/roles`, RoleService.getRoles)
@ -63,4 +63,43 @@ export function useDocuments(folder_id?: number) {
isLoading,
isError: error
}
}
}
export function useDownload(folder_id?: number, id?: number) {
const { data, error, isLoading } = useSWR(
folder_id && id ? `/info/document/${folder_id}&${id}` : null,
blobFetcher,
{
revalidateOnFocus: false,
revalidateOnMount: false
}
)
return {
file: data,
isLoading,
isError: error
}
}
export function useReport(city_id: number) {
const { data, error, isLoading } = useSWR(
city_id ? `/info/reports/${city_id}?to_export=false` : null,
fetcher,
{
revalidateOnFocus: false
}
)
return {
report: JSON.parse(data),
isLoading,
isError: error
}
}
// export function useFileType(file?: Blob){
// const { data, error, isLoading } = useSWR(
// file ? `${file.}`
// )
// }

View File

@ -1,10 +1,27 @@
import axios from 'axios';
import { useAuthStore } from '../store/auth';
const axiosInstance = axios.create({
export const axiosInstanceAuth = axios.create({
baseURL: `${import.meta.env.VITE_API_AUTH_URL}`,
});
const axiosInstance = axios.create({
baseURL: `${import.meta.env.VITE_API_CORE_URL}`,
});
axiosInstanceAuth.interceptors.request.use(
(config) => {
const token = useAuthStore.getState().token;
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
axiosInstance.interceptors.request.use(
(config) => {
const token = useAuthStore.getState().token;
@ -18,6 +35,13 @@ axiosInstance.interceptors.request.use(
}
);
export const fetcher = (url: string) => axiosInstance.get(url).then(res => res.data)
export const fetcher = (url: string) => axiosInstance.get(url, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}).then(res => res.data)
export const blobFetcher = (url: string) => axiosInstance.get(url, {
responseType: "blob"
}).then(res => res.data)
export default axiosInstance;

View File

@ -14,7 +14,7 @@ import Container from '@mui/material/Container';
import MenuIcon from '@mui/icons-material/Menu';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import NotificationsIcon from '@mui/icons-material/Notifications';
import { AccountCircle, Api, ExitToApp, Home, People, Settings, Shield, Storage } from '@mui/icons-material';
import { AccountCircle, Api, Assignment, ExitToApp, Home, People, Print, Report, Schedule, SendAndArchive, Settings, Shield, Storage, Tablet } from '@mui/icons-material';
import { ListItem, ListItemButton, ListItemIcon, ListItemText, colors } from '@mui/material';
import { Outlet, useNavigate } from 'react-router-dom';
import { UserData } from '../interfaces/auth';
@ -93,6 +93,11 @@ const pages = [
path: "/documents",
icon: <Storage />
},
{
label: "Отчеты",
path: "/reports",
icon: <Assignment />
},
{
label: "API Test",
path: "/api-test",

View File

@ -1,31 +1,101 @@
import { useEffect, useState } from "react"
import { Button, Typography } from "@mui/material"
import axiosInstance from "../http/axiosInstance"
import DataTable from "../components/DataTable"
import { GridColDef } from "@mui/x-data-grid"
export default function ApiTest() {
const [data, setData] = useState<any>(null)
const [state, setState] = useState<any>(null)
const [exportData, setExportData] = useState<any>(null)
function getRealtimeData(data: any) {
setData(data)
const fetch = async () => {
await axiosInstance.get(`/info/reports/0?to_export=true`, {
responseType: 'blob'
}).then(response => {
setExportData(response.data)
console.log(response.data)
const url = window.URL.createObjectURL(response.data)
const link = document.createElement('a')
link.href = url
link.setAttribute('download', 'report.xlsx')
document.body.appendChild(link);
link.click();
link.remove();
})
}
useEffect(() => {
const sse = new EventSource(`${import.meta.env.VITE_API_SSE_URL}/stream`)
const fetchBlob = async () => {
// await axiosInstance.get(`/info/document/1&2`, {
// responseType: 'blob'
// }).then(response => {
// setState(response)
// })
sse.onmessage = e => getRealtimeData(e.data)
await axiosInstance.get(`/info/reports/0`).then(response => {
setState(JSON.parse(response.data))
})
}
sse.onerror = () => {
sse.close()
}
return () => {
sse.close()
}
}, [])
const columns: GridColDef[] =
[
{ field: 'id', headerName: "№", type: "number", width: 90 },
{ field: 'region', headerName: 'Регион', type: "string", width: 90, },
{ field: 'city', headerName: 'Город', type: "string", width: 130 },
{ field: 'name_type', headerName: 'Вид объекта', type: "string", width: 90, },
{ field: 'name', headerName: 'Наименование', type: "string", width: 90, },
// { field: 'code', headerName: 'Код', type: "string", width: 130 },
// { field: 'code_city', headerName: '', type: "string", width: 90, },
// { field: 'code_fuel', headerName: '', type: "string", width: 90, },
// { field: 'code_master', headerName: '', type: "string", width: 90, },
// { field: 'code_region', headerName: '', type: "string", width: 90, },
// { field: 'code_type', headerName: '', type: "string", width: 90, },
{ field: 'num', headerName: 'Инвентарный код', type: "string", width: 90, },
{ field: 'fuel_type', headerName: 'Тип топлива', type: "string", width: 90, },
{ field: 'fuel', headerName: 'Топливо', type: "string", width: 90, },
{ field: 'zone', headerName: 'Мертвая зона', type: "string", width: 90, },
{ field: 'active', headerName: 'Активность', type: "boolean", width: 70 },
// { field: 'full_name', headerName: 'Полное наименование', type: "string", width: 90, },
];
return (
<>
<Typography>
{JSON.stringify(data)}
<Button onClick={() => {
fetchBlob()
}}>
Получить таблицу
</Button>
<Button onClick={() => {
fetch()
}}>
Экспорт
</Button>
{state &&
<DataTable
checkboxSelection={false}
columns={
[
{ field: 'id', headerName: '№', width: 70 },
...Object.keys(state).map(key => ({
field: key,
headerName: key.charAt(0).toUpperCase() + key.slice(1),
width: 150
}))
]
}
rows={
[...new Set(Object.keys(state).flatMap(key => Object.keys(state[key])))].map(id => {
const row: any = { id: Number(id) };
Object.keys(state).forEach(key => {
row[key] = state[key][id];
});
return row;
})
} />
}
</Typography>
</>
)

View File

@ -1,7 +1,14 @@
import { Error } from "@mui/icons-material";
import { Box, Typography, colors } from "@mui/material";
import { red } from "@mui/material/colors";
export default function NotFound() {
return (
<>
<h1>Page not found</h1>
<Box sx={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
<Error />
<Typography>Запрашиваемая страница не найдена.</Typography>
</Box>
</>
)
}

View File

@ -0,0 +1,71 @@
import { useEffect, useState } from "react"
import { Box, Button, Typography } from "@mui/material"
import axiosInstance from "../http/axiosInstance"
import DataTable from "../components/DataTable"
export default function Reports() {
const [state, setState] = useState<any>(null)
const [exportData, setExportData] = useState<any>(null)
const fetch = async () => {
await axiosInstance.get(`/info/reports/0?to_export=true`, {
responseType: 'blob',
}).then(response => {
setExportData(response.data)
const url = window.URL.createObjectURL(response.data)
const link = document.createElement('a')
link.href = url
link.setAttribute('download', 'report.xlsx')
document.body.appendChild(link);
link.click();
link.remove();
})
}
const fetchBlob = async () => {
await axiosInstance.get(`/info/reports/0`).then(response => {
setState(JSON.parse(response.data))
})
}
return (
<>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<Box>
<Button onClick={() => fetchBlob()}>
Получить отчет
</Button>
<Button onClick={() => fetch()}>
Экспорт
</Button>
</Box>
{state &&
<DataTable
checkboxSelection={false}
columns={
[
{ field: 'id', headerName: '№', width: 70 },
...Object.keys(state).map(key => ({
field: key,
headerName: key.charAt(0).toUpperCase() + key.slice(1),
width: 150
}))
]
}
rows={
[...new Set(Object.keys(state).flatMap(key => Object.keys(state[key])))].map(id => {
const row: any = { id: Number(id) };
Object.keys(state).forEach(key => {
row[key] = state[key][id];
});
return row;
})
} />
}
</Box>
</>
)
}

View File

@ -1,8 +1,8 @@
import axiosInstance from "../http/axiosInstance";
import axiosInstance, { axiosInstanceAuth } from "../http/axiosInstance";
export default class AuthService {
static async login(data: any) {
return await axiosInstance.post(`/auth/login`, data, {
return await axiosInstanceAuth.post(`/auth/login`, data, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
@ -10,10 +10,10 @@ export default class AuthService {
}
static async refreshToken(token: string) {
return await axiosInstance.post(`/auth/refresh_token/${token}`)
return await axiosInstanceAuth.post(`/auth/refresh_token/${token}`)
}
static async getCurrentUser(token: string) {
return await axiosInstance.get(`/auth/get_current_user/${token}`)
return await axiosInstanceAuth.get(`/auth/get_current_user/${token}`)
}
}

View File

@ -1,18 +1,18 @@
import axiosInstance from "../http/axiosInstance";
import axiosInstance, { axiosInstanceAuth } from "../http/axiosInstance";
import { IRoleCreate } from "../interfaces/role";
export default class RoleService {
static async getRoles() {
return await axiosInstance.get(`/auth/roles`)
return await axiosInstanceAuth.get(`/auth/roles`)
}
static async createRole(data: IRoleCreate) {
return await axiosInstance.post(`/auth/roles/`, data)
return await axiosInstanceAuth.post(`/auth/roles/`, data)
}
static async getRoleById(id: number) {
return await axiosInstance.get(`/auth/roles/${id}`)
return await axiosInstanceAuth.get(`/auth/roles/${id}`)
}
// static async deleteRole(id: number) {

View File

@ -1,17 +1,17 @@
import axiosInstance from "../http/axiosInstance";
import axiosInstance, { axiosInstanceAuth } from "../http/axiosInstance";
import { UserCreds, UserData } from "../interfaces/auth";
export default class UserService {
static async createUser(data: any) {
return await axiosInstance.post(`/auth/user`, data)
return await axiosInstanceAuth.post(`/auth/user`, data)
}
static async getCurrentUser(token: string) {
return await axiosInstance.get(`/auth/get_current_user/${token}`)
return await axiosInstanceAuth.get(`/auth/get_current_user/${token}`)
}
static async getUsers() {
return await axiosInstance.get(`/auth/user`)
return await axiosInstanceAuth.get(`/auth/user`)
}
// static async deleteUser(id: number) {
@ -19,14 +19,14 @@ export default class UserService {
// }
static async getUser(id: number) {
return await axiosInstance.get(`/auth/user/${id}`)
return await axiosInstanceAuth.get(`/auth/user/${id}`)
}
static async updatePassword(data: UserCreds) {
return await axiosInstance.put(`/auth/user/password_change`, data)
return await axiosInstanceAuth.put(`/auth/user/password_change`, data)
}
static async updateUser(data: UserData) {
return await axiosInstance.put(`/auth/user`, data)
return await axiosInstanceAuth.put(`/auth/user`, data)
}
}