add QualificationsPage

This commit is contained in:
Dawid Wysokiński 2021-03-12 15:58:15 +01:00
parent a47cbab211
commit 94cd4129db
10 changed files with 567 additions and 0 deletions

View File

@ -2,6 +2,7 @@ export enum Route {
SignInPage = '/',
UsersPage = '/users',
ProfessionsPage = '/professions',
QualificationsPage = '/qualifications',
}
export const PUBLIC_ROUTES = [Route.SignInPage];

View File

@ -3,6 +3,7 @@ import { Route } from '../config/routing';
import AppLayout from 'common/AppLayout/AppLayout';
import UsersPage from './UsersPage/UsersPage';
import ProfessionsPage from './ProfessionsPage/ProfessionsPage';
import QualificationsPage from './QualificationsPage/QualificationsPage';
function AdminRoutes() {
return (
@ -14,6 +15,9 @@ function AdminRoutes() {
<RRDRoute exact path={Route.ProfessionsPage}>
<ProfessionsPage />
</RRDRoute>
<RRDRoute exact path={Route.QualificationsPage}>
<QualificationsPage />
</RRDRoute>
</Switch>
</AppLayout>
);

View File

@ -0,0 +1,229 @@
import { useState } from 'react';
import { ApolloError, useMutation } from '@apollo/client';
import { useUpdateEffect } from 'react-use';
import {
NumberParam,
StringParam,
useQueryParams,
withDefault,
} from 'use-query-params';
import { useSnackbar } from 'notistack';
import SortParam, { decodeSort } from 'libs/serialize-query-params/SortParam';
import useQualifications from './QualificationsPage.useQualifications';
import { validateRowsPerPage } from 'common/Table/helpers';
import {
MUTATION_CREATE_QUALIFICATION,
MUTATION_UPDATE_QUALIFICATION,
MUTATION_DELETE_QUALIFICATIONS,
} from './mutations';
import { COLUMNS, DEFAULT_SORT, DialogType } from './constants';
import {
Maybe,
MutationCreateQualificationArgs,
MutationDeleteQualificationsArgs,
MutationUpdateQualificationArgs,
Qualification,
QualificationInput,
} from 'libs/graphql/types';
import {
Button,
Container,
IconButton,
Paper,
Snackbar,
} from '@material-ui/core';
import { Edit as EditIcon } from '@material-ui/icons';
import Table from 'common/Table/Table';
import TableToolbar from './components/TableToolbar/TableToolbar';
import FormDialog from './components/FormDialog/FormDialog';
const QualificationsPage = () => {
const [createQualificationMutation] = useMutation<
any,
MutationCreateQualificationArgs
>(MUTATION_CREATE_QUALIFICATION, { ignoreResults: true });
const [updateQualificationMutation] = useMutation<
any,
MutationUpdateQualificationArgs
>(MUTATION_UPDATE_QUALIFICATION, { ignoreResults: true });
const [deleteQualificationsMutation] = useMutation<
any,
MutationDeleteQualificationsArgs
>(MUTATION_DELETE_QUALIFICATIONS, { ignoreResults: true });
const [dialogType, setDialogType] = useState<DialogType>(DialogType.None);
const [qualificationBeingEdited, setQualificationBeingEdited] = useState<
Maybe<Qualification>
>(null);
const [selectedQualifications, setSelectedQualifications] = useState<
Qualification[]
>([]);
const snackbar = useSnackbar();
const [{ page, sort, search, ...rest }, setQueryParams] = useQueryParams({
limit: NumberParam,
page: withDefault(NumberParam, 0),
sort: withDefault(SortParam, DEFAULT_SORT),
search: withDefault(StringParam, ''),
});
const limit = validateRowsPerPage(rest.limit);
const { qualifications, total, loading, refetch } = useQualifications(
page,
limit,
sort.toString(),
search
);
useUpdateEffect(() => {
if (selectedQualifications.length > 0) {
setSelectedQualifications([]);
}
}, [qualifications]);
const handleFormDialogSubmit = async (input: QualificationInput) => {
try {
if (dialogType === DialogType.Create) {
await createQualificationMutation({ variables: { input } });
} else {
await updateQualificationMutation({
variables: { input, id: qualificationBeingEdited?.id ?? -1 },
});
}
await refetch();
snackbar.enqueueSnackbar(
dialogType === DialogType.Create
? 'Pomyślnie utworzono kwalifikację.'
: 'Zapisano zmiany.',
{ variant: 'success' }
);
return true;
} catch (e) {
snackbar.enqueueSnackbar(
e instanceof ApolloError && e.graphQLErrors.length > 0
? e.graphQLErrors[0].message
: e.message,
{ variant: 'error' }
);
}
return false;
};
const handleDeleteQualifications = async () => {
if (!window.confirm('Czy na pewno chcesz usunąć wybrane kwalifikacje?')) {
return;
}
try {
const ids = selectedQualifications.map(qualification => qualification.id);
await deleteQualificationsMutation({ variables: { ids } });
await refetch();
snackbar.enqueueSnackbar(`Usuwanie przebiegło pomyślnie.`, {
variant: 'success',
});
} catch (e) {
snackbar.enqueueSnackbar(
e instanceof ApolloError && e.graphQLErrors.length > 0
? e.graphQLErrors[0].message
: e.message,
{ variant: 'error' }
);
}
};
const handleSelect = (checked: boolean, items: Qualification[]) => {
setSelectedQualifications(prevState =>
checked
? [...prevState, ...items]
: prevState.filter(
item => !items.some(otherItem => otherItem.id === item.id)
)
);
};
return (
<Container>
<Paper>
<TableToolbar
search={search}
onClickCreateQualification={() => {
setQualificationBeingEdited(null);
setDialogType(DialogType.Create);
}}
onChangeSearchValue={val => {
setQueryParams({ page: 0, search: val });
}}
/>
<Table
selection
columns={COLUMNS}
data={qualifications}
selected={selectedQualifications}
onSelect={handleSelect}
actions={[
{
icon: row => {
return (
<IconButton
onClick={() => {
setQualificationBeingEdited(row);
setDialogType(DialogType.Edit);
}}
>
<EditIcon />
</IconButton>
);
},
tooltip: 'Edytuj',
},
]}
loading={loading}
orderBy={sort.orderBy}
orderDirection={sort.orderDirection}
onRequestSort={(orderBy, orderDirection) => {
setQueryParams({
page: 0,
sort: decodeSort(orderBy + ' ' + orderDirection),
});
}}
footerProps={{
count: total,
page,
onChangePage: page => {
setQueryParams({ page });
},
onChangeRowsPerPage: limit => {
setQueryParams({ page: 0, limit });
},
rowsPerPage: limit,
}}
/>
</Paper>
<FormDialog
open={
dialogType === DialogType.Create || dialogType === DialogType.Edit
}
qualification={qualificationBeingEdited as QualificationInput}
onSubmit={handleFormDialogSubmit}
onClose={() => setDialogType(DialogType.None)}
/>
<Snackbar
open={selectedQualifications.length > 0}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
message={`Wybrane kwalifikacje: ${selectedQualifications.length}`}
action={
<>
<Button onClick={handleDeleteQualifications} color="secondary">
Usuń
</Button>
<Button
color="secondary"
onClick={() => setSelectedQualifications([])}
>
Anuluj
</Button>
</>
}
/>
</Container>
);
};
export default QualificationsPage;

View File

@ -0,0 +1,36 @@
import { useQuery } from '@apollo/client';
import { QUERY_QUALIFICATIONS } from './queries';
import { Query, QueryQualificationsArgs } from 'libs/graphql/types';
const useQualifications = (
page: number,
limit: number,
sort: string,
search: string
) => {
const { data, loading, refetch } = useQuery<
Pick<Query, 'qualifications'>,
QueryQualificationsArgs
>(QUERY_QUALIFICATIONS, {
fetchPolicy: 'cache-and-network',
variables: {
offset: page * limit,
sort: [sort],
limit,
filter: {
nameIEQ: '%' + search + '%',
},
},
});
return {
qualifications: data?.qualifications.items ?? [],
get loading() {
return this.qualifications.length === 0 && loading;
},
total: data?.qualifications.total ?? 0,
refetch,
};
};
export default useQualifications;

View File

@ -0,0 +1,149 @@
import { useState } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { pick } from 'lodash';
import { MAX_NAME_LENGTH, FORMULAS } from './constants';
import { QualificationInput } from 'libs/graphql/types';
import { makeStyles } from '@material-ui/core/styles';
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogProps,
DialogTitle,
MenuItem,
TextField,
} from '@material-ui/core';
export interface FormDialogProps extends Pick<DialogProps, 'open'> {
qualification?: QualificationInput;
onClose: () => void;
onSubmit: (input: QualificationInput) => Promise<boolean> | boolean;
}
const FormDialog = ({
open,
onClose,
qualification,
onSubmit,
}: FormDialogProps) => {
const editMode = Boolean(qualification);
const {
register,
handleSubmit,
errors,
control,
} = useForm<QualificationInput>({});
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const classes = useStyles();
const _onSubmit = async (data: QualificationInput) => {
setIsSubmitting(true);
const filtered = editMode
? pick(
data,
Object.keys(data).filter(key => data[key as keyof QualificationInput])
)
: data;
const success = await onSubmit(filtered);
setIsSubmitting(false);
if (success) {
onClose();
}
};
return (
<Dialog
open={open}
onClose={isSubmitting ? undefined : onClose}
fullWidth
maxWidth="xs"
>
<form onSubmit={handleSubmit(_onSubmit)}>
<DialogTitle>
{editMode ? 'Edycja kwalifikacji' : 'Tworzenie kwalifikacji'}
</DialogTitle>
<DialogContent className={classes.dialogContent}>
<TextField
fullWidth
label="Nazwa kwalifikacji"
name="name"
defaultValue={qualification?.name}
inputRef={register({
required: 'Te pole jest wymagane.',
maxLength: {
value: MAX_NAME_LENGTH,
message: `Maksymalna długość nazwy kwalifikacji to ${MAX_NAME_LENGTH} znaki.`,
},
})}
error={!!errors.name?.message}
helperText={errors.name?.message ?? ''}
/>
<TextField
fullWidth
label="Oznaczenie kwalifikacji"
name="code"
defaultValue={qualification?.code}
inputRef={register({
required: 'Te pole jest wymagane.',
})}
error={!!errors.code?.message}
helperText={errors.code?.message ?? ''}
/>
<Controller
name="formula"
defaultValue={qualification?.formula ?? FORMULAS[0]}
control={control}
as={
<TextField select fullWidth label="Formuła">
{FORMULAS.map(formula => (
<MenuItem key={formula} value={formula}>
{formula}
</MenuItem>
))}
</TextField>
}
/>
<TextField
fullWidth
label="Opis"
name="description"
defaultValue={qualification?.description}
inputRef={register}
multiline
/>
</DialogContent>
<DialogActions>
<Button
color="secondary"
type="button"
variant="contained"
onClick={onClose}
disabled={isSubmitting}
>
Anuluj
</Button>
<Button
type="submit"
color="primary"
variant="contained"
disabled={isSubmitting}
>
{editMode ? 'Zapisz' : 'Utwórz'}
</Button>
</DialogActions>
</form>
</Dialog>
);
};
const useStyles = makeStyles(theme => ({
dialogContent: {
'& > *:not(:last-child)': {
marginBottom: theme.spacing(1),
},
},
}));
export default FormDialog;

View File

@ -0,0 +1,2 @@
export const MAX_NAME_LENGTH = 100;
export const FORMULAS = ['Formuła 2012', 'Formuła 2017', 'Formuła 2019'];

View File

@ -0,0 +1,60 @@
import { useState } from 'react';
import { useDebounce } from 'react-use';
import { makeStyles } from '@material-ui/core/styles';
import { IconButton, Tooltip } from '@material-ui/core';
import { Add as AddIcon } from '@material-ui/icons';
import BaseTableToolbar from 'common/Table/TableToolbar';
import SearchInput from 'common/Form/SearchInput';
export interface TableToolbarProps {
search: string;
onChangeSearchValue: (search: string) => void;
onClickCreateQualification: () => void;
}
const TableToolbar = ({
search,
onChangeSearchValue,
onClickCreateQualification,
}: TableToolbarProps) => {
const classes = useStyles();
const [_search, setSearch] = useState<string>(search);
useDebounce(
() => {
onChangeSearchValue(_search);
},
500,
[_search]
);
return (
<BaseTableToolbar className={classes.toolbar}>
<SearchInput
value={_search}
onChange={e => {
setSearch(e.target.value);
}}
onResetValue={() => {
setSearch('');
}}
/>
<Tooltip title="Utwórz kwalifikację">
<IconButton onClick={onClickCreateQualification}>
<AddIcon />
</IconButton>
</Tooltip>
</BaseTableToolbar>
);
};
const useStyles = makeStyles(theme => ({
toolbar: {
justifyContent: 'flex-end',
'& > *:not(:last-child)': {
marginRight: theme.spacing(0.5),
},
},
}));
export default TableToolbar;

View File

@ -0,0 +1,34 @@
import { decodeSort } from 'libs/serialize-query-params/SortParam';
import { Column } from 'common/Table/types';
import { Qualification } from 'libs/graphql/types';
export const DEFAULT_SORT = decodeSort('id DESC');
export const COLUMNS: Column<Qualification>[] = [
{
field: 'id',
sortable: true,
label: 'ID',
},
{
field: 'formula',
sortable: true,
label: 'Formuła',
},
{
field: 'name',
sortable: true,
label: 'Nazwa',
valueFormatter: v => `${v.name} (${v.code})`,
},
{
field: 'createdAt',
sortable: true,
label: 'Data utworzenia',
type: 'datetime',
},
];
export enum DialogType {
Create = 'create',
Edit = 'edit',
None = '',
}

View File

@ -0,0 +1,25 @@
import { gql } from '@apollo/client';
export const MUTATION_CREATE_QUALIFICATION = gql`
mutation createProfession($input: QualificationInput!) {
createQualification(input: $input) {
id
}
}
`;
export const MUTATION_UPDATE_QUALIFICATION = gql`
mutation updateQualification($id: ID!, $input: QualificationInput!) {
updateQualification(id: $id, input: $input) {
id
}
}
`;
export const MUTATION_DELETE_QUALIFICATIONS = gql`
mutation deleteQualifications($ids: [ID!]!) {
deleteQualifications(ids: $ids) {
id
}
}
`;

View File

@ -0,0 +1,27 @@
import { gql } from '@apollo/client';
export const QUERY_QUALIFICATIONS = gql`
query qualifications(
$offset: Int
$limit: Int
$filter: QualificationFilter
$sort: [String!]
) {
qualifications(
offset: $offset
limit: $limit
filter: $filter
sort: $sort
) {
total
items {
id
name
code
formula
description
createdAt
}
}
}
`;