diff options
author | Birte Kristina Friesel <derf@finalrewind.org> | 2025-06-21 20:48:53 +0200 |
---|---|---|
committer | Birte Kristina Friesel <derf@finalrewind.org> | 2025-06-21 20:48:53 +0200 |
commit | da87e97a0b954860e0a53b8a0f475580d44ad73d (patch) | |
tree | 3626df8b8792df43fcb0edf266059f70fbb92573 | |
parent | ca2226b9238ac2159d0001a071edf33915f062ae (diff) |
Add bare-bones support for manual checkins (still bound to a specific backend)
Extension opportunities (maybe, eventually)
* provide datetimes of intermediate stops
* provide an API for real-time data updates
* look up stops that travelynx does not yet know about rather than rejecting
them outright
-rwxr-xr-x | lib/Travelynx.pm | 5 | ||||
-rw-r--r-- | lib/Travelynx/Command/work.pm | 17 | ||||
-rwxr-xr-x | lib/Travelynx/Controller/Traveling.pm | 212 | ||||
-rw-r--r-- | lib/Travelynx/Model/InTransit.pm | 38 | ||||
-rwxr-xr-x | lib/Travelynx/Model/Journeys.pm | 8 | ||||
-rw-r--r-- | templates/_checked_in.html.ep | 82 | ||||
-rw-r--r-- | templates/add_intransit.html.ep | 87 | ||||
-rw-r--r-- | templates/departures.html.ep | 8 |
8 files changed, 421 insertions, 36 deletions
diff --git a/lib/Travelynx.pm b/lib/Travelynx.pm index a06bcea..c8c96b8 100755 --- a/lib/Travelynx.pm +++ b/lib/Travelynx.pm @@ -1316,7 +1316,8 @@ sub startup { if ( $user->{is_dbris} or $user->{is_efa} or $user->{is_hafas} - or $user->{is_motis} ) + or $user->{is_motis} + or $train_id eq 'manual' ) { return $self->_checkout_journey_p(%opt); } @@ -3004,6 +3005,7 @@ sub startup { $authed_r->get('/ajax/status_card.html')->to('traveling#status_card'); $authed_r->get( '/cancelled' => [ format => [ 'html', 'json' ] ] ) ->to( 'traveling#cancelled', format => undef ); + $authed_r->get('/checkin/add')->to('traveling#add_intransit_form'); $authed_r->get('/fgr')->to('passengerrights#list_candidates'); $authed_r->get('/account/password')->to('account#password_form'); $authed_r->get('/account/mail')->to('account#change_mail'); @@ -3031,6 +3033,7 @@ sub startup { $authed_r->post('/account/traewelling')->to('traewelling#settings'); $authed_r->post('/account/insight')->to('account#insight'); $authed_r->post('/account/select_backend')->to('account#change_backend'); + $authed_r->post('/checkin/add')->to('traveling#add_intransit_form'); $authed_r->post('/journey/add')->to('traveling#add_journey_form'); $authed_r->post('/journey/comment')->to('traveling#comment_form'); $authed_r->post('/journey/visibility')->to('traveling#visibility_form'); diff --git a/lib/Travelynx/Command/work.pm b/lib/Travelynx/Command/work.pm index 60417b1..1bc4f53 100644 --- a/lib/Travelynx/Command/work.pm +++ b/lib/Travelynx/Command/work.pm @@ -53,6 +53,23 @@ sub run { my $arr = $entry->{arr_eva}; my $train_id = $entry->{train_id}; + if ( $train_id eq 'manual' ) { + if ( $arr + and $entry->{real_arr_ts} + and $now->epoch - $entry->{real_arr_ts} > 600 ) + { + $self->app->checkout_p( + station => $arr, + force => 2, + dep_eva => $dep, + arr_eva => $arr, + uid => $uid + )->wait; + } + + next; + } + if ( $entry->{is_dbris} ) { eval { diff --git a/lib/Travelynx/Controller/Traveling.pm b/lib/Travelynx/Controller/Traveling.pm index 996b4a5..31c7587 100755 --- a/lib/Travelynx/Controller/Traveling.pm +++ b/lib/Travelynx/Controller/Traveling.pm @@ -2612,4 +2612,216 @@ sub add_journey_form { } } +sub add_intransit_form { + my ($self) = @_; + + $self->stash( backend_id => $self->current_user->{backend_id} ); + + if ( $self->param('action') and $self->param('action') eq 'save' ) { + my $parser = DateTime::Format::Strptime->new( + pattern => '%d.%m.%Y %H:%M', + locale => 'de_DE', + time_zone => 'Europe/Berlin' + ); + my %opt; + my %trip; + + my @parts = split( qr{\s+}, $self->param('train') ); + + if ( @parts == 2 ) { + @trip{ 'train_type', 'train_no' } = @parts; + } + elsif ( @parts == 3 ) { + @trip{ 'train_type', 'train_line', 'train_no' } = @parts; + } + else { + $self->render( + 'add_intransit', + with_autocomplete => 1, + status => 400, + error => +'Fahrt muss als „Typ Nummer“ oder „Typ Linie Nummer“ eingegeben werden.' + ); + return; + } + + for my $key (qw(sched_departure sched_arrival)) { + if ( $self->param($key) ) { + my $datetime = $parser->parse_datetime( $self->param($key) ); + if ( not $datetime ) { + $self->render( + 'add_intransit', + with_autocomplete => 1, + status => 400, + error => "${key}: Ungültiges Datums-/Zeitformat" + ); + return; + } + $trip{$key} = $datetime; + } + } + + for my $key (qw(dep_station arr_station route comment)) { + $trip{$key} = $self->param($key); + } + + $opt{backend_id} = $self->current_user->{backend_id}; + + my $dep_stop = $self->stations->search( $trip{dep_station}, + backend_id => $opt{backend_id} ); + my $arr_stop = $self->stations->search( $trip{arr_station}, + backend_id => $opt{backend_id} ); + + if ( defined $trip{route} ) { + $trip{route} = [ split( qr{\r?\n\r?}, $trip{route} ) ]; + } + + my $route_has_start = 0; + my $route_has_stop = 0; + + for my $station ( @{ $trip{route} || [] } ) { + if ( $station eq $dep_stop->{name} + or $station eq $dep_stop->{eva} ) + { + $route_has_start = 1; + } + if ( $station eq $arr_stop->{name} + or $station eq $arr_stop->{eva} ) + { + $route_has_stop = 1; + } + } + + my @route; + + if ( not $route_has_start ) { + push( + @route, + [ + $dep_stop->{name}, + $dep_stop->{eva}, + { + lat => $dep_stop->{lat}, + lon => $dep_stop->{lon}, + } + ] + ); + } + + if ( $trip{route} ) { + my @unknown_stations; + for my $station ( @{ $trip{route} } ) { + my $station_info = $self->stations->search( $station, + backend_id => $opt{backend_id} ); + if ($station_info) { + push( + @route, + [ + $station_info->{name}, + $station_info->{eva}, + { + lat => $station_info->{lat}, + lon => $station_info->{lon}, + } + ] + ); + } + else { + push( @route, [ $station, undef, {} ] ); + push( @unknown_stations, $station ); + } + } + + if ( @unknown_stations == 1 ) { + $self->render( + 'add_intransit', + with_autocomplete => 1, + status => 400, + error => "Unbekannter Unterwegshalt: $unknown_stations[0]" + ); + return; + } + elsif (@unknown_stations) { + $self->render( + 'add_intransit', + with_autocomplete => 1, + status => 400, + error => 'Unbekannte Unterwegshalte: ' + . join( ', ', @unknown_stations ) + ); + return; + } + } + + if ( not $route_has_stop ) { + push( + @route, + [ + $arr_stop->{name}, + $arr_stop->{eva}, + { + lat => $arr_stop->{lat}, + lon => $arr_stop->{lon}, + } + ] + ); + } + + for my $station (@route) { + if ( $station->[0] eq $dep_stop->{name} + or $station->[1] eq $dep_stop->{eva} ) + { + $station->[2]{sched_dep} = $trip{sched_departure}->epoch; + } + if ( $station->[0] eq $arr_stop->{name} + or $station->[1] eq $arr_stop->{eva} ) + { + $station->[2]{sched_arr} = $trip{sched_arrival}->epoch; + } + } + + my $error; + my $db = $self->pg->db; + my $tx = $db->begin; + + $trip{dep_id} = $dep_stop->{eva}; + $trip{arr_id} = $arr_stop->{eva}; + $trip{route} = \@route; + + $opt{db} = $db; + $opt{manual} = \%trip; + $opt{uid} = $self->current_user->{id}; + + if ( not defined $trip{dep_id} ) { + $error = "Unknown departure stop '$trip{dep_station}'"; + } + elsif ( not defined $trip{arr_id} ) { + $error = "Unknown arrival stop '$trip{arr_station}'"; + } + else { + $error = $self->in_transit->add(%opt); + } + + if ($error) { + $self->render( + 'add_intransit', + with_autocomplete => 1, + status => 400, + error => $error, + ); + } + else { + $tx->commit; + $self->redirect_to('/'); + } + } + else { + $self->render( + 'add_intransit', + with_autocomplete => 1, + error => undef + ); + } +} + 1; diff --git a/lib/Travelynx/Model/InTransit.pm b/lib/Travelynx/Model/InTransit.pm index b67b716..cb8f7c4 100644 --- a/lib/Travelynx/Model/InTransit.pm +++ b/lib/Travelynx/Model/InTransit.pm @@ -101,6 +101,7 @@ sub add { my $journey = $opt{journey}; my $stop = $opt{stop}; my $stopover = $opt{stopover}; + my $manual = $opt{manual}; my $checkin_station_id = $opt{departure_eva}; my $route = $opt{route}; my $data = $opt{data}; @@ -389,16 +390,49 @@ sub add { sched_departure => $stopover->scheduled_departure, real_departure => $stopover->departure, route => $json->encode( \@route ), - data => JSON->new->encode( + data => $json->encode( { rt => $stopover->{is_realtime} ? 1 : 0, %{ $data // {} } } ), - user_data => JSON->new->encode($persistent_data), + user_data => $json->encode($persistent_data), + backend_id => $backend_id, + } + ); + } + elsif ($manual) { + if ( $manual->{comment} ) { + $persistent_data->{comment} = $manual->{comment}; + } + $db->insert( + 'in_transit', + { + user_id => $uid, + cancelled => 0, + checkin_station_id => $manual->{dep_id}, + checkout_station_id => $manual->{arr_id}, + checkin_time => DateTime->now( time_zone => 'Europe/Berlin' ), + train_type => $manual->{train_type}, + train_no => $manual->{train_no} || q{}, + train_id => 'manual', + train_line => $manual->{train_line} || undef, + sched_departure => $manual->{sched_departure}, + real_departure => $manual->{sched_departure}, + sched_arrival => $manual->{sched_arrival}, + real_arrival => $manual->{sched_arrival}, + route => $json->encode( $manual->{route} // [] ), + data => $json->encode( + { + manual => \1, + %{ $data // {} } + } + ), + user_data => $json->encode($persistent_data), backend_id => $backend_id, } ); + return; } else { die('invalid arguments / argument types passed to InTransit->add'); diff --git a/lib/Travelynx/Model/Journeys.pm b/lib/Travelynx/Model/Journeys.pm index 0fb663e..876786a 100755 --- a/lib/Travelynx/Model/Journeys.pm +++ b/lib/Travelynx/Model/Journeys.pm @@ -283,8 +283,14 @@ sub add_from_in_transit { my $db = $opt{db}; my $journey = $opt{journey}; + if ( $journey->{train_id} eq 'manual' ) { + $journey->{edited} = 0x3fff; + } + else { + $journey->{edited} = 0; + } + delete $journey->{data}; - $journey->{edited} = 0; $journey->{checkout_time} = DateTime->now( time_zone => 'Europe/Berlin' ); return $db->insert( 'journeys', $journey, { returning => 'id' } ) diff --git a/templates/_checked_in.html.ep b/templates/_checked_in.html.ep index 988bf39..91f1ce7 100644 --- a/templates/_checked_in.html.ep +++ b/templates/_checked_in.html.ep @@ -202,7 +202,10 @@ % if (@{$journey->{messages} // []} or @{$journey->{extra_data}{qos_msg} // []} or not $journey->{extra_data}{rt}) { <p style="margin-bottom: 2ex;"> <ul> - % if (not $journey->{extra_data}{rt}) { + % if ($journey->{extra_data}{manual}) { + <li><i class="material-icons tiny">gps_off</i> Manueller Checkin ohne Echtzeitdaten + % } + % elsif (not $journey->{extra_data}{rt}) { <li><i class="material-icons tiny">gps_off</i> Keine Echtzeitdaten vorhanden % } % for my $message (reverse @{$journey->{messages} // []}) { @@ -362,41 +365,56 @@ %= include '_map', with_map_header => 0, station_coordinates => stash('station_coordinates'), polyline_groups => stash('polyline_groups') </div> </div> - <div class="card" style="margin-top: 3em;"> - <div class="card-content"> - <i class="material-icons small right sync-failed-marker grey-text" style="display: none;">sync_problem</i> - <span class="card-title">Ziel ändern?</span> - <div class="targetlist"> - % for my $station (@{$journey->{route_after}}) { - % my $is_dest = ($journey->{arr_name} and $station->[0] eq $journey->{arr_name}); - <a class="action-checkout tablerow" style="<%= $is_dest? 'font-weight: bold;' : '' %>" data-station="<%= $station->[1] // $station->[0] %>"> - <span><%= $station->[0] %></span> - <span> - %= include '_show_load_icons', station => $station - % if ($station->[2]{isCancelled}) { - entfällt - % } - % elsif ($station->[2]{rt_arr} or $station->[2]{sched_arr}) { - %= ($station->[2]{rt_arr} || $station->[2]{sched_arr})->strftime('%H:%M') - % } - % elsif ($station->[2]{rt_dep} or $station->[2]{sched_dep}) { - (<%= ($station->[2]{rt_dep} || $station->[2]{sched_dep})->strftime('%H:%M') %>) - % } - % elsif ($station->[2]{isAdditional}) { - Zusatzhalt - % } - </span> + % if ($journey->{extra_data}{manual}) { + <div class="card" style="margin-top: 3em;"> + <div class="card-content"> + <i class="material-icons small right sync-failed-marker grey-text" style="display: none;">sync_problem</i> + <span class="card-title">Manueller Checkin</span> + </div> + <div class="card-action"> + <a class="action-undo blue-text" data-id="in_transit" data-checkints="<%= $journey->{timestamp}->epoch %>" style="margin-right: 0;"> + <i class="material-icons left" aria-hidden="true">undo</i> Checkin Rückgängig </a> - <a class="nonflex" href="<%= resolve_sb_template($user->{sb_template}, name => $station->[0], eva => $station->[1], tt => $journey->{train_type} // q{x}, tn => $journey->{train_no}, id => $journey->{train_id} =~ s{[ #|]}{x}gr, dbris => $journey->{is_dbris} ? $journey->{backend_name} : q{}, efa => $journey->{is_efa} ? $journey->{backend_name} : q{}, hafas => $journey->{is_hafas} ? $journey->{backend_name} : q{}, is_iris => $journey->{is_iris}, motis => $journey->{is_motis} ? $journey->{backend_name} : q{}) %>"><i class="material-icons tiny"><%= $journey->{is_hafas} ? 'directions' : 'train' %></i></a> - % } </div> </div> - <div class="card-action"> - <a class="action-undo blue-text" data-id="in_transit" data-checkints="<%= $journey->{timestamp}->epoch %>" style="margin-right: 0;"> - <i class="material-icons left" aria-hidden="true">undo</i> Checkin Rückgängig - </a> + % } + % else { + <div class="card" style="margin-top: 3em;"> + <div class="card-content"> + <i class="material-icons small right sync-failed-marker grey-text" style="display: none;">sync_problem</i> + <span class="card-title">Ziel ändern?</span> + <div class="targetlist"> + % for my $station (@{$journey->{route_after}}) { + % my $is_dest = ($journey->{arr_name} and $station->[0] eq $journey->{arr_name}); + <a class="action-checkout tablerow" style="<%= $is_dest? 'font-weight: bold;' : '' %>" data-station="<%= $station->[1] // $station->[0] %>"> + <span><%= $station->[0] %></span> + <span> + %= include '_show_load_icons', station => $station + % if ($station->[2]{isCancelled}) { + entfällt + % } + % elsif ($station->[2]{rt_arr} or $station->[2]{sched_arr}) { + %= ($station->[2]{rt_arr} || $station->[2]{sched_arr})->strftime('%H:%M') + % } + % elsif ($station->[2]{rt_dep} or $station->[2]{sched_dep}) { + (<%= ($station->[2]{rt_dep} || $station->[2]{sched_dep})->strftime('%H:%M') %>) + % } + % elsif ($station->[2]{isAdditional}) { + Zusatzhalt + % } + </span> + </a> + <a class="nonflex" href="<%= resolve_sb_template($user->{sb_template}, name => $station->[0], eva => $station->[1], tt => $journey->{train_type} // q{x}, tn => $journey->{train_no}, id => $journey->{train_id} =~ s{[ #|]}{x}gr, dbris => $journey->{is_dbris} ? $journey->{backend_name} : q{}, efa => $journey->{is_efa} ? $journey->{backend_name} : q{}, hafas => $journey->{is_hafas} ? $journey->{backend_name} : q{}, is_iris => $journey->{is_iris}, motis => $journey->{is_motis} ? $journey->{backend_name} : q{}) %>"><i class="material-icons tiny"><%= $journey->{is_hafas} ? 'directions' : 'train' %></i></a> + % } + </div> + </div> + <div class="card-action"> + <a class="action-undo blue-text" data-id="in_transit" data-checkints="<%= $journey->{timestamp}->epoch %>" style="margin-right: 0;"> + <i class="material-icons left" aria-hidden="true">undo</i> Checkin Rückgängig + </a> + </div> </div> - </div> + % } <p> Falls das Backend ausgefallen ist oder die Fahrt aus anderen Gründen verloren ging: diff --git a/templates/add_intransit.html.ep b/templates/add_intransit.html.ep new file mode 100644 index 0000000..1f3d80d --- /dev/null +++ b/templates/add_intransit.html.ep @@ -0,0 +1,87 @@ +<h1>Manuell einchecken</h1> +% if ($error) { + <div class="row"> + <div class="col s12"> + <div class="card caution-color"> + <div class="card-content white-text"> + <span class="card-title">Ungültige Eingabe</span> + <p><%= $error %></p> + </div> + </div> + </div> + </div> +% } +<div class="row"> + <div class="col s12"> + <ul> + <li>Eingabe der Fahrt als „Typ Linie Nummer“ oder „Typ Nummer“, z.B. + „ICE 100“, „S 1 31133“ oder „ABR RE11 26720“</li> + <li>Wenn Nummer nicht bekannt oder vorhanden: einen beliebigen Integer eintragen, z.B. „S 5X 0“ oder „U 11 0“</li> + <li>Zeitangaben im Format DD.MM.YYYY HH:MM</li> + <li>Das ausgewählte Backend bestimmt die verfügbaren Halte für Start, Ziel und Route. Siehe auch <a href="/static/stops.csv">stops.csv</a></li> + </ul> + </div> +</div> +<div class="row"> + <div class="col s12 center-align"> + % if (current_user->{backend_id}) { + <a href="/account/select_backend?redirect_to=/checkin/add" class="btn-small btn-flat"><i class="material-icons left" aria-hidden="true">directions</i><%= current_user->{backend_name} %></a> + % } + % else { + <a href="/account/select_backend?redirect_to=/checkin/add" class="btn-small btn-flat"><i class="material-icons left" aria-hidden="true">train</i>IRIS</a> + % } + </div> +</div> +%= form_for '/checkin/add' => (method => 'POST') => begin + %= csrf_field + <div class="row"> + <div class="input-field col s12"> + %= text_field 'train', id => 'train', class => 'validate', required => undef, pattern => '[0-9a-zA-Z]+ +[0-9a-zA-Z]* *[0-9]+' + <label for="train">Fahrt (Typ Linie Nummer)</label> + </div> + </div> + <div class="row"> + <div class="input-field col s12"> + %= text_field 'dep_station', id => 'dep_station', class => 'autocomplete validate', autocomplete => 'off', required => undef + <label for="dep_station">Start (Name oder ID)</label> + </div> + <div class="input-field col s12"> + %= text_field 'sched_departure', id => 'sched_departure', class => 'validate', required => undef, pattern => '[0-9][0-9]?[.][0-9][0-9]?[.][0-9][0-9][0-9][0-9] +[0-9][0-9]:[0-9][0-9]' + <label for="sched_departure">Geplante Abfahrt</label> + </div> + </div> + <div class="row"> + <div class="input-field col s12"> + %= text_field 'arr_station', id => 'arr_station', class => 'autocomplete validate', autocomplete => 'off', required => undef + <label for="arr_station">Ziel (Name oder ID)</label> + </div> + <div class="input-field col s12"> + %= text_field 'sched_arrival', id => 'sched_arrival', class => 'validate', required => undef, pattern => '[0-9][0-9]?[.][0-9][0-9]?[.][0-9][0-9][0-9][0-9] +[0-9][0-9]:[0-9][0-9]' + <label for="sched_arrival">Geplante Ankunft</label> + </div> + </div> + <div class="row"> + <div class="input-field col s12"> + %= text_area 'route', id => 'route', class => 'materialize-textarea' + <label for="route">Halte (optional, eine Station pro Zeile, wahlweise Unterwegshalte oder komplette Route)</label> + </div> + </div> + <div class="row"> + <div class="input-field col s12"> + %= text_field 'comment' + <label for="comment">Kommentar</label> + </div> + </div> + <div class="row"> + <div class="col s3 m3 l3"> + </div> + <div class="col s6 m6 l6 center-align"> + <button class="btn waves-effect waves-light" type="submit" name="action" value="save"> + Hinzufügen + <i class="material-icons right">send</i> + </button> + </div> + <div class="col s3 m3 l3"> + </div> + </div> +%= end diff --git a/templates/departures.html.ep b/templates/departures.html.ep index 16b5f3c..6df48a8 100644 --- a/templates/departures.html.ep +++ b/templates/departures.html.ep @@ -190,3 +190,11 @@ % } </div> </div> + +% if (not $user_status->{checked_in}) { + <div class="row"> + <div class="col s12 center-align"> + <a class="btn-small" href="<%= url_for('checkinadd')->query({dbris => $dbris, efa => $efa, hafas => $hafas, motis => $motis, dep_station => $station}) %>"><i class="material-icons left" aria-hidden="true">add</i><span>manuell einchecken</span></a> + </div> + </div> +% } |