Add frontend pagination, add per_page query param

This commit is contained in:
adam-skowronek 2022-06-08 21:48:49 +02:00
parent b550fea998
commit 1afd4f8f51
8 changed files with 157 additions and 61 deletions

View File

@ -20,8 +20,7 @@ def order_by_column_name(query: BaseQuery, model_field: str, order_by_col_name:
return query return query
def paginate_models(page: str, query: BaseQuery) -> PaginationResponse or Tuple[dict, int]: def paginate_models(page: str, query: BaseQuery, per_page = 10) -> PaginationResponse or Tuple[dict, int]:
per_page = 10
default_page = 1 default_page = 1
if page is not None: if page is not None:

View File

@ -26,11 +26,12 @@ def list_students(query: dict) -> dict:
order_by_first_name = query.get('order_by_first_name') order_by_first_name = query.get('order_by_first_name')
order_by_last_name = query.get('order_by_last_name') order_by_last_name = query.get('order_by_last_name')
page = query.get('page') page = query.get('page')
per_page = query.get('per_page')
student_query = Student.search_by_fullname_and_order_by_first_name_or_last_name(fullname, order_by_first_name, student_query = Student.search_by_fullname_and_order_by_first_name_or_last_name(fullname, order_by_first_name,
order_by_last_name) order_by_last_name)
response = paginate_models(page, student_query) response = paginate_models(page, student_query, per_page)
if (message := response.get('message')) is not None: if (message := response.get('message')) is not None:
abort(response['status_code'], message) abort(response['status_code'], message)
# print(get_debug_queries()[0]) # print(get_debug_queries()[0])

View File

@ -16,6 +16,7 @@
"@types/react": "^18.0.9", "@types/react": "^18.0.9",
"@types/react-dom": "^18.0.4", "@types/react-dom": "^18.0.4",
"axios": "^0.27.2", "axios": "^0.27.2",
"classnames": "^2.3.1",
"daisyui": "^2.15.2", "daisyui": "^2.15.2",
"react": "^18.1.0", "react": "^18.1.0",
"react-dom": "^18.1.0", "react-dom": "^18.1.0",
@ -5260,6 +5261,11 @@
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz",
"integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==" "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA=="
}, },
"node_modules/classnames": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
},
"node_modules/clean-css": { "node_modules/clean-css": {
"version": "5.3.0", "version": "5.3.0",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.0.tgz", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.0.tgz",
@ -20237,6 +20243,11 @@
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz",
"integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==" "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA=="
}, },
"classnames": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
},
"clean-css": { "clean-css": {
"version": "5.3.0", "version": "5.3.0",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.0.tgz", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.0.tgz",

View File

@ -11,6 +11,7 @@
"@types/react": "^18.0.9", "@types/react": "^18.0.9",
"@types/react-dom": "^18.0.4", "@types/react-dom": "^18.0.4",
"axios": "^0.27.2", "axios": "^0.27.2",
"classnames": "^2.3.1",
"daisyui": "^2.15.2", "daisyui": "^2.15.2",
"react": "^18.1.0", "react": "^18.1.0",
"react-dom": "^18.1.0", "react-dom": "^18.1.0",

View File

@ -1,5 +1,7 @@
import axiosInstance from './axiosInstance' import axiosInstance from './axiosInstance'
type OrderType = 'asc' | 'desc'
interface StudentResponse { interface StudentResponse {
max_pages: number max_pages: number
students: Student[] students: Student[]
@ -13,9 +15,18 @@ export interface Student {
group?: any group?: any
} }
export const getStudents = () => export const getStudents = (
params: Partial<{
fullname: string
order_by_first_name: OrderType
order_by_last_name: OrderType
page: number
per_page: number
}> = {},
) =>
axiosInstance.get<StudentResponse>( axiosInstance.get<StudentResponse>(
'http://127.0.0.1:5000/api/coordinator/students', 'http://127.0.0.1:5000/api/coordinator/students',
{ params },
) )
export const createStudent = (payload: Student) => export const createStudent = (payload: Student) =>

View File

@ -4,7 +4,7 @@ const TopBar = () => {
const linkClass = ({ isActive }: { isActive: boolean }) => const linkClass = ({ isActive }: { isActive: boolean }) =>
isActive ? 'underline font-bold' : '' isActive ? 'underline font-bold' : ''
return ( return (
<div className="flex items-center bg-gray-300 p-6"> <div className="flex items-center bg-gray-300 py-6 px-10 shadow">
<h1 className="text-xl font-bold">System PRI</h1> <h1 className="text-xl font-bold">System PRI</h1>
<div className="flex ml-10 gap-3"> <div className="flex ml-10 gap-3">
<NavLink className={linkClass} to="/coordinator/groups"> <NavLink className={linkClass} to="/coordinator/groups">

View File

@ -1,17 +1,40 @@
import React, { useState } from 'react' import React, { useEffect, useState } from 'react'
import { useMutation, useQuery } from 'react-query' import { useMutation, useQuery } from 'react-query'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { getStudents, uploadStudents } from '../../api/students' import { getStudents, uploadStudents } from '../../api/students'
import classNames from 'classnames'
const Students = () => { const Students = () => {
let navigate = useNavigate() let navigate = useNavigate()
const [showGroupless, setShowGroupless] = useState(false) const [showGroupless, setShowGroupless] = useState(false)
const [page, setPage] = useState(1)
const [perPage, setPerPage] = useState(10)
const perPageOptions = [
{
value: 10,
label: '10 rekordów',
},
{
value: 20,
label: '20 rekordów',
},
{
value: 50,
label: '50 rekordów',
},
{
value: 1000,
label: 'Pokaż wszystkie',
},
]
const { const {
isLoading: isStudentsLoading, isLoading: isStudentsLoading,
data: students, data: students,
refetch: refetchStudents, refetch: refetchStudents,
} = useQuery('students', () => getStudents()) } = useQuery(['students', page, perPage], () =>
getStudents({ page, per_page: perPage }),
)
const { mutate: mutateUpload } = useMutation( const { mutate: mutateUpload } = useMutation(
'uploadStudents', 'uploadStudents',
@ -28,65 +51,115 @@ const Students = () => {
mutateUpload(payload) mutateUpload(payload)
} }
if (isStudentsLoading) { useEffect(() => {
return <div>Ładowanie</div> setPage(1)
} }, [perPage])
return ( return (
<div> <div>
<div className="flex items-center"> <div className="flex items-center justify-between">
<button <div>
className="btn btn-success" <button
onClick={() => navigate('/coordinator/add-student')} className="btn btn-success"
> onClick={() => navigate('/coordinator/add-student')}
Dodaj nowego studenta >
</button> Dodaj nowego studenta
<label className="ml-4 btn btn-success btn-outline" htmlFor="file"> </button>
Importuj <label className="ml-4 btn btn-success btn-outline" htmlFor="file">
</label> Importuj
<input </label>
type="file"
id="file"
name="avatar"
accept=".csv"
className="hidden"
onChange={handleOnChange}
/>
<label className="label ml-auto">
<span className="mr-2">Tylko niezapisani</span>
<input <input
type="checkbox" type="file"
className="checkbox" id="file"
onChange={() => setShowGroupless(!showGroupless)} name="avatar"
accept=".csv"
className="hidden"
onChange={handleOnChange}
/> />
</label> </div>
<div className="flex">
<select className="select select-bordered mr-3">
{perPageOptions.map(({ value, label }) => (
<option onClick={() => setPerPage(value)}>{label}</option>
))}
</select>
<label className="label">
<span className="mr-2">Tylko niezapisani</span>
<input
type="checkbox"
className="checkbox"
onChange={() => setShowGroupless(!showGroupless)}
/>
</label>
</div>
</div> </div>
{isStudentsLoading ? (
<div className="flex mx-auto mt-5 overflow-hidden overflow-x-auto border border-gray-100 rounded"> <div>Ładowanie</div>
<table className="min-w-full table table-compact"> ) : (
<thead> <>
<tr className="bg-gray-50"> <div className="flex mx-auto mt-5 overflow-hidden overflow-x-auto border border-gray-100 rounded">
<th>Imię</th> <table className="min-w-full table table-compact">
<th>Nazwisko</th> <thead>
<th>Indeks</th> <tr className="bg-gray-50">
<th>Zapisany</th> <th>Imię</th>
<th>Tryb</th> <th>Nazwisko</th>
</tr> <th>Indeks</th>
</thead> <th>Zapisany</th>
<tbody className="divide-y divide-gray-100"> <th>Tryb</th>
{students?.data?.students
?.filter((st) => st.group === null || !showGroupless)
.map(({ first_name, last_name, index, group, mode }) => (
<tr key={index}>
<td>{first_name}</td>
<td>{last_name}</td>
<td>{index}</td>
<td>{group === null ? 'Nie' : 'Tak'}</td>
<td>{mode ? 'stacjonarny' : 'niestacjonarny'}</td>
</tr> </tr>
))} </thead>
</tbody> <tbody className="divide-y divide-gray-100">
</table> {students?.data?.students
</div> ?.filter((st) => st.group === null || !showGroupless)
.map(({ first_name, last_name, index, group, mode }) => (
<tr key={index}>
<td>{first_name}</td>
<td>{last_name}</td>
<td>{index}</td>
<td>{group === null ? 'Nie' : 'Tak'}</td>
<td>{mode ? 'stacjonarny' : 'niestacjonarny'}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="w-full flex items-center justify-center mt-2">
<div className="btn-group">
<button
className="btn btn-outline"
onClick={() => setPage(page - 1)}
disabled={page === 1}
>
«
</button>
{[
...Array(
students?.data?.max_pages && students?.data?.max_pages + 1,
).keys(),
]
.slice(1)
.map((p) => (
<button
key={p}
className={classNames('btn btn-outline', {
'bg-success': p === page,
})}
onClick={() => setPage(p)}
>
{p}
</button>
))}
<button
className="btn btn-outline"
onClick={() => setPage(page + 1)}
disabled={page === students?.data?.max_pages}
>
»
</button>
</div>
</div>
</>
)}
</div> </div>
) )
} }

View File

@ -1,6 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es2015",
"lib": [ "lib": [
"dom", "dom",
"dom.iterable", "dom.iterable",