@autobusal/operator-routes 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,112 @@
1
+ import { TFunction } from 'i18next';
2
+ import { useParams, Navigate, useNavigate } from 'react-router-dom';
3
+ import { BsPlusCircleFill } from 'react-icons/bs';
4
+ import { Meta, BackWithTitle, Button, Table, Row } from '@autobusal/common';
5
+ import { usePage } from '@autobusal/hooks';
6
+ import { success } from '@autobusal/utilities';
7
+ import { GetAlternates, PostAdd, DeleteAlternate } from '../../../modules/OperatorRoutes/Alternates/services';
8
+
9
+ interface Props {
10
+ url: string
11
+ backUrl: string
12
+ t: TFunction<'common'>
13
+ }
14
+
15
+ const Browse = ({ url, backUrl, t }: Props): JSX.Element => {
16
+ const params = useParams();
17
+
18
+ const id = Number(params.id);
19
+
20
+ url = url.replace('[id]', String(id));
21
+ backUrl = backUrl.replace('[id]', String(id));
22
+
23
+ const navigate = useNavigate();
24
+
25
+ usePage('routes');
26
+
27
+ const { data, isLoading, isFetching, refetch } = GetAlternates(id);
28
+
29
+ const { mutate: Add, isPending } = PostAdd(id);
30
+
31
+ const { mutate: Delete } = DeleteAlternate(id);
32
+
33
+ if (id === 0) {
34
+ return <Navigate to={ backUrl } />;
35
+ }
36
+
37
+ const onNew = (): void => {
38
+ if (!window.confirm(t('alternates.browse.confirm', { ns: 'common' }))) {
39
+ return;
40
+ }
41
+
42
+ Add({}, {
43
+ onSuccess: (data) => {
44
+ success(t('alternates.browse.messages.saved', { ns: 'common' }));
45
+
46
+ navigate(`${ url }/manage/${ data.id }`);
47
+ }
48
+ });
49
+ };
50
+
51
+ const onDelete = (name: number): void => {
52
+ Delete(name, {
53
+ onSuccess: () => {
54
+ success(t('alternates.browse.messages.deleted', { ns: 'common' }));
55
+
56
+ refetch();
57
+ }
58
+ });
59
+ };
60
+
61
+ const rows = data?.alternates.map(item => (
62
+ <Row
63
+ key={ item.id }
64
+ id={ item.id }
65
+ data={ [
66
+ `${ item.date_from } - ${ item.date_to }`
67
+ ] }
68
+ />
69
+ ));
70
+
71
+ const title = t('alternates.browse.title', { name: data?.name, ns: 'common' });
72
+
73
+ return (
74
+ <Meta title={ title }>
75
+ <BackWithTitle title={ title } to={ backUrl } t={ t } />
76
+
77
+ <Table
78
+ url={ url }
79
+ columns={ [{
80
+ name: t('alternates.browse.from', { ns: 'common' }),
81
+ width: 75
82
+ }] }
83
+ rows={ rows }
84
+ loading={ isLoading }
85
+ fetching={ isFetching }
86
+ t={ t }
87
+ actions={ [
88
+ 'view', 'delete', 'extra'
89
+ ] }
90
+ handlers={ {
91
+ delete: onDelete
92
+ } }
93
+ extra={
94
+ <Button
95
+ type="button"
96
+ loading={ isPending }
97
+ text={
98
+ <>
99
+ <BsPlusCircleFill />
100
+ { t('table.actions.new', { ns: 'common' }) }
101
+ </>
102
+ }
103
+ noMargin
104
+ onClick={ onNew }
105
+ />
106
+ }
107
+ />
108
+ </Meta>
109
+ );
110
+ };
111
+
112
+ export default Browse;
@@ -0,0 +1,67 @@
1
+ import { UseMutationResult, UseQueryResult, useMutation, useQuery } from '@tanstack/react-query';
2
+ import { apiClient } from '@autobusal/providers';
3
+ import { RouteData, AlternateData } from '@autobusal/providers/types/routes';
4
+ import { ScheduleForm } from '../Prices/types';
5
+
6
+ export const GetAlternates = (id: number): UseQueryResult<RouteData> => (
7
+ useQuery({
8
+ queryKey: ['operator-alternates', { id }],
9
+ queryFn: async () => (
10
+ await apiClient
11
+ .get('/api/alternates/operator/browse', {
12
+ params: { id }
13
+ })
14
+ .then(response => (
15
+ response.data
16
+ ))
17
+ )
18
+ })
19
+ );
20
+
21
+ export const PostAdd = (id: number): UseMutationResult<AlternateData> => (
22
+ useMutation({
23
+ mutationKey: ['operator-alternates-add', { id }],
24
+ mutationFn: async () => (
25
+ await apiClient
26
+ .post('/api/alternates/operator/add', { id })
27
+ .then(response => (
28
+ response.data
29
+ ))
30
+ )
31
+ })
32
+ );
33
+
34
+ export const PostSchedule = (id: number, name: string): UseMutationResult<void, Error, ScheduleForm, unknown> => (
35
+ useMutation({
36
+ mutationKey: ['operator-alternates-schedule', { id, name }],
37
+ mutationFn: async (data: ScheduleForm) => (
38
+ await apiClient
39
+ .post('/api/alternates/operator/schedule', {
40
+ ...data,
41
+ id,
42
+ name
43
+ })
44
+ .then(response => (
45
+ response.data
46
+ ))
47
+ )
48
+ })
49
+ );
50
+
51
+ export const DeleteAlternate = (id: number): UseMutationResult<void, Error, number, unknown> => (
52
+ useMutation({
53
+ mutationKey: ['operator-alternates-delete'],
54
+ mutationFn: async (name: number) => (
55
+ await apiClient
56
+ .delete('/api/alternates/operator/delete', {
57
+ params: {
58
+ id,
59
+ name
60
+ }
61
+ })
62
+ .then(response => (
63
+ response.data
64
+ ))
65
+ )
66
+ })
67
+ );
@@ -0,0 +1,33 @@
1
+ import { TFunction } from 'i18next';
2
+ import Item from './Item';
3
+ import { Items } from './styles';
4
+ import { PriceData } from '@autobusal/providers/types/routes';
5
+
6
+ interface Props {
7
+ id: number
8
+ type: 'normal' | 'alternate'
9
+ data?: PriceData[]
10
+ name: string
11
+ t: TFunction<'normal'>
12
+ }
13
+
14
+ const Browse = ({ id, type, data, name, t }: Props): JSX.Element | JSX.Element[] | undefined => {
15
+ const items = data?.map(item => (
16
+ <Item
17
+ key={ item.id }
18
+ id={ id }
19
+ type={ type }
20
+ item={ item }
21
+ name={ name }
22
+ t={ t }
23
+ />
24
+ ));
25
+
26
+ return (
27
+ <Items>
28
+ { items }
29
+ </Items>
30
+ );
31
+ };
32
+
33
+ export default Browse;
@@ -0,0 +1,106 @@
1
+ import { TFunction } from 'i18next';
2
+ import { useForm } from 'react-hook-form';
3
+ import { BiEditAlt } from 'react-icons/bi';
4
+ import { Button } from '@autobusal/common';
5
+ import { Validate, Display, success } from '@autobusal/utilities';
6
+ import { Prices, Actions } from './styles';
7
+ import { PriceData } from '@autobusal/providers/types/routes';
8
+ import { PriceForm } from '../types';
9
+ import { PostPrice } from '../services';
10
+
11
+ interface Props {
12
+ id: number
13
+ type: 'normal' | 'alternate'
14
+ item: PriceData
15
+ name: string
16
+ t: TFunction<'normal'>
17
+ }
18
+
19
+ const Item = ({ id, type, item, name, t }: Props): JSX.Element => {
20
+ const { register, handleSubmit, formState: { errors } } = useForm<PriceForm>();
21
+
22
+ const { mutate: Save, isPending } = PostPrice(id, item.id, type, name);
23
+
24
+ const onSubmit = (data: PriceForm): void => {
25
+ Save(data, {
26
+ onSuccess: () => success(t('prices_manage.messages.saved', { ns: 'common' }))
27
+ });
28
+ };
29
+
30
+ return (
31
+ <div className="box">
32
+ <h4>
33
+ { item.from.city.name } ({ item.from.city.country?.name }) &rsaquo; { item.to.city.name } ({ item.to.city.country?.name })
34
+ </h4>
35
+
36
+ <form onSubmit={ handleSubmit(onSubmit) }>
37
+ <Prices>
38
+ <div className="row">
39
+ { t('prices_manage.prices.adult', { ns: 'common' }) }
40
+
41
+ <input type="number" defaultValue={ item.adult } { ...register('adult', Validate('required', t)) } />
42
+
43
+ { Display(errors.adult) }
44
+ </div>
45
+
46
+ <div className="row">
47
+ { t('prices_manage.prices.child', { ns: 'common' }) }
48
+
49
+ <input type="number" defaultValue={ item.child } { ...register('child', Validate('required', t)) } />
50
+
51
+ { Display(errors.child) }
52
+ </div>
53
+
54
+ <div className="row">
55
+ { t('prices_manage.prices.baby', { ns: 'common' }) }
56
+
57
+ <input type="number" defaultValue={ item.baby } { ...register('baby', Validate('required', t)) } />
58
+
59
+ { Display(errors.baby) }
60
+ </div>
61
+ </Prices>
62
+
63
+ <Prices>
64
+ <div className="row">
65
+ { t('prices_manage.prices.adult_return', { ns: 'common' }) }
66
+
67
+ <input type="number" defaultValue={ item.adult_return } { ...register('adult_return', Validate('required', t)) } />
68
+
69
+ { Display(errors.adult_return) }
70
+ </div>
71
+
72
+ <div className="row">
73
+ { t('prices_manage.prices.child_return', { ns: 'common' }) }
74
+
75
+ <input type="number" defaultValue={ item.child_return } { ...register('child_return', Validate('required', t)) } />
76
+
77
+ { Display(errors.child_return) }
78
+ </div>
79
+
80
+ <div className="row">
81
+ { t('prices_manage.prices.baby_return', { ns: 'common' }) }
82
+
83
+ <input type="number" defaultValue={ item.baby_return } { ...register('baby_return', Validate('required', t)) } />
84
+
85
+ { Display(errors.baby_return) }
86
+ </div>
87
+ </Prices>
88
+
89
+ <Actions>
90
+ <Button
91
+ type="submit"
92
+ loading={ isPending }
93
+ text={
94
+ <>
95
+ <BiEditAlt />
96
+ { t('table.actions.update', { ns: 'common' }) }
97
+ </>
98
+ }
99
+ />
100
+ </Actions>
101
+ </form>
102
+ </div>
103
+ );
104
+ };
105
+
106
+ export default Item;
@@ -0,0 +1,22 @@
1
+ import styled from 'styled-components';
2
+
3
+ export const Items = styled.div`
4
+ display: flex;
5
+ flex-direction: column;
6
+ gap: 15px;
7
+ `;
8
+
9
+ export const Prices = styled.div`
10
+ display: flex;
11
+ flex-direction: column;
12
+
13
+ @media (min-width: 540px) {
14
+ flex-direction: row;
15
+ gap: 15px;
16
+ }
17
+ `;
18
+
19
+ export const Actions = styled.div`
20
+ display: flex;
21
+ justify-content: center;
22
+ `;
@@ -0,0 +1,65 @@
1
+ import { TFunction } from 'i18next';
2
+ import { useParams } from 'react-router-dom';
3
+ import { Meta, BackWithTitle } from '@autobusal/common';
4
+ import { usePage } from '@autobusal/hooks';
5
+ import Options from './Options/Options';
6
+ import Schedule from './Schedule/Schedule';
7
+ import Browse from './Browse/Browse';
8
+ import { GetPrices } from './services';
9
+
10
+ interface Props {
11
+ type: 'normal' | 'alternate'
12
+ url: string
13
+ t: TFunction<'common'>
14
+ }
15
+
16
+ const Manage = ({ type, url, t }: Props): JSX.Element => {
17
+ const params = useParams();
18
+
19
+ const id = Number(params.id);
20
+ const name = String(params.name ?? '');
21
+
22
+ url = url.replace('[id]', String(id));
23
+
24
+ usePage('routes');
25
+
26
+ const { data, refetch } = GetPrices(id, type, name);
27
+
28
+ const onReload = () => {
29
+ refetch();
30
+ };
31
+
32
+ const title = t(`prices_manage.title.${ type }`, { name: data?.name, ns: 'common' });
33
+
34
+ // get the correct column
35
+ const prices = type === 'alternate' ? data?.alternates : data?.prices;
36
+
37
+ return (
38
+ <Meta title={ title }>
39
+ <BackWithTitle title={ title } to={ url } t={ t } />
40
+
41
+ <div className="route-prices">
42
+ <Options
43
+ id={ id }
44
+ type={ type }
45
+ name={ name }
46
+ t={ t }
47
+ onReload={ onReload }
48
+ />
49
+
50
+ { type === 'alternate' && (
51
+ <Schedule
52
+ id={ id }
53
+ data={ data?.alternates }
54
+ name={ name }
55
+ t={ t }
56
+ />
57
+ ) }
58
+
59
+ <Browse id={ id } type={ type } data={ prices } name={ name } t={ t } />
60
+ </div>
61
+ </Meta>
62
+ );
63
+ };
64
+
65
+ export default Manage;
@@ -0,0 +1,63 @@
1
+ import { useState, useRef } from 'react';
2
+ import { TFunction } from 'i18next';
3
+ import { FaCaretDown } from 'react-icons/fa';
4
+ import { Inline } from '@autobusal/common';
5
+ import { useOutside } from '@autobusal/hooks';
6
+ import { success } from '@autobusal/utilities';
7
+ import { Container, ButtonMore, Submenu, ButtonOption, Loading } from './styles';
8
+ import { PostCopy } from '../services';
9
+
10
+ interface Props {
11
+ id: number
12
+ type: 'normal' | 'alternate'
13
+ name: string
14
+ t: TFunction<'normal'>
15
+ onReload: () => void
16
+ }
17
+
18
+ const Options = ({ id, type, name, t, onReload }: Props): JSX.Element => {
19
+ const [ show, setShow ] = useState<boolean>(false);
20
+
21
+ const ref = useRef(null);
22
+
23
+ const { mutate: Copy, isPending } = PostCopy(id, type, name);
24
+
25
+ useOutside(ref, () => setShow(false));
26
+
27
+ const onCopy = (): void => {
28
+ if (window.confirm(t('prices_manage.options.copy.ask', { ns: 'common' }))) {
29
+ Copy({}, {
30
+ onSuccess: () => {
31
+ success(t('prices_manage.messages.copied', { ns: 'common' }));
32
+
33
+ onReload();
34
+ }
35
+ });
36
+ }
37
+ };
38
+
39
+ if (isPending) {
40
+ return (
41
+ <Loading>
42
+ <Inline text={ t('prices_manage.options.copy.loading', { ns: 'common' }) } />
43
+ </Loading>
44
+ );
45
+ }
46
+
47
+ return (
48
+ <Container ref={ ref }>
49
+ <ButtonMore onClick={ () => setShow(true) }>
50
+ { t('prices_manage.options.more', { ns: 'common' }) }
51
+ <FaCaretDown />
52
+ </ButtonMore>
53
+
54
+ { show && (
55
+ <Submenu>
56
+ <ButtonOption onClick={ onCopy }>{ t('prices_manage.options.copy.text', { ns: 'common' }) }</ButtonOption>
57
+ </Submenu>
58
+ ) }
59
+ </Container>
60
+ );
61
+ };
62
+
63
+ export default Options;
@@ -0,0 +1,52 @@
1
+ import styled from 'styled-components';
2
+
3
+ export const Container = styled.div`
4
+ position: relative;
5
+ display: flex;
6
+ margin-bottom: 10px;
7
+ `;
8
+
9
+ export const ButtonMore = styled.button`
10
+ margin-left: auto;
11
+ display: flex;
12
+ align-items: center;
13
+ gap: 3px;
14
+ color: ${ props => props.theme.font.info };
15
+ font-size: ${ props => props.theme.size.xs };
16
+
17
+ &:hover {
18
+ text-decoration: underline;
19
+ }
20
+ `;
21
+
22
+ export const Submenu = styled.div`
23
+ position: absolute;
24
+ top: 25px;
25
+ right: 0;
26
+ width: 200px;
27
+ padding: 5px;
28
+ background: ${ props => props.theme.submenu.transparent };
29
+ border-radius: ${ props => props.theme.borderRadius };
30
+ box-shadow: ${ props => props.theme.boxShadow };
31
+ backdrop-filter: blur(15px);
32
+ -webkit-backdrop-filter: blur(15px);
33
+ `;
34
+
35
+ export const ButtonOption = styled.button`
36
+ display: block;
37
+ width: 100%;
38
+ padding: 5px 10px;
39
+ font-size: ${ props => props.theme.size.s };
40
+ color: ${ props => props.theme.submenu.font };
41
+ border-radius: ${ props => props.theme.borderRadius };
42
+
43
+ &:hover {
44
+ background: ${ props => props.theme.submenu.hover };
45
+ }
46
+ `;
47
+
48
+ export const Loading = styled.div`
49
+ display: flex;
50
+ justify-content: center;
51
+ padding: 25px 0;
52
+ `;
@@ -0,0 +1,93 @@
1
+ import { TFunction } from 'i18next';
2
+ import { useForm } from 'react-hook-form';
3
+ import { BiEditAlt } from 'react-icons/bi';
4
+ import { Calendar, Button } from '@autobusal/common';
5
+ import { Display, success } from '@autobusal/utilities';
6
+ import { Container, Dates } from './styles';
7
+ import { AlternateData } from '@autobusal/providers/types/routes';
8
+ import { PostSchedule } from '../../Alternates/services';
9
+ import { ScheduleForm } from '../types';
10
+
11
+ interface Props {
12
+ id: number
13
+ data?: AlternateData[]
14
+ name: string
15
+ t: TFunction<'normal'>
16
+ }
17
+
18
+ const Schedule = ({ id, data, name, t }: Props): JSX.Element => {
19
+ const { register, handleSubmit, formState: { errors }, setValue } = useForm<ScheduleForm>();
20
+
21
+ // we get the 1st value, as they're identical
22
+ const details = data !== undefined ? data[0] : undefined;
23
+
24
+ const { mutate: Save, isPending } = PostSchedule(id, name);
25
+
26
+ const onSubmit = (data: ScheduleForm): void => {
27
+ Save(data, {
28
+ onSuccess: () => success(t('alternates.manage.messages.saved', { ns: 'common' }))
29
+ });
30
+ };
31
+
32
+ return (
33
+ <Container className="box">
34
+ <h2>{ t('alternates.manage.title', { ns: 'common' }) }</h2>
35
+
36
+ <form onSubmit={ handleSubmit(onSubmit) }>
37
+ <Dates>
38
+ <div className="row">
39
+ { t('alternates.manage.from', { ns: 'common' }) }
40
+
41
+ <Calendar
42
+ type="picker"
43
+ name="date_from"
44
+ defaultValue={ details?.date_from }
45
+ t={ t }
46
+ validation="required"
47
+ refs={ register }
48
+ onUpdate={ setValue }
49
+ />
50
+
51
+ { Display(errors.date_from) }
52
+ </div>
53
+
54
+ <div className="row">
55
+ { t('alternates.manage.to', { ns: 'common' }) }
56
+
57
+ <Calendar
58
+ type="picker"
59
+ name="date_to"
60
+ defaultValue={ details?.date_to }
61
+ t={ t }
62
+ validation="required"
63
+ refs={ register }
64
+ onUpdate={ setValue }
65
+ />
66
+
67
+ { Display(errors.date_to) }
68
+ </div>
69
+ </Dates>
70
+
71
+ <div className="row">
72
+ <label>
73
+ <input type="checkbox" value="1" defaultChecked={ details?.offer } { ...register('offer') } />&nbsp;
74
+ { t('alternates_manage.offer', { ns: 'common' }) }
75
+ </label>
76
+ </div>
77
+
78
+ <Button
79
+ type="submit"
80
+ loading={ isPending }
81
+ text={
82
+ <>
83
+ <BiEditAlt />
84
+ { t('table.actions.update', { ns: 'common' }) }
85
+ </>
86
+ }
87
+ />
88
+ </form>
89
+ </Container>
90
+ );
91
+ };
92
+
93
+ export default Schedule;
@@ -0,0 +1,10 @@
1
+ import styled from 'styled-components';
2
+
3
+ export const Container = styled.div`
4
+ margin-bottom: 50px;
5
+ `;
6
+
7
+ export const Dates = styled.div`
8
+ display: flex;
9
+ gap: 15px;
10
+ `;
@@ -0,0 +1,59 @@
1
+ import { UseMutationResult, UseSuspenseQueryResult, useMutation, useSuspenseQuery } from '@tanstack/react-query';
2
+ import { apiClient } from '@autobusal/providers';
3
+ import { RouteData } from '@autobusal/providers/types/routes';
4
+ import { PriceForm } from './types';
5
+
6
+ export const GetPrices = (id: number, type: string, name: string): UseSuspenseQueryResult<RouteData> => (
7
+ useSuspenseQuery({
8
+ queryKey: [`operator-prices-${ type }`, { id, type, name }],
9
+ queryFn: async () => (
10
+ await apiClient
11
+ .get('/api/prices/operator/get', {
12
+ params: {
13
+ id,
14
+ type,
15
+ name
16
+ }
17
+ })
18
+ .then(response => (
19
+ response.data
20
+ ))
21
+ )
22
+ })
23
+ );
24
+
25
+ export const PostPrice = (id: number, price_id: number, type: ('normal' | 'alternate'), name: string): UseMutationResult<void, Error, PriceForm, unknown> => (
26
+ useMutation({
27
+ mutationKey: ['operator-prices-save', { id, price_id, type, name }],
28
+ mutationFn: async (data: PriceForm) => (
29
+ await apiClient
30
+ .post('/api/prices/operator/update', {
31
+ ...data,
32
+ id,
33
+ price_id,
34
+ type,
35
+ name
36
+ })
37
+ .then(response => (
38
+ response.data
39
+ ))
40
+ )
41
+ })
42
+ );
43
+
44
+ export const PostCopy = (id: number, type: ('normal' | 'alternate'), name: string): UseMutationResult<void> => (
45
+ useMutation({
46
+ mutationKey: ['operator-prices-copy', { id, type, name }],
47
+ mutationFn: async () => (
48
+ await apiClient
49
+ .post('/api/prices/operator/copy', {
50
+ id,
51
+ type,
52
+ name
53
+ })
54
+ .then(response => (
55
+ response.data
56
+ ))
57
+ )
58
+ })
59
+ );
@@ -0,0 +1,14 @@
1
+ export interface PriceForm {
2
+ adult: number
3
+ child: number
4
+ baby: number
5
+ adult_return: number
6
+ child_return: number
7
+ baby_return: number
8
+ }
9
+
10
+ export interface ScheduleForm {
11
+ date_from: string
12
+ date_to: string
13
+ offer: number
14
+ }
package/index.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import Browse from './Browse';
2
2
  import Manage from './Manage';
3
+ import AlternatesBrowse from './Alternates/Browse';
3
4
  import LocationsManage from './Locations/Manage';
5
+ import PricesManage from './Prices/Manage';
4
6
  import SchedulesManage from './Schedule/Manage';
5
7
  import TransitingManage from './Transiting/Manage';
6
8
  import UnavailableBrowse from './Unavailable/Browse';
@@ -9,7 +11,9 @@ import UnavailableManage from './Unavailable/Manage';
9
11
  export {
10
12
  Browse,
11
13
  Manage,
14
+ AlternatesBrowse,
12
15
  LocationsManage,
16
+ PricesManage,
13
17
  SchedulesManage,
14
18
  TransitingManage,
15
19
  UnavailableBrowse,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/operator-routes",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "type": "module",
5
5
  "main": "index.ts"
6
6
  }