@autobusal/operator-routes 1.0.4 → 1.0.6

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,71 @@
1
+ import { TFunction } from 'i18next';
2
+ import { useForm } from 'react-hook-form';
3
+ import { FiPlus } from 'react-icons/fi';
4
+ import { Button } from '@autobusal/common';
5
+ import { Validate, Display, success } from '@autobusal/utilities';
6
+ import Loading from './Loading';
7
+ import { getStops } from '../utilities';
8
+ import { Container, Title, ContainerAdd, Choose, Notice } from './styles';
9
+ import { AddForm } from '../types';
10
+ import { GetStops, AddLocation } from '../services';
11
+
12
+ interface Props {
13
+ routeId: number
14
+ t: TFunction<'normal'>
15
+ onReload: () => void
16
+ }
17
+
18
+ const Add = ({ routeId, t, onReload }: Props): JSX.Element => {
19
+ const { register, handleSubmit, formState: { errors } } = useForm<AddForm>();
20
+
21
+ const { data, isLoading } = GetStops(routeId);
22
+
23
+ const { mutate: Save, isPending } = AddLocation(routeId);
24
+
25
+ if (isLoading) {
26
+ return <Loading t={ t } />;
27
+ }
28
+
29
+ const onSubmit = (data: AddForm): void => {
30
+ Save(data, {
31
+ onSuccess: () => {
32
+ success(t('locations_manage.messages.added', { ns: 'common' }));
33
+
34
+ onReload();
35
+ }
36
+ });
37
+ };
38
+
39
+ return (
40
+ <Container className="box">
41
+ <Title>{ t('locations_manage.add_city.title', { ns: 'common' }) }</Title>
42
+
43
+ <form onSubmit={ handleSubmit(onSubmit) }>
44
+ <ContainerAdd>
45
+ <Choose>
46
+ <select { ...register('group_id', Validate('required', t)) }>
47
+ { getStops(t, data) }
48
+ </select>
49
+
50
+ { Display(errors.group_id) }
51
+ </Choose>
52
+
53
+ <Button
54
+ loading={ isPending }
55
+ text={
56
+ <>
57
+ <FiPlus />
58
+ { t('locations_manage.add_city.action', { ns: 'common' }) }
59
+ </>
60
+ }
61
+ noMargin
62
+ />
63
+ </ContainerAdd>
64
+
65
+ <Notice>{ t('locations_manage.add_city.notice', { ns: 'common' }) }</Notice>
66
+ </form>
67
+ </Container>
68
+ );
69
+ };
70
+
71
+ export default Add;
@@ -0,0 +1,17 @@
1
+ import { TFunction } from 'i18next';
2
+ import { Inline } from '@autobusal/common';
3
+ import { Container, Title } from './styles';
4
+
5
+ interface Props {
6
+ t: TFunction<'normal'>
7
+ }
8
+
9
+ const Loading = ({ t }: Props): JSX.Element => (
10
+ <Container className="box">
11
+ <Title>{ t('locations_manage.add_city.title', { ns: 'common' }) }</Title>
12
+
13
+ <Inline text={ t('locations_manage.add_city.loading', { ns: 'common' }) } />
14
+ </Container>
15
+ );
16
+
17
+ export default Loading;
@@ -0,0 +1,39 @@
1
+ import styled from 'styled-components';
2
+
3
+ export const Container = styled.div`
4
+ margin-bottom: 25px;
5
+ `;
6
+
7
+ export const Title = styled.h5`
8
+ margin-bottom: 10px;
9
+ padding-bottom: 15px;
10
+ font-size: ${ props => props.theme.size.m };
11
+ border-bottom: 1px solid ${ props => props.theme.background.neutral };
12
+ `;
13
+
14
+ export const ContainerAdd = styled.div`
15
+ display: flex;
16
+ align-items: center;
17
+ flex-direction: column;
18
+ gap: 20px;
19
+
20
+ @media (min-width: 640px) {
21
+ flex-direction: row;
22
+ }
23
+ `;
24
+
25
+ export const Choose = styled.div`
26
+ flex: 1;
27
+ width: 100%;
28
+ `;
29
+
30
+ export const Notice = styled.div`
31
+ margin-top: 15px;
32
+ font-size: ${ props => props.theme.size.xs };
33
+ color: ${ props => props.theme.font.info };
34
+ text-align: center;
35
+
36
+ @media (min-width: 640px) {
37
+ text-align: left;
38
+ }
39
+ `;
@@ -0,0 +1,90 @@
1
+ import { TFunction } from 'i18next';
2
+ import { FiArrowDownCircle, FiArrowUpCircle } from 'react-icons/fi';
3
+ import { BiEditAlt, BiTrash } from 'react-icons/bi';
4
+ import { Button } from '@autobusal/common';
5
+ import { ContainerActions, Group } from './styles';
6
+ import { LocationData } from '@autobusal/providers/types/locations';
7
+
8
+ interface Props {
9
+ item: LocationData
10
+ loading: boolean
11
+ t: TFunction<'normal'>
12
+ onMove: (order: number) => void
13
+ onDelete: () => void
14
+ }
15
+
16
+ const Actions = ({ item, loading, t, onMove, onDelete }: Props): JSX.Element => {
17
+ const onChange = (type: 'up' | 'down'): void => {
18
+ if (!window.confirm(t('locations_manage.actions.move', { ns: 'common' }))) {
19
+ return;
20
+ }
21
+
22
+ const order = type === 'up' ? item.order_id - 1 : item.order_id + 1;
23
+
24
+ onMove(order);
25
+ };
26
+
27
+ const onAskDelete = (): void => {
28
+ if (window.confirm(t('locations_manage.actions.delete', { ns: 'common' }))) {
29
+ onDelete();
30
+ }
31
+ };
32
+
33
+ return (
34
+ <ContainerActions>
35
+ <Group>
36
+ <Button
37
+ type="button"
38
+ loading={ loading }
39
+ subtype="secondary"
40
+ size="small"
41
+ text={ <FiArrowUpCircle /> }
42
+ noMargin
43
+ onClick={ () => onChange('up') }
44
+ />
45
+
46
+ <Button
47
+ type="button"
48
+ subtype="secondary"
49
+ loading={ loading }
50
+ size="small"
51
+ text={ <FiArrowDownCircle /> }
52
+ noMargin
53
+ onClick={ () => onChange('down') }
54
+ />
55
+ </Group>
56
+
57
+ <Group>
58
+ <Button
59
+ type="submit"
60
+ loading={ loading }
61
+ size="small"
62
+ text={
63
+ <>
64
+ <BiEditAlt />
65
+ { t('table.actions.update', { ns: 'common' }) }
66
+ </>
67
+ }
68
+ noMargin
69
+ />
70
+
71
+ <Button
72
+ type="button"
73
+ loading={ loading }
74
+ size="small"
75
+ subtype="delete"
76
+ text={
77
+ <>
78
+ <BiTrash />
79
+ { t('table.actions.delete', { ns: 'common' }) }
80
+ </>
81
+ }
82
+ noMargin
83
+ onClick={ onAskDelete }
84
+ />
85
+ </Group>
86
+ </ContainerActions>
87
+ );
88
+ };
89
+
90
+ export default Actions;
@@ -0,0 +1,45 @@
1
+ import { TFunction } from 'i18next';
2
+ import Item from './Item';
3
+ import { Items, NotFound } from './styles';
4
+ import { LocationData } from '@autobusal/providers/types/locations';
5
+
6
+ interface Props {
7
+ routeId: number
8
+ data?: LocationData[]
9
+ t: TFunction<'normal'>
10
+ onReload: () => void
11
+ }
12
+
13
+ const Browse = ({ routeId, data, t, onReload }: Props): JSX.Element => {
14
+ if (data === undefined || data.length === 0) {
15
+ return (
16
+ <NotFound className="box">
17
+ { t('locations_manage.no_locations', { ns: 'common' }) }
18
+ </NotFound>
19
+ );
20
+ }
21
+
22
+ const items = data.map(item => (
23
+ <Item
24
+ key={ item.id }
25
+ item={ item }
26
+ routeId={ routeId }
27
+ t={ t }
28
+ onReload={ onReload }
29
+ />
30
+ ));
31
+
32
+ return (
33
+ <>
34
+ <h2>
35
+ { t('locations_manage.browse', { ns: 'common' }) }
36
+ </h2>
37
+
38
+ <Items>
39
+ { items }
40
+ </Items>
41
+ </>
42
+ );
43
+ };
44
+
45
+ export default Browse;
@@ -0,0 +1,92 @@
1
+ import { TFunction } from 'i18next';
2
+ import { useForm } from 'react-hook-form';
3
+ import { Validate, Display, success } from '@autobusal/utilities';
4
+ import Actions from './Actions';
5
+ import { getTomorrow } from '../utilities';
6
+ import { Data } from './styles';
7
+ import { LocationForm } from '../types';
8
+ import { LocationData } from '@autobusal/providers/types/locations';
9
+ import { PostMove, DeleteLocation, PostUpdate } from '../services';
10
+
11
+ interface Props {
12
+ routeId: number
13
+ item: LocationData
14
+ t: TFunction<'normal'>
15
+ onReload: () => void
16
+ }
17
+
18
+ const Item = ({ routeId, item, t, onReload }: Props): JSX.Element => {
19
+ const { register, handleSubmit, formState: { errors } } = useForm<LocationForm>();
20
+
21
+ const { mutate: Save, isPending: isPendingUpdate } = PostUpdate(routeId, item.id);
22
+
23
+ const { mutate: Move, isPending: isPendingMove } = PostMove(routeId, item.id);
24
+
25
+ const { mutate: Delete, isPending: isPendingDelete } = DeleteLocation(routeId, item.id);
26
+
27
+ const onSubmit = (data: LocationForm): void => {
28
+ Save(data);
29
+ };
30
+
31
+ const onMove = (order: number): void => {
32
+ Move(order, {
33
+ onSuccess: () => {
34
+ success(t('locations_manage.messages.moved', { ns: 'common' }));
35
+
36
+ onReload();
37
+ }
38
+ });
39
+ };
40
+
41
+ const onDelete = (): void => {
42
+ Delete({}, {
43
+ onSuccess: () => {
44
+ success(t('locations_manage.messages.deleted', { ns: 'common' }));
45
+
46
+ onReload();
47
+ }
48
+ });
49
+ };
50
+
51
+ return (
52
+ <div className="box">
53
+ <h4>{ item.city.country?.name } &rsaquo; { item.city.name } &rsaquo; { item.stop?.name }</h4>
54
+
55
+ <form onSubmit={ handleSubmit(onSubmit) }>
56
+ <Data>
57
+ <div className="row">
58
+ { t('locations_manage.city.tomorrow', { ns: 'common' }) }
59
+
60
+ <select defaultValue={ item.tomorrow } { ...register('tomorrow') }>
61
+ { getTomorrow(t) }
62
+ </select>
63
+ </div>
64
+
65
+ <div className="row">
66
+ { t('locations_manage.city.departure', { ns: 'common' }) }
67
+
68
+ <input type="text" defaultValue={ item.departure } { ...register('departure', Validate('required', t)) } />
69
+ { Display(errors.departure) }
70
+ </div>
71
+
72
+ <div className="row">
73
+ { t('locations_manage.city.arrival', { ns: 'common' }) }
74
+
75
+ <input type="text" defaultValue={ item.arrival } { ...register('arrival', Validate('required', t)) } />
76
+ { Display(errors.arrival) }
77
+ </div>
78
+ </Data>
79
+
80
+ <Actions
81
+ item={ item }
82
+ loading={ isPendingMove || isPendingDelete || isPendingUpdate }
83
+ t={ t }
84
+ onMove={ onMove }
85
+ onDelete={ onDelete }
86
+ />
87
+ </form>
88
+ </div>
89
+ );
90
+ };
91
+
92
+ export default Item;
@@ -0,0 +1,36 @@
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 NotFound = styled.div`
10
+ margin-top: 15px;
11
+ padding: 30px 0;
12
+ color: ${ props => props.theme.font.neutral };
13
+ text-align: center;
14
+ font-size: ${ props => props.theme.size.xs };
15
+ `;
16
+
17
+ export const Data = styled.div`
18
+ display: flex;
19
+ gap: 10px;
20
+ flex-direction: column;
21
+
22
+ @media (min-width: 480px) {
23
+ flex-direction: row;
24
+ gap: 15px;
25
+ }
26
+ `;
27
+
28
+ export const ContainerActions = styled.div`
29
+ display: flex;
30
+ justify-content: space-between;
31
+ `;
32
+
33
+ export const Group = styled.div`
34
+ display: flex;
35
+ gap: 10px;
36
+ `;
@@ -0,0 +1,44 @@
1
+ import { TFunction } from 'i18next';
2
+ import { useParams } from 'react-router-dom';
3
+ import { Meta, BackWithTitle, Report } from '@autobusal/common';
4
+ import { usePage } from '@autobusal/hooks';
5
+ import Add from './Add/Add';
6
+ import Browse from './Browse/Browse';
7
+ import { GetLocations } from './services';
8
+
9
+ interface Props {
10
+ url: string
11
+ t: TFunction<'common'>
12
+ }
13
+
14
+ const Manage = ({ url, t }: Props): JSX.Element => {
15
+ const params = useParams();
16
+
17
+ const id = Number(params.id);
18
+
19
+ url = url.replace('[id]', String(id));
20
+
21
+ usePage('routes');
22
+
23
+ const { data, refetch } = GetLocations(id);
24
+
25
+ const onReload = (): void => {
26
+ refetch();
27
+ };
28
+
29
+ const title = t('locations_manage.title', { name: data?.name, ns: 'common' });
30
+
31
+ return (
32
+ <Meta title={ title }>
33
+ <BackWithTitle title={ title } to={ url } t={ t } />
34
+
35
+ <Add routeId={ id } t={ t } onReload={ onReload } />
36
+
37
+ <Report margin={ 15 } question={ t('locations_manage.report', { ns: 'common' }) } type="location" t={ t } />
38
+
39
+ <Browse routeId={ id } data={ data?.locations } t={ t } onReload={ onReload } />
40
+ </Meta>
41
+ );
42
+ };
43
+
44
+ export default Manage;
@@ -0,0 +1,105 @@
1
+ import { UseMutationResult, UseQueryResult, UseSuspenseQueryResult, useMutation, useQuery, useSuspenseQuery } from '@tanstack/react-query';
2
+ import { apiClient } from '@autobusal/providers';
3
+ import { RouteData } from '@autobusal/providers/types/routes';
4
+ import { CountryData } from '@autobusal/providers/types/locations';
5
+ import { AddForm, LocationForm } from './types';
6
+
7
+ export const GetLocations = (id: number): UseSuspenseQueryResult<RouteData> => (
8
+ useSuspenseQuery({
9
+ queryKey: ['operator-locations', { id }],
10
+ queryFn: async () => (
11
+ await apiClient
12
+ .get('/api/locations/operator/get', {
13
+ params: { id }
14
+ })
15
+ .then(response => (
16
+ response.data
17
+ ))
18
+ )
19
+ })
20
+ );
21
+
22
+ export const GetStops = (routeId: number): UseQueryResult<CountryData[]> => (
23
+ useQuery({
24
+ queryKey: ['operator-stops', { routeId }],
25
+ queryFn: async () => (
26
+ await apiClient
27
+ .get('/api/stops/operator/browse', {
28
+ params: {
29
+ route_id: routeId
30
+ }
31
+ })
32
+ .then(response => (
33
+ response.data
34
+ ))
35
+ )
36
+ })
37
+ );
38
+
39
+ export const AddLocation = (id: number): UseMutationResult<void, Error, AddForm, unknown> => (
40
+ useMutation({
41
+ mutationKey: ['operator-locations-add', { id }],
42
+ mutationFn: async (data: AddForm) => {
43
+ await apiClient
44
+ .post('/api/locations/operator/add', {
45
+ ...data,
46
+ id
47
+ })
48
+ .then(response => (
49
+ response.data
50
+ ))
51
+ }
52
+ })
53
+ );
54
+
55
+ export const PostMove = (id: number, location_id: number): UseMutationResult<void, Error, number, unknown> => (
56
+ useMutation({
57
+ mutationKey: ['operator-locations-move', { id, location_id }],
58
+ mutationFn: async (order: number) => (
59
+ await apiClient
60
+ .post('/api/locations/operator/move', {
61
+ order,
62
+ id,
63
+ location_id
64
+ })
65
+ .then(response => (
66
+ response.data
67
+ ))
68
+ )
69
+ })
70
+ );
71
+
72
+ export const DeleteLocation = (id: number, location_id: number): UseMutationResult<void> => (
73
+ useMutation({
74
+ mutationKey: ['operator-locations-delete', { id, location_id }],
75
+ mutationFn: async () => (
76
+ await apiClient
77
+ .delete('/api/locations/operator/delete', {
78
+ params: {
79
+ id,
80
+ location_id
81
+ }
82
+ })
83
+ .then(response => (
84
+ response.data
85
+ ))
86
+ )
87
+ })
88
+ );
89
+
90
+ export const PostUpdate = (id: number, location_id: number): UseMutationResult<void, Error, LocationForm, unknown> => (
91
+ useMutation({
92
+ mutationKey: ['operator-locations-update', { id, location_id }],
93
+ mutationFn: async (data: LocationForm) => (
94
+ await apiClient
95
+ .post('/api/locations/operator/update', {
96
+ ...data,
97
+ id,
98
+ location_id
99
+ })
100
+ .then(response => (
101
+ response.data
102
+ ))
103
+ )
104
+ })
105
+ );
@@ -0,0 +1,9 @@
1
+ export interface AddForm {
2
+ group_id: string
3
+ }
4
+
5
+ export interface LocationForm {
6
+ tomorrow: number
7
+ departure: string
8
+ arrival: string
9
+ }
@@ -0,0 +1,53 @@
1
+ import { TFunction } from 'i18next';
2
+ import { CityData, CountryData } from '@autobusal/providers/types/locations';
3
+
4
+ export const getStops = (t: TFunction<'normal'>, countries?: CountryData[]): JSX.Element[] => {
5
+ const options: JSX.Element[] = [
6
+ <option key="-none=" value="">{ t('locations_manage.add_city.choose', { ns: 'common' }) }</option>
7
+ ];
8
+
9
+ countries?.forEach(country => {
10
+ options.push(
11
+ <optgroup key={ country.id } label={ country.name }>
12
+ { getOptions(country.cities) }
13
+ </optgroup>
14
+ );
15
+ });
16
+
17
+ return options;
18
+ };
19
+
20
+ const getOptions = (cities?: CityData[]): JSX.Element[] => {
21
+ const options: JSX.Element[] = [];
22
+
23
+ cities?.forEach(city => {
24
+ city.stops?.forEach(stop => {
25
+ const value = `${ city.id }#${ stop.id }`;
26
+
27
+ options.push(
28
+ <option key={ value } value={ value }>{ city.name } &rsaquo; { stop.name }</option>
29
+ );
30
+ });
31
+ });
32
+
33
+ return options;
34
+ };
35
+
36
+ export const getTomorrow = (t: TFunction<'normal'>): JSX.Element[] => {
37
+ const available = [
38
+ { name: t('locations_manage.tomorrow.same_day', { ns: 'common' }), value: 0 },
39
+ { name: t('locations_manage.tomorrow.1_day', { ns: 'common' }), value: 1 },
40
+ { name: t('locations_manage.tomorrow.2_days', { ns: 'common' }), value: 2 },
41
+ { name: t('locations_manage.tomorrow.3_days', { ns: 'common' }), value: 3 },
42
+ { name: t('locations_manage.tomorrow.4_days', { ns: 'common' }), value: 4 },
43
+ { name: t('locations_manage.tomorrow.5_days', { ns: 'common' }), value: 5 },
44
+ { name: t('locations_manage.tomorrow.6_days', { ns: 'common' }), value: 6 },
45
+ { name: t('locations_manage.tomorrow.7_days', { ns: 'common' }), value: 7 }
46
+ ];
47
+
48
+ const options = available.map(item => (
49
+ <option key={ item.value } value={ item.value }>{ item.name }</option>
50
+ ));
51
+
52
+ return options;
53
+ };
@@ -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 '@pages/Operator/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
+ }
@@ -17,7 +17,7 @@ const Countries = ({ transiting, t, onDelete }: Props): JSX.Element => {
17
17
  }
18
18
 
19
19
  const onDeleteConfirm = (id: number): void => {
20
- if (window.confirm(t('private.transiting_manage.confirm'))) {
20
+ if (window.confirm(t('transiting_manage.confirm', { ns: 'confirm' }))) {
21
21
  onDelete(id);
22
22
  }
23
23
  };
package/index.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import Browse from './Browse';
2
2
  import Manage from './Manage';
3
+ import LocationsManage from './Locations/Manage';
4
+ import PricesManage from './Prices/Manage';
3
5
  import SchedulesManage from './Schedule/Manage';
4
6
  import TransitingManage from './Transiting/Manage';
5
7
  import UnavailableBrowse from './Unavailable/Browse';
@@ -8,6 +10,8 @@ import UnavailableManage from './Unavailable/Manage';
8
10
  export {
9
11
  Browse,
10
12
  Manage,
13
+ LocationsManage,
14
+ PricesManage,
11
15
  SchedulesManage,
12
16
  TransitingManage,
13
17
  UnavailableBrowse,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/operator-routes",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "type": "module",
5
5
  "main": "index.ts"
6
6
  }