Plugin Directory

Changeset 3790901


Ignore:
Timestamp:
11/08/2026 11:23:25 AM (less than one hour ago)
Author:
rentbaik
Message:

Release 1.0.1

Location:
rentbaik
Files:
58 added
22 edited
1 copied

Legend:

Unmodified
Added
Removed
  • rentbaik/tags/1.0.1/assets/admin/admin.css

    r3790685 r3790901  
    158158}
    159159
     160/* Customer pages card */
     161.rentbaik-sitepages__list {
     162        list-style: none;
     163        margin: 8px 0 0;
     164        padding: 0;
     165}
     166
     167.rentbaik-sitepages__item {
     168        display: grid;
     169        grid-template-columns: minmax(0, 1fr) auto;
     170        align-items: center;
     171        gap: 10px;
     172        margin: 0;
     173        padding-block: 10px;
     174        border-block-start: 1px solid var(--rentbaik-line);
     175}
     176
     177.rentbaik-sitepages__item:first-child {
     178        border-block-start: 0;
     179}
     180
     181.rentbaik-sitepages__text span:not(.rentbaik-pill) {
     182        display: block;
     183        color: var(--rentbaik-muted);
     184        overflow-wrap: anywhere;
     185}
     186
     187.rentbaik-sitepages__text code {
     188        display: inline-block;
     189        margin-block-start: 4px;
     190        font-size: 12px;
     191        overflow-wrap: anywhere;
     192}
     193
     194.rentbaik-sitepages__actions {
     195        display: flex;
     196        align-items: center;
     197        gap: 10px;
     198        white-space: nowrap;
     199}
     200
     201.rentbaik-sitepages__try {
     202        display: flex;
     203        align-items: center;
     204        gap: 6px;
     205        margin: 10px 0 0;
     206        padding-block-start: 10px;
     207        border-block-start: 1px solid var(--rentbaik-line);
     208}
     209
     210@media (max-width: 600px) {
     211        .rentbaik-sitepages__item {
     212                grid-template-columns: minmax(0, 1fr);
     213        }
     214}
     215
    160216/* Status list */
    161217.rentbaik-status {
    … …  
    11851241.rentbaik-cal__legend .rentbaik-cal__unpaid {
    11861242        margin: 0;
     1243}
     1244
     1245/* Short labels on a booking bar: late, today, not fully paid, deposit. */
     1246.rentbaik-cal__text {
     1247        flex: 1 1 auto;
     1248}
     1249
     1250.rentbaik-cal__tags {
     1251        flex: none;
     1252        display: inline-flex;
     1253        gap: 3px;
     1254        margin-inline-start: auto;
     1255}
     1256
     1257.rentbaik-cal__tag {
     1258        display: inline-block;
     1259        padding-inline: 6px;
     1260        border-radius: 8px;
     1261        font-size: 10px;
     1262        font-weight: 600;
     1263        line-height: 16px;
     1264        white-space: nowrap;
     1265}
     1266
     1267.rentbaik-cal__tag--alert {
     1268        background: #b32d2e;
     1269        color: #fff;
     1270}
     1271
     1272.rentbaik-cal__tag--warn {
     1273        background: #f5e6ab;
     1274        color: #614a00;
     1275}
     1276
     1277.rentbaik-cal__tag--info {
     1278        background: #135e96;
     1279        color: #fff;
     1280}
     1281
     1282.rentbaik-cal__tag--muted {
     1283        background: rgba(0, 0, 0, 0.07);
     1284        color: #3c434a;
     1285}
     1286
     1287.rentbaik-cal__legend .rentbaik-cal__tag {
     1288        margin-inline-end: 2px;
     1289}
     1290
     1291.rentbaik-agenda__tags .rentbaik-cal__tag {
     1292        vertical-align: middle;
    11871293}
    11881294
  • rentbaik/tags/1.0.1/assets/admin/admin.js

    r3790685 r3790901  
    148148                                } );
    149149                };
     150                // rest_url() already holds "?rest_route=" when permalinks are plain, so add parameters with "&" then.
     151                var sep = function ( url ) {
     152                        return url.indexOf( '?' ) === -1 ? '?' : '&';
     153                };
    150154                var open = function ( id, from, day ) {
    151155                        var back = window.location.href.split( '#' )[ 0 ];
    152156                        mark( id, '' );
    153                         load( 'booking-' + id, cfg.panelUrl + id + '?back=' + encodeURIComponent( back ), from, 'panelError', day ? function () {
     157                        load( 'booking-' + id, cfg.panelUrl + id + sep( cfg.panelUrl ) + 'back=' + encodeURIComponent( back ), from, 'panelError', day ? function () {
    154158                                // Opened from the day agenda: offer a way back to it.
    155159                                var button = document.createElement( 'button' );
    … …  
    167171                        var vehicle = new URLSearchParams( window.location.search ).get( 'vehicle' ) || '0';
    168172                        mark( 0, day );
    169                         load( 'day-' + day, cfg.dayUrl + day + '?vehicle=' + encodeURIComponent( vehicle ), from, 'dayError' );
     173                        load( 'day-' + day, cfg.dayUrl + day + sep( cfg.dayUrl ) + 'vehicle=' + encodeURIComponent( vehicle ), from, 'dayError' );
    170174                };
    171175
  • rentbaik/tags/1.0.1/includes/Admin/CalendarView.php

    r3790685 r3790901  
    145145                $photos = Photos::for_bookings( array_map( 'intval', wp_list_pluck( $rows, 'id' ) ) );
    146146                $now    = Time::to_mysql( Time::now() );
     147                $today  = Time::local( Time::now() )->format( 'Y-m-d' );
    147148                $labels = BookingService::status_labels();
    148149                $pay    = BookingService::payment_state_labels();
    … …  
    180181                        $plate         = $unit ? (string) $unit['plate_number'] : '';
    181182                        $state         = BookingService::payment_state( $row );
     183                        $late_hours    = 'overdue' === $tone ? max( 1, (int) floor( ( time() - $return->getTimestamp() ) / HOUR_IN_SECONDS ) ) : 0;
     184                        $flags         = self::flags( $row, $status, $state, $start_day, Time::local( $return )->format( 'Y-m-d' ), $today, $late_hours );
    182185
    183186                        $title = implode(
    … …  
    192195                                                sprintf( __( 'Return: %s', 'rentbaik' ), Format::datetime( (string) $row['return_at'] ) ),
    193196                                                ( $labels[ $status ] ?? $status ) . ' · ' . ( $pay[ $state ] ?? $state ),
     197                                                implode( ' · ', wp_list_pluck( $flags, 'title' ) ),
    194198                                                $customer ? (string) $customer['phone'] : '',
    195199                                        )
    … …  
    214218                                'vehicle'    => $vehicle_label,
    215219                                'plate'      => $plate,
    216                                 'unpaid'     => 'paid' !== $state && ! in_array( $status, array( 'completed' ), true ),
     220                                'unpaid'     => isset( $flags['unpaid'] ),
     221                                'flags'      => $flags,
    217222                                'photo'      => isset( $photos[ (int) $row['id'] ] ) ? Photos::url( $photos[ (int) $row['id'] ], true ) : '',
    218223                                'title'      => $title,
    … …  
    225230                }
    226231                return $list;
     232        }
     233
     234        /**
     235         * Short labels on a booking bar: late, return or pick-up today, not fully paid, deposit not given back.
     236         *
     237         * @param array<string, mixed> $row        Booking row.
     238         * @param string               $status     Booking status.
     239         * @param string               $state      Payment state (unpaid, partial, paid).
     240         * @param string               $start_day  Local pick-up day (Y-m-d).
     241         * @param string               $return_day Local return day (Y-m-d).
     242         * @param string               $today      Local today (Y-m-d).
     243         * @param int                  $late_hours Hours past the return time (0 = not late).
     244         * @return array<string, array{label: string, title: string, tone: string, at: string}> Key => label, full text,
     245         *                                                                                      tone (alert, warn, info, muted)
     246         *                                                                                      and end of the bar (start, end).
     247         */
     248        public static function flags( array $row, string $status, string $state, string $start_day, string $return_day, string $today, int $late_hours ): array {
     249                $flags = array();
     250                if ( $late_hours > 0 ) {
     251                        $flags['late'] = array(
     252                                'label' => __( 'Late', 'rentbaik' ),
     253                                /* translators: %d: number of hours. */
     254                                'title' => sprintf( _n( 'Late by %d hour', 'Late by %d hours', $late_hours, 'rentbaik' ), $late_hours ),
     255                                'tone'  => 'alert',
     256                                'at'    => 'end',
     257                        );
     258                } elseif ( 'active' === $status && $return_day === $today ) {
     259                        $flags['return_today'] = array(
     260                                'label' => '↓ ' . __( 'Today', 'rentbaik' ),
     261                                'title' => __( 'Comes back today', 'rentbaik' ),
     262                                'tone'  => 'info',
     263                                'at'    => 'end',
     264                        );
     265                }
     266                if ( in_array( $status, array( 'pending', 'confirmed' ), true ) && $start_day === $today ) {
     267                        $flags['pickup_today'] = array(
     268                                'label' => '↑ ' . __( 'Today', 'rentbaik' ),
     269                                'title' => __( 'Picked up today', 'rentbaik' ),
     270                                'tone'  => 'info',
     271                                'at'    => 'start',
     272                        );
     273                }
     274                if ( (int) $row['total'] > 0 && 'paid' !== $state && ! in_array( $status, array( 'completed', 'cancelled', 'no_show' ), true ) ) {
     275                        $flags['unpaid'] = array(
     276                                'label' => 'partial' === $state ? __( 'Part paid', 'rentbaik' ) : __( 'Unpaid', 'rentbaik' ),
     277                                'title' => __( 'Not fully paid', 'rentbaik' ),
     278                                // Money to collect now is stronger than a future booking that is paid at pick-up.
     279                                'tone'  => in_array( $status, array( 'active', 'returned' ), true ) ? 'warn' : 'muted',
     280                                'at'    => 'end',
     281                        );
     282                }
     283                if ( in_array( $status, array( 'returned', 'completed' ), true ) && 'held' === (string) $row['deposit_status'] ) {
     284                        $flags['deposit'] = array(
     285                                'label' => __( 'Deposit', 'rentbaik' ),
     286                                'title' => __( 'Deposit not given back yet', 'rentbaik' ),
     287                                'tone'  => 'warn',
     288                                'at'    => 'end',
     289                        );
     290                }
     291                return $flags;
    227292        }
    228293
    … …  
    474539                        echo '<span class="rentbaik-cal__nounit">' . esc_html__( 'No unit', 'rentbaik' ) . '</span>';
    475540                }
    476                 if ( $segment['unpaid'] ) {
    477                         echo '<i class="rentbaik-cal__unpaid" title="' . esc_attr__( 'Not fully paid', 'rentbaik' ) . '"></i>';
     541                $tags = array();
     542                foreach ( (array) ( $segment['flags'] ?? array() ) as $key => $flag ) {
     543                        // Pick-up labels sit on the first piece of a bar that crosses weeks, the others on the last piece.
     544                        if ( ( 'start' === $flag['at'] && ! $segment['cont_in'] ) || ( 'end' === $flag['at'] && ! $segment['cont_out'] ) ) {
     545                                $tags[ $key ] = $flag;
     546                        }
     547                }
     548                if ( ! empty( $tags ) ) {
     549                        echo '<span class="rentbaik-cal__tags">';
     550                        foreach ( $tags as $key => $flag ) {
     551                                echo '<span class="rentbaik-cal__tag rentbaik-cal__tag--' . esc_attr( $flag['tone'] ) . ' rentbaik-cal__tag--' . esc_attr( $key ) . '" title="' . esc_attr( $flag['title'] ) . '">' . esc_html( $flag['label'] ) . '</span>';
     552                        }
     553                        echo '</span>';
    478554                }
    479555                if ( $grip_b ) {
    … …  
    708784                        echo '<li><i class="rentbaik-cal__swatch rentbaik-cal__swatch--' . esc_attr( $key ) . '" aria-hidden="true"></i>' . esc_html( $label ) . '</li>';
    709785                }
    710                 echo '<li><i class="rentbaik-cal__unpaid" aria-hidden="true"></i>' . esc_html__( 'Not fully paid', 'rentbaik' ) . '</li>';
     786                foreach ( array(
     787                        array( 'alert', __( 'Late', 'rentbaik' ), __( 'Late return', 'rentbaik' ) ),
     788                        array( 'info', '↑↓ ' . __( 'Today', 'rentbaik' ), __( 'Picked up or comes back today', 'rentbaik' ) ),
     789                        array( 'warn', __( 'Unpaid', 'rentbaik' ), __( 'Not fully paid', 'rentbaik' ) ),
     790                        array( 'warn', __( 'Deposit', 'rentbaik' ), __( 'Deposit not given back yet', 'rentbaik' ) ),
     791                ) as $tag ) {
     792                        echo '<li><span class="rentbaik-cal__tag rentbaik-cal__tag--' . esc_attr( $tag[0] ) . '" aria-hidden="true">' . esc_html( $tag[1] ) . '</span>' . esc_html( $tag[2] ) . '</li>';
     793                }
    711794                if ( $fleet > 0 ) {
    712795                        echo '<li><span class="rentbaik-cal__occ">' . esc_html( '1/' . $fleet ) . '</span> ' . esc_html__( 'vehicles out / fleet', 'rentbaik' ) . '</li>';
  • rentbaik/tags/1.0.1/includes/Admin/DashboardPage.php

    r3790685 r3790901  
    215215
    216216                echo '<div class="rentbaik-grid">';
     217                SitePages::render_card();
    217218                if ( $done < count( $steps ) ) {
    218219                        self::render_setup( $steps, $done );
  • rentbaik/tags/1.0.1/includes/Admin/DayAgenda.php

    r3790685 r3790901  
    229229                echo '<span class="rentbaik-agenda__tags">';
    230230                View::print_status( $status, $labels[ $status ] ?? $status );
    231                 if ( $row['unpaid'] ) {
    232                         echo ' <i class="rentbaik-cal__unpaid" title="' . esc_attr__( 'Not fully paid', 'rentbaik' ) . '"></i><span class="screen-reader-text">' . esc_html__( 'Not fully paid', 'rentbaik' ) . '</span>';
     231                foreach ( (array) ( $row['flags'] ?? array() ) as $key => $flag ) {
     232                        echo ' <span class="rentbaik-cal__tag rentbaik-cal__tag--' . esc_attr( $flag['tone'] ) . ' rentbaik-cal__tag--' . esc_attr( $key ) . '" title="' . esc_attr( $flag['title'] ) . '">' . esc_html( $flag['label'] ) . '</span>';
    233233                }
    234234                echo '</span></button></li>';
  • rentbaik/tags/1.0.1/includes/Customers/Photos.php

    r3790685 r3790901  
    263263         * @return void
    264264         */
    265         private static function make_thumb( string $path ): void {
     265        public static function make_thumb( string $path ): void {
    266266                $editor = wp_get_image_editor( $path );
    267267                if ( is_wp_error( $editor ) ) {
    … …  
    278278         * @return string
    279279         */
    280         private static function thumb_path( string $path ): string {
     280        public static function thumb_path( string $path ): string {
    281281                $info = pathinfo( $path );
    282282                return $info['dirname'] . '/' . $info['filename'] . '-thumb.' . ( $info['extension'] ?? 'jpg' );
  • rentbaik/tags/1.0.1/includes/Setup/SampleData.php

    r3790685 r3790901  
    11<?php
    22/**
    3  * Sample data: example vehicles, units, extras and bookings for a first look, removable in one click.
     3 * Sample data: example vehicles, units, extras and a month of bookings for a first look, removable in one click.
    44 *
    55 * @package Rentbaik
    … …  
    88namespace Rentbaik\Setup;
    99
     10use DateTimeImmutable;
    1011use Rentbaik\Availability\BlockRepository;
    1112use Rentbaik\Booking\BookingRepository;
    1213use Rentbaik\Booking\BookingService;
    1314use Rentbaik\Customers\CustomerRepository;
     15use Rentbaik\Customers\Photos;
    1416use Rentbaik\Database\Repository;
    1517use Rentbaik\Fleet\UnitRepository;
    … …  
    3638
    3739        /**
    38          * Post meta that marks a sample vehicle.
     40         * Post meta that marks a sample vehicle (and sample images in the Media Library).
    3941         */
    4042        public const META = '_rentbaik_sample';
    … …  
    8890
    8991        /**
     92         * Payment methods paid before the pick-up (the others are paid at the counter).
     93         */
     94        private const PREPAID = array( 'bank_transfer', 'woocommerce', 'qris' );
     95
     96        /**
     97         * Media Library images made in this request: asset name => attachment ID.
     98         *
     99         * @var array<string, int>
     100         */
     101        private static $media = array();
     102
     103        /**
    90104         * Saved record of the sample data.
    91105         *
    … …  
    101115                        'bookings'  => array(),
    102116                        'customers' => array(),
     117                        'media'     => array(),
     118                        'blocks'    => array(),
    103119                );
    104120                foreach ( $defaults as $key => $empty ) {
    … …  
    144160                $types    = array_keys( Vehicle::enabled_types() );
    145161                $plate    = 100;
    146                 $first    = array();
     162                $models   = array();
     163                $extras   = array();
    147164
    148165                Emails::mute( true );
    … …  
    157174                                }
    158175                                $data['vehicles'][] = $vehicle_id;
    159                                 if ( ! isset( $first[ $definition['type'] ] ) ) {
    160                                         $first[ $definition['type'] ] = $vehicle_id;
    161                                 }
     176                                self::add_vehicle_images( $vehicle_id, $definition );
     177                                $models[ $definition['art'] ] = $vehicle_id;
    162178                                for ( $i = 0; $i < $definition['units']; $i++ ) {
    163179                                        $unit_id = self::create_unit( $vehicle_id, $location, $plate, $definition['colors'][ $i % count( $definition['colors'] ) ] );
    … …  
    165181                                        if ( $unit_id > 0 ) {
    166182                                                $data['units'][] = $unit_id;
     183                                                // A photo of this unit in its own colour.
     184                                                $art_color = $definition['art_colors'][ $i % count( $definition['art_colors'] ) ];
     185                                                $photo     = self::media( $definition['art'] . '-' . $art_color, (string) $definition['title'] );
     186                                                if ( $photo > 0 ) {
     187                                                        UnitRepository::save( $unit_id, array( 'photo_id' => $photo ) );
     188                                                }
    167189                                        }
    168190                                }
    169191                        }
    170192
    171                         foreach ( self::extra_definitions() as $extra ) {
     193                        foreach ( self::extra_definitions() as $key => $extra ) {
    172194                                if ( empty( array_intersect( $extra['vehicle_types'], $types ) ) ) {
    173195                                        continue;
    … …  
    190212                                if ( $extra_id > 0 ) {
    191213                                        $data['extras'][] = $extra_id;
     214                                        $extras[ $key ]   = $extra_id;
    192215                                }
    193216                        }
    194217
    195218                        // Save what exists so far, so a failure below still leaves everything removable.
     219                        $data['media'] = array_values( array_unique( array_merge( $data['media'], array_values( self::$media ) ) ) );
    196220                        update_option( self::OPTION, $data, false );
    197221
    198                         $vehicles = array_values( $first );
    199                         if ( ! empty( $vehicles ) ) {
    200                                 $data = self::create_bookings( $data, $vehicles, $location );
     222                        if ( ! empty( $models ) ) {
     223                                $data = self::create_bookings( $data, $models, $extras, $location );
     224                                update_option( self::OPTION, $data, false );
     225                                $data = self::add_service_block( $data, $models );
    201226                        }
    202227                } finally {
    … …  
    204229                }
    205230
     231                $data['media'] = array_values( array_unique( array_merge( $data['media'], array_values( self::$media ) ) ) );
    206232                update_option( self::OPTION, $data, false );
    207233                Repository::bump_data_version();
    … …  
    222248        public static function remove(): void {
    223249                $data = self::data();
     250
     251                foreach ( $data['blocks'] as $block_id ) {
     252                        BlockRepository::delete( $block_id );
     253                }
    224254
    225255                foreach ( $data['bookings'] as $booking_id ) {
    … …  
    233263                foreach ( $data['customers'] as $customer_id ) {
    234264                        if ( empty( BookingRepository::for_customer( $customer_id ) ) ) {
     265                                // Sample photos and payment proofs in the private folder.
     266                                Photos::remove_for_customer( $customer_id );
    235267                                CustomerRepository::delete( $customer_id );
    236268                        }
    … …  
    270302                }
    271303
     304                foreach ( $data['media'] as $attachment_id ) {
     305                        if ( '1' === (string) get_post_meta( $attachment_id, self::META, true ) ) {
     306                                wp_delete_attachment( $attachment_id, true );
     307                        }
     308                }
     309
    272310                delete_option( self::OPTION );
    273311                Repository::bump_data_version();
     312        }
     313
     314        /**
     315         * Path of a sample illustration shipped with the plugin.
     316         *
     317         * @param string $name Asset name without extension.
     318         * @return string '' when missing.
     319         */
     320        private static function asset( string $name ): string {
     321                $path = RENTBAIK_PATH . 'assets/sample/' . sanitize_file_name( $name ) . '.png';
     322                return file_exists( $path ) ? $path : '';
     323        }
     324
     325        /**
     326         * Media Library image from a sample illustration (made once per request and reused).
     327         *
     328         * @param string $name  Asset name.
     329         * @param string $title Image title.
     330         * @return int Attachment ID (0 when it could not be made).
     331         */
     332        private static function media( string $name, string $title ): int {
     333                if ( isset( self::$media[ $name ] ) ) {
     334                        return self::$media[ $name ];
     335                }
     336                $source = self::asset( $name );
     337                if ( '' === $source ) {
     338                        return 0;
     339                }
     340                $upload = wp_upload_bits( 'rentbaik-sample-' . $name . '.png', null, (string) Photos::filesystem()->get_contents( $source ) );
     341                if ( ! empty( $upload['error'] ) ) {
     342                        return 0;
     343                }
     344                $attachment_id = wp_insert_attachment(
     345                        array(
     346                                'post_mime_type' => 'image/png',
     347                                'post_title'     => $title,
     348                                'post_status'    => 'inherit',
     349                        ),
     350                        $upload['file']
     351                );
     352                if ( ! $attachment_id || is_wp_error( $attachment_id ) ) {
     353                        return 0;
     354                }
     355                require_once ABSPATH . 'wp-admin/includes/image.php';
     356                wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) );
     357                update_post_meta( $attachment_id, self::META, '1' );
     358                update_post_meta( $attachment_id, '_wp_attachment_image_alt', $title );
     359                self::$media[ $name ] = (int) $attachment_id;
     360                return (int) $attachment_id;
     361        }
     362
     363        /**
     364         * Featured image and gallery of a sample vehicle: its colours and the gear that comes with it.
     365         *
     366         * @param int                  $vehicle_id Vehicle post ID.
     367         * @param array<string, mixed> $definition Vehicle definition.
     368         * @return void
     369         */
     370        private static function add_vehicle_images( int $vehicle_id, array $definition ): void {
     371                $title    = (string) $definition['title'];
     372                $featured = self::media( $definition['art'] . '-' . $definition['art_colors'][0], $title );
     373                if ( $featured > 0 ) {
     374                        set_post_thumbnail( $vehicle_id, $featured );
     375                }
     376                $gallery = array();
     377                foreach ( array_slice( $definition['art_colors'], 1 ) as $color ) {
     378                        $gallery[] = self::media( $definition['art'] . '-' . $color, $title );
     379                }
     380                $gear      = array(
     381                        'motorcycle' => 'gear-scooter',
     382                        'car'        => 'gear-car',
     383                );
     384                $gallery[] = self::media( $gear[ $definition['type'] ] ?? 'gear-bike', __( 'Included gear (sample)', 'rentbaik' ) );
     385                $gallery   = array_filter( $gallery );
     386                if ( ! empty( $gallery ) ) {
     387                        update_post_meta( $vehicle_id, '_rentbaik_gallery', implode( ',', $gallery ) );
     388                }
     389        }
     390
     391        /**
     392         * Copy a sample illustration into the private folder.
     393         *
     394         * @param string $name   Asset name.
     395         * @param string $prefix File name prefix (photo, proof).
     396         * @return string File name in the private folder ('' when missing).
     397         */
     398        private static function private_copy( string $name, string $prefix ): string {
     399                $source = self::asset( $name );
     400                if ( '' === $source ) {
     401                        return '';
     402                }
     403                $file = $prefix . '-' . strtolower( wp_generate_password( 24, false ) ) . '.png';
     404                $path = Photos::dir() . '/' . $file;
     405                if ( ! Photos::filesystem()->put_contents( $path, (string) Photos::filesystem()->get_contents( $source ), FS_CHMOD_FILE ) ) {
     406                        return '';
     407                }
     408                Photos::make_thumb( $path );
     409                return $file;
     410        }
     411
     412        /**
     413         * Customer photo taken at the hand-over and the transfer receipt a customer uploaded, as on a real booking.
     414         *
     415         * @param int                  $booking_id Booking ID.
     416         * @param array<string, mixed> $plan       Plan.
     417         * @return void
     418         */
     419        private static function add_booking_files( int $booking_id, array $plan ): void {
     420                $booking = BookingRepository::find( $booking_id );
     421                if ( ! $booking ) {
     422                        return;
     423                }
     424                $add = static function ( string $asset, string $prefix, string $type, string $status, string $when ) use ( $booking_id, $booking ): void {
     425                        $file = self::private_copy( $asset, $prefix );
     426                        if ( '' !== $file ) {
     427                                Photos::insert(
     428                                        array(
     429                                                'customer_id' => (int) $booking['customer_id'],
     430                                                'booking_id'  => $booking_id,
     431                                                'doc_type'    => $type,
     432                                                'file_name'   => $file,
     433                                                'mime_type'   => 'image/png',
     434                                                'status'      => $status,
     435                                                'created_at'  => $when,
     436                                        ),
     437                                        array( '%d', '%d', '%s', '%s', '%s', '%s', '%s' )
     438                                );
     439                        }
     440                };
     441                if ( ! empty( $plan['proof'] ) ) {
     442                        $add( 'receipt-sample', 'proof', 'payment_proof', (string) $plan['proof'], self::created_at( $booking ) );
     443                }
     444                if ( ! empty( $plan['photo'] ) && in_array( $plan['status'], array( 'active', 'returned', 'completed' ), true ) ) {
     445                        $add( 'customer-' . (int) $plan['photo'], 'photo', 'photo', 'valid', (string) $booking['pickup_at'] );
     446                }
    274447        }
    275448
    … …  
    283456                return array(
    284457                        array(
    285                                 'type'    => 'motorcycle',
    286                                 'title'   => __( 'Automatic scooter 125 cc (sample)', 'rentbaik' ),
    287                                 'content' => __( 'Easy automatic scooter for the city. Two helmets included.', 'rentbaik' ) . "\n\n" . $note,
    288                                 'specs'   => array(
     458                                'type'       => 'motorcycle',
     459                                'art'        => 'scooter-125',
     460                                'art_colors' => array( 'white', 'red', 'black' ),
     461                                'title'      => __( 'Automatic scooter 125 cc (sample)', 'rentbaik' ),
     462                                'content'    => __( 'Easy automatic scooter for the city. Two helmets included.', 'rentbaik' ) . "\n\n" . $note,
     463                                'specs'      => array(
    289464                                        'engine_cc'    => 125,
    290465                                        'transmission' => 'automatic',
    291466                                        'helmets'      => 2,
    292467                                ),
    293                                 'rates'   => array(
     468                                'rates'      => array(
    294469                                        'hour' => 4,
    295470                                        'day'  => 15,
    296471                                        'week' => 90,
    297472                                ),
    298                                 'deposit' => 50,
    299                                 'units'   => 3,
    300                                 'colors'  => array( __( 'White', 'rentbaik' ), __( 'Red', 'rentbaik' ), __( 'Black', 'rentbaik' ) ),
     473                                'deposit'    => 50,
     474                                'units'      => 3,
     475                                'colors'     => array( __( 'White', 'rentbaik' ), __( 'Red', 'rentbaik' ), __( 'Black', 'rentbaik' ) ),
    301476                        ),
    302477                        array(
    303                                 'type'    => 'motorcycle',
    304                                 'title'   => __( 'Comfort scooter 160 cc (sample)', 'rentbaik' ),
    305                                 'content' => __( 'Bigger scooter with a large seat and storage, good for longer trips.', 'rentbaik' ) . "\n\n" . $note,
    306                                 'specs'   => array(
     478                                'type'       => 'motorcycle',
     479                                'art'        => 'scooter-160',
     480                                'art_colors' => array( 'grey', 'blue' ),
     481                                'title'      => __( 'Comfort scooter 160 cc (sample)', 'rentbaik' ),
     482                                'content'    => __( 'Bigger scooter with a large seat and storage, good for longer trips.', 'rentbaik' ) . "\n\n" . $note,
     483                                'specs'      => array(
    307484                                        'engine_cc'    => 160,
    308485                                        'transmission' => 'automatic',
    309486                                        'helmets'      => 2,
    310487                                ),
    311                                 'rates'   => array(
     488                                'rates'      => array(
    312489                                        'day'  => 22,
    313490                                        'week' => 130,
    314491                                ),
    315                                 'deposit' => 80,
    316                                 'units'   => 2,
    317                                 'colors'  => array( __( 'Grey', 'rentbaik' ), __( 'Blue', 'rentbaik' ) ),
     492                                'deposit'    => 80,
     493                                'units'      => 2,
     494                                'colors'     => array( __( 'Grey', 'rentbaik' ), __( 'Blue', 'rentbaik' ) ),
    318495                        ),
    319496                        array(
    320                                 'type'    => 'car',
    321                                 'title'   => __( 'Compact car, automatic (sample)', 'rentbaik' ),
    322                                 'content' => __( 'Small and easy to park. Air conditioning, five seats.', 'rentbaik' ) . "\n\n" . $note,
    323                                 'specs'   => array(
     497                                'type'       => 'car',
     498                                'art'        => 'car-compact',
     499                                'art_colors' => array( 'silver', 'white' ),
     500                                'title'      => __( 'Compact car, automatic (sample)', 'rentbaik' ),
     501                                'content'    => __( 'Small and easy to park. Air conditioning, five seats.', 'rentbaik' ) . "\n\n" . $note,
     502                                'specs'      => array(
    324503                                        'seats'        => 5,
    325504                                        'doors'        => 5,
    … …  
    329508                                        'luggage'      => 2,
    330509                                ),
    331                                 'rates'   => array(
     510                                'rates'      => array(
    332511                                        'day'  => 45,
    333512                                        'week' => 270,
    334513                                ),
    335                                 'deposit' => 200,
    336                                 'units'   => 2,
    337                                 'colors'  => array( __( 'Silver', 'rentbaik' ), __( 'White', 'rentbaik' ) ),
     514                                'deposit'    => 200,
     515                                'units'      => 2,
     516                                'colors'     => array( __( 'Silver', 'rentbaik' ), __( 'White', 'rentbaik' ) ),
    338517                        ),
    339518                        array(
    340                                 'type'    => 'car',
    341                                 'title'   => __( 'Family car, 7 seats (sample)', 'rentbaik' ),
    342                                 'content' => __( 'Room for the whole family and their bags.', 'rentbaik' ) . "\n\n" . $note,
    343                                 'specs'   => array(
     519                                'type'       => 'car',
     520                                'art'        => 'car-family',
     521                                'art_colors' => array( 'black' ),
     522                                'title'      => __( 'Family car, 7 seats (sample)', 'rentbaik' ),
     523                                'content'    => __( 'Room for the whole family and their bags.', 'rentbaik' ) . "\n\n" . $note,
     524                                'specs'      => array(
    344525                                        'seats'        => 7,
    345526                                        'doors'        => 5,
    … …  
    349530                                        'luggage'      => 3,
    350531                                ),
    351                                 'rates'   => array(
     532                                'rates'      => array(
    352533                                        'day'  => 65,
    353534                                        'week' => 390,
    354535                                ),
    355                                 'deposit' => 300,
    356                                 'units'   => 1,
    357                                 'colors'  => array( __( 'Black', 'rentbaik' ) ),
     536                                'deposit'    => 300,
     537                                'units'      => 1,
     538                                'colors'     => array( __( 'Black', 'rentbaik' ) ),
    358539                        ),
    359540                        array(
    360                                 'type'    => 'bicycle',
    361                                 'title'   => __( 'City bike (sample)', 'rentbaik' ),
    362                                 'content' => __( 'Comfortable bike with a basket and a lock.', 'rentbaik' ) . "\n\n" . $note,
    363                                 'specs'   => array(
     541                                'type'       => 'bicycle',
     542                                'art'        => 'bike-city',
     543                                'art_colors' => array( 'green' ),
     544                                'title'      => __( 'City bike (sample)', 'rentbaik' ),
     545                                'content'    => __( 'Comfortable bike with a basket and a lock.', 'rentbaik' ) . "\n\n" . $note,
     546                                'specs'      => array(
    364547                                        'frame_size' => 'M',
    365548                                        'gears'      => 7,
    366549                                ),
    367                                 'rates'   => array(
     550                                'rates'      => array(
    368551                                        'hour' => 3,
    369552                                        'day'  => 10,
    370553                                ),
    371                                 'deposit' => 0,
    372                                 'units'   => 2,
    373                                 'colors'  => array( __( 'Green', 'rentbaik' ) ),
     554                                'deposit'    => 0,
     555                                'units'      => 2,
     556                                'colors'     => array( __( 'Green', 'rentbaik' ) ),
    374557                        ),
    375558                        array(
    376                                 'type'    => 'ebike',
    377                                 'title'   => __( 'Electric bike (sample)', 'rentbaik' ),
    378                                 'content' => __( 'Pedal assist up to 60 km per charge.', 'rentbaik' ) . "\n\n" . $note,
    379                                 'specs'   => array(
     559                                'type'       => 'ebike',
     560                                'art'        => 'ebike',
     561                                'art_colors' => array( 'teal' ),
     562                                'title'      => __( 'Electric bike (sample)', 'rentbaik' ),
     563                                'content'    => __( 'Pedal assist up to 60 km per charge.', 'rentbaik' ) . "\n\n" . $note,
     564                                'specs'      => array(
    380565                                        'frame_size' => 'M',
    381566                                        'range_km'   => 60,
    382567                                ),
    383                                 'rates'   => array(
     568                                'rates'      => array(
    384569                                        'hour' => 6,
    385570                                        'day'  => 25,
    386571                                ),
    387                                 'deposit' => 100,
    388                                 'units'   => 2,
    389                                 'colors'  => array( __( 'White', 'rentbaik' ) ),
    390                         ),
    391                 );
    392         }
    393 
    394         /**
    395          * Sample extras.
    396          *
    397          * @return array<int, array<string, mixed>>
     572                                'deposit'    => 100,
     573                                'units'      => 2,
     574                                'colors'     => array( __( 'White', 'rentbaik' ) ),
     575                        ),
     576                );
     577        }
     578
     579        /**
     580         * Sample extras, by key.
     581         *
     582         * @return array<string, array<string, mixed>>
    398583         */
    399584        private static function extra_definitions(): array {
    400585                return array(
    401                         array(
     586                        'helmet'     => array(
    402587                                'name'          => __( 'Extra helmet (sample)', 'rentbaik' ),
    403588                                'description'   => __( 'Clean helmet for a passenger.', 'rentbaik' ),
    … …  
    407592                                'vehicle_types' => array( 'motorcycle', 'bicycle', 'ebike' ),
    408593                        ),
    409                         array(
     594                        'poncho'     => array(
    410595                                'name'          => __( 'Rain poncho (sample)', 'rentbaik' ),
    411596                                'description'   => __( 'Yours to keep.', 'rentbaik' ),
    … …  
    415600                                'vehicle_types' => array( 'motorcycle', 'bicycle', 'ebike' ),
    416601                        ),
    417                         array(
     602                        'child_seat' => array(
    418603                                'name'          => __( 'Child seat (sample)', 'rentbaik' ),
    419604                                'description'   => __( 'For children from 9 to 18 kg.', 'rentbaik' ),
    … …  
    490675
    491676        /**
    492          * Three sample bookings: one picked up today, one returning today and one waiting for confirmation.
     677         * A month of sample bookings in every common situation, around today.
     678         *
     679         * Keys: model (vehicle art), offset (days from today), hour, days, status, source, method (payment), paid
     680         * (none, part, full, refunded), part (share paid), deposit (none, held, returned), proof (pending, valid),
     681         * photo (customer portrait 1-8), extra (extra key), no_unit (taken although no unit is free).
     682         *
     683         * @return array<int, array<string, mixed>>
     684         */
     685        private static function plans(): array {
     686                $keys = array( 'first_name', 'country', 'phone', 'model', 'offset', 'hour', 'days', 'status', 'source', 'method', 'paid', 'part', 'deposit', 'proof', 'photo', 'extra', 'no_unit' );
     687                $rows = array(
     688                        // Finished last week: paid in cash, deposit given back.
     689                        array( 'Emma', 'GB', '15550106', 'scooter-125', -7, 10, 2, 'completed', 'whatsapp', 'cash', 'full', 0, 'returned', '', 1, 'helmet', false ),
     690                        // Finished: paid by card at the counter.
     691                        array( 'Olivia', 'US', '15550108', 'car-compact', -6, 9, 2, 'completed', 'web', 'card', 'full', 0, 'returned', '', 4, 'child_seat', false ),
     692                        // Returned two days ago, the deposit is still to be given back.
     693                        array( 'Lucas', 'FR', '15550103', 'scooter-160', -4, 9, 2, 'returned', 'admin', 'cash', 'full', 0, 'held', '', 3, '', false ),
     694                        // Cancelled by the customer: the transfer was refunded.
     695                        array( 'Liam', 'NZ', '15550107', 'bike-city', -3, 10, 1, 'cancelled', 'web', 'bank_transfer', 'refunded', 0.5, 'none', '', 0, '', false ),
     696                        // Did not come.
     697                        array( 'Wei', 'CN', '15550109', 'ebike', -2, 11, 1, 'no_show', 'web', 'pay_at_pickup', 'none', 0, 'none', '', 0, '', false ),
     698                        // Should have come back yesterday: late, deposit held, rent not paid yet.
     699                        array( 'Noah', 'US', '15550105', 'car-compact', -3, 9, 2, 'active', 'admin', 'pay_at_pickup', 'none', 0, 'held', '', 7, '', false ),
     700                        // On rent, comes back today; half paid with a QR code.
     701                        array( 'Maria', 'DE', '15550101', 'scooter-125', -2, 16, 2, 'active', 'whatsapp', 'qris', 'part', 0.5, 'held', '', 2, 'poncho', false ),
     702                        // Long rental under way, paid in advance by bank transfer.
     703                        array( 'Sofia', 'IT', '15550104', 'car-family', -5, 10, 12, 'active', 'web', 'bank_transfer', 'full', 0, 'held', 'valid', 5, '', false ),
     704                        // Picks up today, pays at the counter.
     705                        array( 'Alex', 'AU', '15550100', 'scooter-125', 0, 10, 3, 'confirmed', 'web', 'pay_at_pickup', 'none', 0, 'none', '', 0, '', false ),
     706                        // Picks up this afternoon, already paid online.
     707                        array( 'Aisha', 'AE', '15550110', 'scooter-160', 0, 15, 2, 'confirmed', 'web', 'woocommerce', 'full', 0, 'none', '', 0, '', false ),
     708                        // Taken by phone although the only family car is out: needs a unit before pick-up.
     709                        array( 'Omar', 'EG', '15550111', 'car-family', 2, 10, 3, 'confirmed', 'admin', 'pay_at_pickup', 'none', 0, 'none', '', 0, '', true ),
     710                        // Waiting for confirmation, transfer receipt uploaded but not checked yet.
     711                        array( 'Kenji', 'JP', '15550102', 'ebike', 3, 11, 4, 'pending', 'web', 'bank_transfer', 'none', 0, 'none', 'pending', 0, '', false ),
     712                        // Confirmed with a part payment by transfer.
     713                        array( 'Priya', 'IN', '15550112', 'car-compact', 6, 10, 5, 'confirmed', 'web', 'bank_transfer', 'part', 0.3, 'none', 'valid', 0, 'child_seat', false ),
     714                        // A week by WhatsApp, pays at pick-up.
     715                        array( 'Mateo', 'MX', '15550113', 'scooter-125', 9, 9, 7, 'confirmed', 'whatsapp', 'pay_at_pickup', 'none', 0, 'none', '', 0, 'helmet', false ),
     716                        // Waiting for confirmation from WhatsApp.
     717                        array( 'Putri', 'ID', '15550114', 'bike-city', 12, 8, 1, 'pending', 'whatsapp', 'pay_at_pickup', 'none', 0, 'none', '', 0, '', false ),
     718                        // Paid online for later this month.
     719                        array( 'Lena', 'SE', '15550115', 'car-compact', 16, 10, 4, 'confirmed', 'web', 'woocommerce', 'full', 0, 'none', '', 0, '', false ),
     720                        // A returning customer books again.
     721                        array( 'Emma', 'GB', '15550106', 'scooter-160', 19, 10, 3, 'pending', 'whatsapp', 'pay_at_pickup', 'none', 0, 'none', '', 0, '', false ),
     722                );
     723                return array_map(
     724                        static function ( array $row ) use ( $keys ): array {
     725                                return array_combine( $keys, $row );
     726                        },
     727                        $rows
     728                );
     729        }
     730
     731        /**
     732         * The planned model, or a similar one when its vehicle type is switched off (a shop with only cars still gets
     733         * every situation).
     734         *
     735         * @param string             $model  Vehicle art.
     736         * @param array<string, int> $models Created models: art => vehicle ID.
     737         * @return string '' when no model exists.
     738         */
     739        private static function model_for( string $model, array $models ): string {
     740                $similar = array(
     741                        'scooter-125' => array( 'scooter-160', 'bike-city', 'ebike' ),
     742                        'scooter-160' => array( 'scooter-125', 'ebike', 'bike-city' ),
     743                        'car-compact' => array( 'car-family', 'scooter-160', 'scooter-125' ),
     744                        'car-family'  => array( 'car-compact', 'scooter-160', 'scooter-125' ),
     745                        'bike-city'   => array( 'ebike', 'scooter-125', 'scooter-160' ),
     746                        'ebike'       => array( 'bike-city', 'scooter-160', 'scooter-125' ),
     747                );
     748                foreach ( array_merge( array( $model ), $similar[ $model ] ?? array(), array_keys( $models ) ) as $candidate ) {
     749                        if ( isset( $models[ $candidate ] ) ) {
     750                                return $candidate;
     751                        }
     752                }
     753                return '';
     754        }
     755
     756        /**
     757         * Days from today for a plan, kept inside the current month where its situation allows it.
     758         *
     759         * @param array<string, mixed> $plan Plan.
     760         * @return int
     761         */
     762        private static function offset_in_month( array $plan ): int {
     763                $today  = new DateTimeImmutable( 'today', wp_timezone() );
     764                $offset = (int) $plan['offset'];
     765                $days   = (int) $plan['days'];
     766                if ( $offset > 0 ) {
     767                        // Later this month: keep the whole rental before the end of the month when there is room.
     768                        $left = (int) $today->format( 't' ) - (int) $today->format( 'j' );
     769                        return max( 1, min( $offset, $left - $days ) );
     770                }
     771                if ( $offset + $days <= 0 ) {
     772                        // Already over: as early as needed, but not before the first day of the month when it fits.
     773                        $first = 1 - (int) $today->format( 'j' );
     774                        return max( $offset, $first ) + $days <= 0 ? max( $offset, $first ) : $offset;
     775                }
     776                return $offset;
     777        }
     778
     779        /**
     780         * Create the sample bookings and bring each to the situation of its plan.
    493781         *
    494782         * @param array<string, int[]> $data        Sample data record.
    495          * @param int[]                $vehicles    Sample vehicle IDs to book.
     783         * @param array<string, int>   $models      Vehicle art => vehicle ID.
     784         * @param array<string, int>   $extras      Extra key => extra ID.
    496785         * @param int                  $location_id Location ID.
    497786         * @return array<string, int[]>
    498787         */
    499         private static function create_bookings( array $data, array $vehicles, int $location_id ): array {
    500                 $plans = array(
    501                         array(
    502                                 'first_name' => 'Alex',
    503                                 'country'    => 'AU',
    504                                 'phone'      => '15550100',
    505                                 'offset'     => 0,
    506                                 'hour'       => 10,
    507                                 'days'       => 3,
    508                                 'status'     => 'confirmed',
    509                                 'source'     => 'web',
    510                         ),
    511                         array(
    512                                 'first_name' => 'Maria',
    513                                 'country'    => 'DE',
    514                                 'phone'      => '15550101',
    515                                 'offset'     => -2,
    516                                 'hour'       => 16,
    517                                 'days'       => 2,
    518                                 'status'     => 'active',
    519                                 'source'     => 'whatsapp',
    520                         ),
    521                         array(
    522                                 'first_name' => 'Kenji',
    523                                 'country'    => 'JP',
    524                                 'phone'      => '15550102',
    525                                 'offset'     => 3,
    526                                 'hour'       => 11,
    527                                 'days'       => 4,
    528                                 'status'     => 'pending',
    529                                 'source'     => 'web',
    530                         ),
    531                 );
    532 
    533                 foreach ( $plans as $index => $plan ) {
    534                         $vehicle_id = $vehicles[ $index % count( $vehicles ) ];
     788        private static function create_bookings( array $data, array $models, array $extras, int $location_id ): array {
     789                foreach ( self::plans() as $plan ) {
     790                        $plan['model'] = self::model_for( (string) $plan['model'], $models );
     791                        if ( '' === $plan['model'] ) {
     792                                continue;
     793                        }
     794                        $offset     = self::offset_in_month( $plan );
    535795                        $booking_id = 0;
    536                         // Move a day later when the location is closed on the planned day.
     796                        // Move a day later when the location is closed on the planned day or no unit is free.
    537797                        for ( $shift = 0; $shift < 7 && $booking_id <= 0; $shift++ ) {
    538                                 $pickup     = Time::local_midnight( $plan['offset'] + $shift )->modify( '+' . $plan['hour'] . ' hours' );
    539                                 $result     = BookingService::create(
    540                                         array(
    541                                                 'vehicle_id'     => $vehicle_id,
    542                                                 'pickup'         => $pickup,
    543                                                 'return'         => $pickup->modify( '+' . $plan['days'] . ' days' ),
    544                                                 'location_id'    => $location_id,
    545                                                 'customer'       => array(
    546                                                         'first_name' => $plan['first_name'],
    547                                                         'last_name'  => __( 'Sample', 'rentbaik' ),
    548                                                         'phone'      => $plan['phone'],
    549                                                         'country'    => $plan['country'],
    550                                                         'notes'      => __( 'Sample data', 'rentbaik' ),
    551                                                 ),
    552                                                 'status'         => 'pending' === $plan['status'] ? 'pending' : 'confirmed',
    553                                                 'source'         => $plan['source'],
    554                                                 'payment_method' => 'pay_at_pickup',
    555                                                 'internal_note'  => __( 'Sample booking. It is removed together with the sample data.', 'rentbaik' ),
    556                                                 'allow_past'     => true,
    557                                         )
     798                                $pickup = Time::local_midnight( $offset + $shift )->modify( '+' . $plan['hour'] . ' hours' );
     799                                $input  = array(
     800                                        'vehicle_id'       => $models[ $plan['model'] ],
     801                                        'pickup'           => $pickup,
     802                                        'return'           => $pickup->modify( '+' . $plan['days'] . ' days' ),
     803                                        'location_id'      => $location_id,
     804                                        'customer'         => array(
     805                                                'first_name' => $plan['first_name'],
     806                                                'last_name'  => __( 'Sample', 'rentbaik' ),
     807                                                'phone'      => $plan['phone'],
     808                                                'country'    => $plan['country'],
     809                                                'notes'      => __( 'Sample data', 'rentbaik' ),
     810                                        ),
     811                                        'status'           => 'pending' === $plan['status'] ? 'pending' : 'confirmed',
     812                                        'source'           => $plan['source'],
     813                                        'payment_method'   => $plan['method'],
     814                                        'internal_note'    => __( 'Sample booking. It is removed together with the sample data.', 'rentbaik' ),
     815                                        'allow_past'       => true,
     816                                        'allow_unassigned' => ! empty( $plan['no_unit'] ),
    558817                                );
     818                                if ( ! empty( $plan['extra'] ) && isset( $extras[ $plan['extra'] ] ) ) {
     819                                        $input['extras'] = array( $extras[ $plan['extra'] ] => 1 );
     820                                }
     821                                $result = BookingService::create( $input );
     822                                // An extra the vehicle type does not allow: try again without it.
     823                                if ( ! is_int( $result ) && isset( $input['extras'] ) ) {
     824                                        unset( $input['extras'] );
     825                                        $result = BookingService::create( $input );
     826                                }
    559827                                $booking_id = is_int( $result ) ? $result : 0;
    560828                        }
    … …  
    567835                                $data['customers'][] = (int) $booking['customer_id'];
    568836                        }
    569                         if ( 'active' === $plan['status'] ) {
    570                                 BookingService::set_status( $booking_id, 'active' );
     837                        self::play_out( $booking_id, $plan );
     838                        self::add_booking_files( $booking_id, $plan );
     839                }
     840                return $data;
     841        }
     842
     843        /**
     844         * When the sample booking was made: three days before the pick-up, never later than an hour ago.
     845         *
     846         * @param array<string, mixed> $booking Booking row.
     847         * @return string UTC (Y-m-d H:i:s).
     848         */
     849        private static function created_at( array $booking ): string {
     850                $pickup = Time::from_utc( (string) $booking['pickup_at'] );
     851                $latest = Time::now()->modify( '-1 hour' );
     852                $made   = $pickup ? $pickup->modify( '-3 days' ) : $latest;
     853                return Time::to_mysql( $made < $latest ? $made : $latest );
     854        }
     855
     856        /**
     857         * Bring a sample booking to the situation of its plan: handed over, paid, returned, cancelled or not shown.
     858         * Dates are set to when it would have happened, so the reports and the calendar look like a real month.
     859         *
     860         * @param int                  $booking_id Booking ID.
     861         * @param array<string, mixed> $plan       Plan.
     862         * @return void
     863         */
     864        private static function play_out( int $booking_id, array $plan ): void {
     865                $booking = BookingRepository::find( $booking_id );
     866                if ( ! $booking ) {
     867                        return;
     868                }
     869                $now     = Time::to_mysql( Time::now() );
     870                $made    = self::created_at( $booking );
     871                $pickup  = min( (string) $booking['pickup_at'], $now );
     872                $return  = min( (string) $booking['return_at'], $now );
     873                $method  = (string) $plan['method'];
     874                $paid    = (string) ( $plan['paid'] ?? 'none' );
     875                $deposit = (string) ( $plan['deposit'] ?? 'none' );
     876                // Money paid in advance arrives two hours after booking; the rest at the counter.
     877                $prepaid = Time::to_mysql( min( Time::now(), Time::from_utc( $made )->modify( '+2 hours' ) ) );
     878                $pay     = static function ( string $type, string $how, int $amount, string $when, string $note = '' ) use ( $booking_id ): void {
     879                        if ( $amount > 0 ) {
     880                                BookingService::add_payment(
     881                                        $booking_id,
     882                                        array(
     883                                                'payment_type' => $type,
     884                                                'method'       => $how,
     885                                                'amount'       => $amount,
     886                                                'paid_at'      => $when,
     887                                                'reference'    => 'cash' === $how ? '' : 'SAMPLE-' . $booking_id,
     888                                                'note'         => '' === $note ? __( 'Sample payment', 'rentbaik' ) : $note,
     889                                        )
     890                                );
     891                        }
     892                };
     893
     894                BookingRepository::patch( $booking_id, array( 'created_at' => $made ) );
     895
     896                $total = (int) $booking['total'];
     897                $share = 'full' === $paid ? $total : (int) round( $total * (float) ( $plan['part'] ?? 0 ) );
     898                if ( in_array( $paid, array( 'full', 'part', 'refunded' ), true ) && in_array( $method, self::PREPAID, true ) ) {
     899                        $pay( 'payment', $method, $share, $prepaid );
     900                }
     901
     902                if ( in_array( $plan['status'], array( 'active', 'returned', 'completed' ), true ) ) {
     903                        BookingService::set_status( $booking_id, 'active' );
     904                        BookingRepository::patch( $booking_id, array( 'actual_pickup_at' => $pickup ) );
     905                        if ( in_array( $paid, array( 'full', 'part' ), true ) && ! in_array( $method, self::PREPAID, true ) ) {
     906                                $pay( 'payment', $method, $share, $pickup );
     907                        }
     908                        if ( 'none' !== $deposit ) {
     909                                $pay( 'deposit', 'cash', (int) $booking['deposit_amount'], $pickup, __( 'Deposit at pick-up', 'rentbaik' ) );
     910                        }
     911                }
     912
     913                if ( in_array( $plan['status'], array( 'returned', 'completed' ), true ) ) {
     914                        BookingService::set_status( $booking_id, 'returned' );
     915                        BookingRepository::patch( $booking_id, array( 'actual_return_at' => $return ) );
     916                        if ( 'returned' === $deposit ) {
     917                                $pay( 'deposit_refund', 'cash', (int) $booking['deposit_amount'], $return, __( 'Deposit given back', 'rentbaik' ) );
     918                        }
     919                        if ( 'completed' === $plan['status'] ) {
     920                                BookingService::set_status( $booking_id, 'completed' );
     921                        }
     922                }
     923
     924                if ( 'cancelled' === $plan['status'] ) {
     925                        BookingService::set_status( $booking_id, 'cancelled' );
     926                        if ( 'refunded' === $paid ) {
     927                                $pay( 'refund', $method, $share, max( $prepaid, Time::to_mysql( Time::now()->modify( '-1 day' ) ) ), __( 'Refund after cancelling', 'rentbaik' ) );
     928                        }
     929                }
     930                if ( 'no_show' === $plan['status'] ) {
     931                        BookingService::set_status( $booking_id, 'no_show' );
     932                }
     933        }
     934
     935        /**
     936         * Service for a scooter over the next two days, so the calendar shows a unit that cannot be rented.
     937         *
     938         * @param array<string, int[]> $data   Sample data record.
     939         * @param array<string, int>   $models Vehicle art => vehicle ID.
     940         * @return array<string, int[]>
     941         */
     942        private static function add_service_block( array $data, array $models ): array {
     943                $vehicle_id = $models['scooter-160'] ?? ( $models['scooter-125'] ?? 0 );
     944                if ( $vehicle_id <= 0 ) {
     945                        return $data;
     946                }
     947                $start = Time::to_mysql( Time::local_midnight( 1 )->modify( '+8 hours' ) );
     948                $end   = Time::to_mysql( Time::local_midnight( 3 )->modify( '+8 hours' ) );
     949                $units = array();
     950                foreach ( $data['units'] as $unit_id ) {
     951                        $unit = UnitRepository::find( $unit_id );
     952                        if ( $unit && (int) $unit['vehicle_id'] === $vehicle_id ) {
     953                                $units[] = $unit_id;
     954                        }
     955                }
     956                $busy = BlockRepository::busy_units( $units, $start, $end );
     957                foreach ( $units as $unit_id ) {
     958                        if ( ! in_array( $unit_id, $busy, true ) ) {
     959                                $block_id = BlockRepository::add( $unit_id, $start, $end, 'maintenance', 0, null, __( 'Oil change (sample)', 'rentbaik' ) );
     960                                if ( $block_id > 0 ) {
     961                                        $data['blocks'][] = $block_id;
     962                                }
     963                                break;
    571964                        }
    572965                }
    … …  
    580973         * @return int
    581974         */
    582         private static function price( float $usd ): int {
     975        public static function price( float $usd ): int {
    583976                if ( $usd <= 0 ) {
    584977                        return 0;
    … …  
    586979                $currency = Format::currency();
    587980                $amount   = $usd * (float) ( self::USD_RATES[ $currency ] ?? 1 );
    588                 $step     = $amount >= 100 ? 10 ** ( (int) floor( log10( $amount ) ) - 1 ) : 1;
    589                 $amount   = max( $step, round( $amount / $step ) * $step );
     981                // Small amounts keep halves (5.50), larger ones are rounded to two significant digits (88,000).
     982                $step   = $amount >= 100 ? 10 ** ( (int) floor( log10( $amount ) ) - 1 ) : ( $amount < 10 ? 0.5 : 1 );
     983                $amount = max( $step, round( $amount / $step ) * $step );
    590984                return (int) round( $amount * ( 10 ** Format::decimals() ) );
    591985        }
  • rentbaik/tags/1.0.1/languages/rentbaik.pot

    r3790685 r3790901  
    33msgid ""
    44msgstr ""
    5 "Project-Id-Version: Rentbaik 1.0.0\n"
     5"Project-Id-Version: Rentbaik 1.0.1\n"
    66"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/rentbaik\n"
    77"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
    … …  
    1010"Content-Type: text/plain; charset=UTF-8\n"
    1111"Content-Transfer-Encoding: 8bit\n"
    12 "POT-Creation-Date: 2026-11-03T05:38:00+00:00\n"
     12"POT-Creation-Date: 2026-11-08T10:09:24+00:00\n"
    1313"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
    1414"X-Generator: WP-CLI 2.12.0\n"
    … …  
    1818#. Author of the plugin
    1919#: rentbaik.php
    20 #: includes/Admin/Menu.php:46
    21 #: includes/Admin/Menu.php:47
     20#: includes/Admin/Menu.php:44
     21#: includes/Admin/Menu.php:45
    2222#: includes/Admin/View.php:31
    2323#: includes/Privacy/Privacy.php:170
    … …  
    114114msgstr ""
    115115
    116 #: includes/Admin/Admin.php:47
    117 #: includes/Admin/Menu.php:122
     116#: includes/Admin/Admin.php:48
     117#: includes/Admin/Menu.php:132
    118118#: includes/Admin/SettingsPage.php:480
    119119msgid "Settings"
    120120msgstr ""
    121121
    122 #: includes/Admin/Admin.php:53
     122#: includes/Admin/Admin.php:54
    123123#: includes/Admin/DashboardPage.php:186
    124 #: includes/Admin/Menu.php:58
     124#: includes/Admin/Menu.php:56
    125125msgid "Dashboard"
    126126msgstr ""
    … …  
    177177#: includes/Admin/BookingsPage.php:331
    178178#: includes/Admin/BookingsPage.php:732
     179#: includes/Admin/CalendarView.php:285
     180#: includes/Admin/CalendarView.php:790
    179181msgid "Deposit"
    180182msgstr ""
    … …  
    305307#: includes/Admin/ExtrasPage.php:59
    306308#: includes/Admin/ExtrasPage.php:111
    307 #: includes/Fleet/VehiclePostType.php:393
     309#: includes/Fleet/VehiclePostType.php:395
    308310msgid "Price"
    309311msgstr ""
    … …  
    311313#: includes/Admin/BookingMove.php:191
    312314#: includes/Admin/BookingsPage.php:162
    313 #: includes/Admin/CalendarView.php:521
     315#: includes/Admin/CalendarView.php:597
    314316msgid "Unit"
    315317msgstr ""
    … …  
    518520#: includes/Admin/BookingPanel.php:201
    519521#: includes/Admin/ExtrasPage.php:52
    520 #: includes/Admin/Menu.php:86
    521 #: includes/Admin/Menu.php:87
     522#: includes/Admin/Menu.php:96
     523#: includes/Admin/Menu.php:97
    522524#: templates/booking-form.php:96
    523525msgid "Extras"
    … …  
    688690#: includes/Admin/CustomersPage.php:63
    689691#: includes/Admin/CustomersPage.php:117
    690 #: includes/Admin/Menu.php:66
    691 #: includes/Admin/Menu.php:67
     692#: includes/Admin/Menu.php:64
     693#: includes/Admin/Menu.php:65
    692694msgid "Bookings"
    693695msgstr ""
    … …  
    782784
    783785#: includes/Admin/BookingsPage.php:190
    784 #: includes/Admin/CalendarView.php:703
     786#: includes/Admin/CalendarView.php:779
    785787#: includes/Booking/BookingService.php:44
    786788#: includes/Frontend/BookingForm.php:311
    … …  
    825827
    826828#: includes/Admin/BookingsPage.php:286
    827 #: includes/Admin/DashboardPage.php:390
     829#: includes/Admin/DashboardPage.php:391
    828830#: includes/Admin/ExtrasPage.php:79
     831#: includes/Admin/SitePages.php:162
    829832#: includes/Admin/UnitsTable.php:368
    830833msgid "Edit"
    … …  
    925928#: includes/Admin/SetupWizard.php:455
    926929#: includes/Admin/UnitsTable.php:54
    927 #: includes/Fleet/VehiclePostType.php:392
     930#: includes/Fleet/VehiclePostType.php:394
    928931#: includes/Frontend/VehicleContent.php:49
    929932msgid "Type"
    … …  
    10821085
    10831086#: includes/Admin/CalendarView.php:117
    1084 #: includes/Admin/Menu.php:75
    1085 #: includes/Admin/Menu.php:76
     1087#: includes/Admin/Menu.php:85
     1088#: includes/Admin/Menu.php:86
    10861089#: includes/Admin/UnitsPage.php:59
    1087 #: includes/Fleet/VehiclePostType.php:394
     1090#: includes/Fleet/VehiclePostType.php:396
    10881091msgid "Units"
    10891092msgstr ""
    … …  
    10941097
    10951098#: includes/Admin/CalendarView.php:124
    1096 #: includes/Admin/DashboardPage.php:312
     1099#: includes/Admin/CalendarView.php:260
     1100#: includes/Admin/CalendarView.php:268
     1101#: includes/Admin/CalendarView.php:788
     1102#: includes/Admin/DashboardPage.php:313
    10971103msgid "Today"
    10981104msgstr ""
    … …  
    11101116
    11111117#. translators: %s: date and time.
    1112 #: includes/Admin/CalendarView.php:190
     1118#: includes/Admin/CalendarView.php:193
    11131119#: includes/Notifications/WhatsApp.php:74
    11141120#, php-format
    … …  
    11171123
    11181124#. translators: %s: date and time.
    1119 #: includes/Admin/CalendarView.php:192
     1125#: includes/Admin/CalendarView.php:195
    11201126#: includes/Notifications/WhatsApp.php:79
    11211127#, php-format
    … …  
    11231129msgstr ""
    11241130
     1131#: includes/Admin/CalendarView.php:252
     1132#: includes/Admin/CalendarView.php:787
     1133msgid "Late"
     1134msgstr ""
     1135
     1136#. translators: %d: number of hours.
     1137#: includes/Admin/CalendarView.php:254
     1138#, php-format
     1139msgid "Late by %d hour"
     1140msgid_plural "Late by %d hours"
     1141msgstr[0] ""
     1142msgstr[1] ""
     1143
     1144#: includes/Admin/CalendarView.php:261
     1145msgid "Comes back today"
     1146msgstr ""
     1147
     1148#: includes/Admin/CalendarView.php:269
     1149msgid "Picked up today"
     1150msgstr ""
     1151
     1152#: includes/Admin/CalendarView.php:276
     1153#: includes/Booking/BookingService.php:600
     1154msgid "Part paid"
     1155msgstr ""
     1156
     1157#: includes/Admin/CalendarView.php:276
     1158#: includes/Admin/CalendarView.php:789
     1159#: includes/Booking/BookingService.php:599
     1160msgid "Unpaid"
     1161msgstr ""
     1162
     1163#: includes/Admin/CalendarView.php:277
     1164#: includes/Admin/CalendarView.php:789
     1165msgid "Not fully paid"
     1166msgstr ""
     1167
     1168#: includes/Admin/CalendarView.php:286
     1169#: includes/Admin/CalendarView.php:790
     1170msgid "Deposit not given back yet"
     1171msgstr ""
     1172
    11251173#. translators: %s: date.
    1126 #: includes/Admin/CalendarView.php:366
    1127 #: includes/Admin/CalendarView.php:535
     1174#: includes/Admin/CalendarView.php:431
     1175#: includes/Admin/CalendarView.php:611
    11281176#, php-format
    11291177msgid "Show the agenda of %s"
    … …  
    11311179
    11321180#. translators: 1: vehicles out, 2: fleet size.
    1133 #: includes/Admin/CalendarView.php:371
     1181#: includes/Admin/CalendarView.php:436
    11341182#, php-format
    11351183msgid "%1$d of %2$d vehicles out"
    11361184msgstr ""
    11371185
    1138 #: includes/Admin/CalendarView.php:374
     1186#: includes/Admin/CalendarView.php:439
    11391187msgid "Closed"
    11401188msgstr ""
    11411189
    11421190#. translators: %s: date.
    1143 #: includes/Admin/CalendarView.php:385
     1191#: includes/Admin/CalendarView.php:450
    11441192#, php-format
    11451193msgid "Add booking on %s"
    … …  
    11471195
    11481196#. translators: %d: number of bookings not shown.
    1149 #: includes/Admin/CalendarView.php:397
     1197#: includes/Admin/CalendarView.php:462
    11501198#, php-format
    11511199msgid "+%d more"
    11521200msgstr ""
    11531201
    1154 #: includes/Admin/CalendarView.php:457
     1202#: includes/Admin/CalendarView.php:522
    11551203msgid "Drag to change the pick-up date"
    11561204msgstr ""
    11571205
    1158 #: includes/Admin/CalendarView.php:474
     1206#: includes/Admin/CalendarView.php:539
    11591207msgid "No unit"
    11601208msgstr ""
    11611209
    1162 #: includes/Admin/CalendarView.php:477
    1163 #: includes/Admin/CalendarView.php:710
    1164 #: includes/Admin/DayAgenda.php:232
    1165 msgid "Not fully paid"
    1166 msgstr ""
    1167 
    1168 #: includes/Admin/CalendarView.php:480
     1210#: includes/Admin/CalendarView.php:556
    11691211msgid "Drag to change the return date"
    11701212msgstr ""
    11711213
    11721214#. translators: %d: number of units.
    1173 #: includes/Admin/CalendarView.php:554
     1215#: includes/Admin/CalendarView.php:630
    11741216#, php-format
    11751217msgid "%d unit"
    … …  
    11781220msgstr[1] ""
    11791221
    1180 #: includes/Admin/CalendarView.php:570
     1222#: includes/Admin/CalendarView.php:646
    11811223msgid "No unit yet"
    11821224msgstr ""
    11831225
    1184 #: includes/Admin/CalendarView.php:571
     1226#: includes/Admin/CalendarView.php:647
    11851227msgid "drag to a unit"
    11861228msgstr ""
    11871229
    1188 #: includes/Admin/CalendarView.php:670
     1230#: includes/Admin/CalendarView.php:746
    11891231#: includes/Admin/UnitsPage.php:166
    11901232#: includes/Admin/UnitsPage.php:222
    1191 #: includes/Admin/UnitsPage.php:388
     1233#: includes/Admin/UnitsPage.php:389
    11921234msgid "Service"
    11931235msgstr ""
    11941236
    1195 #: includes/Admin/CalendarView.php:670
     1237#: includes/Admin/CalendarView.php:746
    11961238#: includes/Admin/UnitsPage.php:167
    1197 #: includes/Admin/UnitsPage.php:388
     1239#: includes/Admin/UnitsPage.php:389
    11981240msgid "Blocked"
    11991241msgstr ""
    12001242
    1201 #: includes/Admin/CalendarView.php:688
     1243#: includes/Admin/CalendarView.php:764
    12021244msgid "No units yet. Add the units (plate numbers) of your vehicles first."
    12031245msgstr ""
    12041246
    1205 #: includes/Admin/CalendarView.php:702
     1247#: includes/Admin/CalendarView.php:778
    12061248#: includes/Booking/BookingService.php:43
    12071249msgid "Waiting"
    12081250msgstr ""
    12091251
    1210 #: includes/Admin/CalendarView.php:704
     1252#: includes/Admin/CalendarView.php:780
    12111253#: includes/Booking/BookingService.php:45
    12121254#: includes/Fleet/UnitRepository.php:41
    … …  
    12151257msgstr ""
    12161258
    1217 #: includes/Admin/CalendarView.php:705
     1259#: includes/Admin/CalendarView.php:781
     1260#: includes/Admin/CalendarView.php:787
    12181261#: includes/Admin/UnitsTable.php:312
    12191262msgid "Late return"
    12201263msgstr ""
    12211264
    1222 #: includes/Admin/CalendarView.php:706
     1265#: includes/Admin/CalendarView.php:782
    12231266msgid "Returned / completed"
    12241267msgstr ""
    12251268
    1226 #: includes/Admin/CalendarView.php:712
     1269#: includes/Admin/CalendarView.php:788
     1270msgid "Picked up or comes back today"
     1271msgstr ""
     1272
     1273#: includes/Admin/CalendarView.php:795
    12271274msgid "vehicles out / fleet"
    12281275msgstr ""
    12291276
    12301277#. translators: %d: number of pick-ups.
    1231 #: includes/Admin/CalendarView.php:736
     1278#: includes/Admin/CalendarView.php:819
    12321279#, php-format
    12331280msgid "%d pick-up"
    … …  
    12371284
    12381285#. translators: %d: number of returns.
    1239 #: includes/Admin/CalendarView.php:738
     1286#: includes/Admin/CalendarView.php:821
    12401287#, php-format
    12411288msgid "%d return"
    … …  
    12451292
    12461293#. translators: %d: number of bookings waiting for confirmation.
    1247 #: includes/Admin/CalendarView.php:740
     1294#: includes/Admin/CalendarView.php:823
    12481295#, php-format
    12491296msgid "%d waiting"
    … …  
    12531300
    12541301#: includes/Admin/CustomersPage.php:58
    1255 #: includes/Admin/Menu.php:95
    1256 #: includes/Admin/Menu.php:96
     1302#: includes/Admin/Menu.php:105
     1303#: includes/Admin/Menu.php:106
    12571304msgid "Customers"
    12581305msgstr ""
    … …  
    13581405
    13591406#: includes/Admin/DashboardPage.php:65
    1360 #: includes/Fleet/VehiclePostType.php:146
     1407#: includes/Fleet/VehiclePostType.php:148
    13611408msgid "Units (plate numbers)"
    13621409msgstr ""
    … …  
    15051552msgstr ""
    15061553
    1507 #: includes/Admin/DashboardPage.php:235
     1554#: includes/Admin/DashboardPage.php:236
    15081555msgid "Pick-ups today"
    15091556msgstr ""
    15101557
    1511 #: includes/Admin/DashboardPage.php:236
     1558#: includes/Admin/DashboardPage.php:237
    15121559msgid "Returns today"
    15131560msgstr ""
    15141561
    1515 #: includes/Admin/DashboardPage.php:237
     1562#: includes/Admin/DashboardPage.php:238
    15161563msgid "Late returns"
    15171564msgstr ""
    15181565
    1519 #: includes/Admin/DashboardPage.php:238
     1566#: includes/Admin/DashboardPage.php:239
    15201567msgid "Waiting for confirmation"
    15211568msgstr ""
    15221569
    15231570#. translators: %d: number of more bookings.
    1524 #: includes/Admin/DashboardPage.php:261
     1571#: includes/Admin/DashboardPage.php:262
    15251572#, php-format
    15261573msgid "+ %d more"
    15271574msgstr ""
    15281575
    1529 #: includes/Admin/DashboardPage.php:265
     1576#: includes/Admin/DashboardPage.php:266
    15301577msgid "Review and confirm"
    15311578msgstr ""
    15321579
    1533 #: includes/Admin/DashboardPage.php:267
     1580#: includes/Admin/DashboardPage.php:268
    15341581msgid "Everything is back on time."
    15351582msgstr ""
    15361583
    1537 #: includes/Admin/DashboardPage.php:267
     1584#: includes/Admin/DashboardPage.php:268
    15381585msgid "Nothing planned."
    15391586msgstr ""
    15401587
    1541 #: includes/Admin/DashboardPage.php:283
     1588#: includes/Admin/DashboardPage.php:284
    15421589msgid "Fleet right now"
    15431590msgstr ""
    15441591
    1545 #: includes/Admin/DashboardPage.php:283
     1592#: includes/Admin/DashboardPage.php:284
    15461593#: includes/Admin/UnitsPage.php:94
    15471594msgid "All units"
    15481595msgstr ""
    15491596
    1550 #: includes/Admin/DashboardPage.php:285
     1597#: includes/Admin/DashboardPage.php:286
    15511598msgid "No units yet."
    15521599msgstr ""
    15531600
    15541601#. translators: %d: percentage of the fleet on rent.
    1555 #: includes/Admin/DashboardPage.php:290
     1602#: includes/Admin/DashboardPage.php:291
    15561603#, php-format
    15571604msgid "%d%% on rent"
    15581605msgstr ""
    15591606
    1560 #: includes/Admin/DashboardPage.php:291
     1607#: includes/Admin/DashboardPage.php:292
    15611608msgid "Fleet status"
    15621609msgstr ""
    15631610
    1564 #: includes/Admin/DashboardPage.php:312
     1611#: includes/Admin/DashboardPage.php:313
    15651612msgid "vs yesterday"
    15661613msgstr ""
    15671614
    1568 #: includes/Admin/DashboardPage.php:313
     1615#: includes/Admin/DashboardPage.php:314
    15691616msgid "Last 7 days"
    15701617msgstr ""
    15711618
    1572 #: includes/Admin/DashboardPage.php:313
     1619#: includes/Admin/DashboardPage.php:314
    15731620msgid "vs the 7 days before"
    15741621msgstr ""
    15751622
    1576 #: includes/Admin/DashboardPage.php:314
     1623#: includes/Admin/DashboardPage.php:315
    15771624msgid "This month"
    15781625msgstr ""
    15791626
    1580 #: includes/Admin/DashboardPage.php:314
     1627#: includes/Admin/DashboardPage.php:315
    15811628msgid "vs the same days last month"
    15821629msgstr ""
    15831630
    1584 #: includes/Admin/DashboardPage.php:316
     1631#: includes/Admin/DashboardPage.php:317
    15851632msgid "Income received"
    15861633msgstr ""
    15871634
    1588 #: includes/Admin/DashboardPage.php:326
     1635#: includes/Admin/DashboardPage.php:327
    15891636msgid "Rental payments recorded in Rentbaik, without deposits."
    15901637msgstr ""
    15911638
    1592 #: includes/Admin/DashboardPage.php:347
     1639#: includes/Admin/DashboardPage.php:348
    15931640msgid "Welcome to Rentbaik"
    15941641msgstr ""
    15951642
    1596 #: includes/Admin/DashboardPage.php:348
     1643#: includes/Admin/DashboardPage.php:349
    15971644msgid "Answer a few simple questions and your booking page is ready. Not sure yet? Try it with sample data first."
    15981645msgstr ""
    15991646
    1600 #: includes/Admin/DashboardPage.php:349
     1647#: includes/Admin/DashboardPage.php:350
    16011648msgid "About 5 minutes"
    16021649msgstr ""
    16031650
    1604 #: includes/Admin/DashboardPage.php:349
     1651#: includes/Admin/DashboardPage.php:350
    16051652msgid "Every step can be skipped"
    16061653msgstr ""
    16071654
    1608 #: includes/Admin/DashboardPage.php:349
     1655#: includes/Admin/DashboardPage.php:350
    16091656msgid "Change everything later"
    16101657msgstr ""
    16111658
    1612 #: includes/Admin/DashboardPage.php:351
     1659#: includes/Admin/DashboardPage.php:352
    16131660msgid "Start the setup wizard"
    16141661msgstr ""
    16151662
    1616 #: includes/Admin/DashboardPage.php:356
     1663#: includes/Admin/DashboardPage.php:357
    16171664msgid "Hide this"
    16181665msgstr ""
    16191666
    1620 #: includes/Admin/DashboardPage.php:370
     1667#: includes/Admin/DashboardPage.php:371
    16211668msgid "Get ready to take bookings"
    16221669msgstr ""
    16231670
    16241671#. translators: 1: completed steps, 2: total steps.
    1625 #: includes/Admin/DashboardPage.php:372
     1672#: includes/Admin/DashboardPage.php:373
    16261673#, php-format
    16271674msgid "%1$d of %2$d done"
    16281675msgstr ""
    16291676
    1630 #: includes/Admin/DashboardPage.php:374
     1677#: includes/Admin/DashboardPage.php:375
    16311678msgid "Prefer step by step? Open the setup wizard."
    16321679msgstr ""
    16331680
    1634 #: includes/Admin/DashboardPage.php:390
     1681#: includes/Admin/DashboardPage.php:391
    16351682msgid "Set up"
    16361683msgstr ""
    16371684
    1638 #: includes/Admin/DashboardPage.php:404
     1685#: includes/Admin/DashboardPage.php:405
    16391686msgid "System status"
    16401687msgstr ""
    16411688
    1642 #: includes/Admin/DashboardPage.php:407
     1689#: includes/Admin/DashboardPage.php:408
    16431690msgid "OK"
    16441691msgstr ""
    16451692
    1646 #: includes/Admin/DashboardPage.php:407
     1693#: includes/Admin/DashboardPage.php:408
    16471694msgid "Check"
    16481695msgstr ""
    16491696
    1650 #: includes/Admin/DashboardPage.php:407
     1697#: includes/Admin/DashboardPage.php:408
    16511698msgid "Optional"
    16521699msgstr ""
    … …  
    16991746#: includes/Admin/ExtrasPage.php:187
    17001747#: includes/Admin/UnitsPage.php:51
    1701 #: includes/Admin/UnitsPage.php:429
     1748#: includes/Admin/UnitsPage.php:430
    17021749msgid "You are not allowed to manage the fleet."
    17031750msgstr ""
    … …  
    17631810#: includes/Admin/ExtrasPage.php:112
    17641811#: includes/Admin/SettingsPage.php:110
    1765 #: includes/Fleet/VehiclePostType.php:213
     1812#: includes/Fleet/VehiclePostType.php:215
    17661813#: includes/Frontend/VehicleContent.php:74
    17671814msgid "Per day"
    … …  
    18101857msgstr ""
    18111858
    1812 #: includes/Admin/Menu.php:57
     1859#: includes/Admin/Menu.php:55
    18131860msgid "Rentbaik Dashboard"
    18141861msgstr ""
    18151862
    1816 #: includes/Admin/Menu.php:104
    1817 #: includes/Fleet/VehiclePostType.php:115
     1863#: includes/Admin/Menu.php:76
     1864#: includes/Admin/Menu.php:77
     1865#: includes/Admin/SetupWizard.php:78
     1866#: includes/Fleet/VehiclePostType.php:80
     1867#: includes/Fleet/VehiclePostType.php:90
     1868#: includes/Fleet/VehiclePostType.php:91
     1869msgid "Vehicles"
     1870msgstr ""
     1871
     1872#: includes/Admin/Menu.php:114
     1873#: includes/Fleet/VehiclePostType.php:117
    18181874msgid "Vehicle categories"
    18191875msgstr ""
    18201876
    1821 #: includes/Admin/Menu.php:105
    1822 #: includes/Fleet/VehiclePostType.php:117
     1877#: includes/Admin/Menu.php:115
     1878#: includes/Fleet/VehiclePostType.php:119
    18231879msgid "Categories"
    18241880msgstr ""
    18251881
    1826 #: includes/Admin/Menu.php:112
     1882#: includes/Admin/Menu.php:122
    18271883#: includes/Admin/SetupWizard.php:140
    18281884#: includes/Admin/SetupWizard.php:162
    … …  
    18301886msgstr ""
    18311887
    1832 #: includes/Admin/Menu.php:113
     1888#: includes/Admin/Menu.php:123
    18331889#: includes/Admin/SettingsPage.php:480
    18341890msgid "Setup wizard"
    18351891msgstr ""
    18361892
    1837 #: includes/Admin/Menu.php:121
     1893#: includes/Admin/Menu.php:131
    18381894msgid "Rentbaik Settings"
     1895msgstr ""
     1896
     1897#: includes/Admin/PrivateCheck.php:111
     1898msgid "Customer files are not protected yet."
     1899msgstr ""
     1900
     1901#: includes/Admin/PrivateCheck.php:112
     1902msgid "Your web server opens files in the Rentbaik private folder directly. Customer photos and payment receipts have random names that cannot be guessed, but to block the folder completely add this rule to your nginx configuration (or ask your host):"
     1903msgstr ""
     1904
     1905#: includes/Admin/PrivateCheck.php:114
     1906msgid "Check again"
     1907msgstr ""
     1908
     1909#: includes/Admin/PrivateCheck.php:124
     1910msgid "You are not allowed to do this."
     1911msgstr ""
     1912
     1913#: includes/Admin/PrivateCheck.php:129
     1914msgid "The private folder is protected."
     1915msgstr ""
     1916
     1917#: includes/Admin/PrivateCheck.php:131
     1918msgid "The check could not reach your site. Please try again later."
    18391919msgstr ""
    18401920
    … …  
    19202000
    19212001#: includes/Admin/SettingsPage.php:111
    1922 #: includes/Fleet/VehiclePostType.php:212
     2002#: includes/Fleet/VehiclePostType.php:214
    19232003#: includes/Frontend/VehicleContent.php:73
    19242004msgid "Per hour"
    … …  
    20262106
    20272107#: includes/Admin/SettingsPage.php:175
    2028 #: includes/Fleet/VehiclePostType.php:228
     2108#: includes/Fleet/VehiclePostType.php:230
    20292109msgid "Minimum rental (hours)"
    20302110msgstr ""
    20312111
    20322112#: includes/Admin/SettingsPage.php:177
    2033 #: includes/Fleet/VehiclePostType.php:229
     2113#: includes/Fleet/VehiclePostType.php:231
    20342114msgid "Maximum rental (days)"
    20352115msgstr ""
    … …  
    20642144
    20652145#: includes/Admin/SettingsPage.php:194
    2066 #: includes/Fleet/VehiclePostType.php:227
     2146#: includes/Fleet/VehiclePostType.php:229
    20672147msgid "Security deposit"
    20682148msgstr ""
    … …  
    23362416
    23372417#: includes/Admin/SettingsPage.php:587
     2418#: includes/Admin/SitePages.php:159
    23382419msgid "View"
    23392420msgstr ""
    … …  
    24822563#: includes/Admin/SetupWizard.php:76
    24832564msgid "Pick-up place"
    2484 msgstr ""
    2485 
    2486 #: includes/Admin/SetupWizard.php:78
    2487 #: includes/Fleet/VehiclePostType.php:80
    2488 #: includes/Fleet/VehiclePostType.php:90
    2489 #: includes/Fleet/VehiclePostType.php:91
    2490 msgid "Vehicles"
    24912565msgstr ""
    24922566
    … …  
    27172791
    27182792#: includes/Admin/SetupWizard.php:521
     2793#: includes/Admin/SitePages.php:159
     2794#: includes/Admin/SitePages.php:177
    27192795#: templates/booking-form.php:69
    27202796msgid "(opens in a new tab)"
    … …  
    27982874msgstr ""
    27992875
     2876#: includes/Admin/SitePages.php:32
     2877msgid "Customers choose dates and see the free vehicles with the price."
     2878msgstr ""
     2879
     2880#: includes/Admin/SitePages.php:33
     2881msgid "Booking form in three steps; the confirmation and the booking details open here too."
     2882msgstr ""
     2883
     2884#: includes/Admin/SitePages.php:34
     2885msgid "Customers look up their booking with its number and their email or phone."
     2886msgstr ""
     2887
     2888#: includes/Admin/SitePages.php:56
     2889msgid "Vehicle pages"
     2890msgstr ""
     2891
     2892#. translators: %s: vehicle name.
     2893#: includes/Admin/SitePages.php:58
     2894#, php-format
     2895msgid "Every vehicle has its own page with photos, prices, availability and a booking button. Example: %s"
     2896msgstr ""
     2897
     2898#: includes/Admin/SitePages.php:126
     2899msgid "Your pages for customers"
     2900msgstr ""
     2901
     2902#: includes/Admin/SitePages.php:128
     2903msgid "Change pages"
     2904msgstr ""
     2905
     2906#: includes/Admin/SitePages.php:131
     2907msgid "Rentbaik made these pages when it was activated. Add them to your site menu, or put the shortcodes on any page or in any page builder."
     2908msgstr ""
     2909
     2910#. translators: %d: number of pages.
     2911#: includes/Admin/SitePages.php:136
     2912#, php-format
     2913msgid "%d page is missing."
     2914msgid_plural "%d pages are missing."
     2915msgstr[0] ""
     2916msgstr[1] ""
     2917
     2918#: includes/Admin/SitePages.php:139
     2919msgid "Create them in Settings → Display"
     2920msgstr ""
     2921
     2922#: includes/Admin/SitePages.php:148
     2923msgid "Not published"
     2924msgstr ""
     2925
     2926#: includes/Admin/SitePages.php:151
     2927msgid "Missing"
     2928msgstr ""
     2929
     2930#. translators: %s: booking number.
     2931#: includes/Admin/SitePages.php:174
     2932#, php-format
     2933msgid "See sample booking %s as the customer sees it"
     2934msgstr ""
     2935
    28002936#: includes/Admin/UnitsPage.php:59
    28012937msgid "Every real vehicle with its plate number, kilometres and status."
    … …  
    28052941#: includes/Admin/UnitsPage.php:93
    28062942#: includes/Admin/UnitsPage.php:142
    2807 #: includes/Fleet/VehiclePostType.php:296
     2943#: includes/Fleet/VehiclePostType.php:298
    28082944msgid "Add unit"
    28092945msgstr ""
    … …  
    29513087
    29523088#. translators: %s: unit status.
    2953 #: includes/Admin/UnitsPage.php:344
     3089#: includes/Admin/UnitsPage.php:345
    29543090#, php-format
    29553091msgid "Status set to %s (bulk action)."
    … …  
    29573093
    29583094#. translators: %d: number of units.
    2959 #: includes/Admin/UnitsPage.php:347
     3095#: includes/Admin/UnitsPage.php:348
    29603096#, php-format
    29613097msgid "%d unit updated."
    … …  
    29643100msgstr[1] ""
    29653101
    2966 #: includes/Admin/UnitsPage.php:371
     3102#: includes/Admin/UnitsPage.php:372
    29673103msgid "Choose a start and an end; the end must be after the start."
    29683104msgstr ""
    29693105
    2970 #: includes/Admin/UnitsPage.php:376
     3106#: includes/Admin/UnitsPage.php:377
    29713107msgid "Dates blocked. Note: this unit already has a booking or block in that period."
    29723108msgstr ""
    29733109
    2974 #: includes/Admin/UnitsPage.php:378
     3110#: includes/Admin/UnitsPage.php:379
    29753111msgid "Dates blocked."
    29763112msgstr ""
    29773113
    29783114#. translators: 1: block type, 2: start, 3: end.
    2979 #: includes/Admin/UnitsPage.php:387
     3115#: includes/Admin/UnitsPage.php:388
    29803116#, php-format
    29813117msgid "Blocked (%1$s): %2$s – %3$s."
    … …  
    29833119
    29843120#. translators: 1: start, 2: end.
    2985 #: includes/Admin/UnitsPage.php:410
     3121#: includes/Admin/UnitsPage.php:411
    29863122#, php-format
    29873123msgid "Block removed: %1$s – %2$s."
    29883124msgstr ""
    29893125
    2990 #: includes/Admin/UnitsPage.php:411
     3126#: includes/Admin/UnitsPage.php:412
    29913127msgid "Block removed."
    29923128msgstr ""
    … …  
    30773213
    30783214#: includes/Admin/UnitsTable.php:330
    3079 #: includes/Fleet/VehiclePostType.php:267
     3215#: includes/Fleet/VehiclePostType.php:269
    30803216msgid "No units yet. A unit is one real vehicle with its own plate number."
    30813217msgstr ""
    … …  
    32183354msgstr ""
    32193355
    3220 #: includes/Booking/BookingService.php:599
    3221 msgid "Unpaid"
    3222 msgstr ""
    3223 
    3224 #: includes/Booking/BookingService.php:600
    3225 msgid "Part paid"
    3226 msgstr ""
    3227 
    32283356#: includes/Capabilities.php:70
    32293357msgid "Rental Manager"
    … …  
    33953523msgstr ""
    33963524
    3397 #: includes/Fleet/VehiclePostType.php:116
     3525#: includes/Fleet/VehiclePostType.php:118
    33983526msgid "Vehicle category"
    33993527msgstr ""
    34003528
    3401 #: includes/Fleet/VehiclePostType.php:118
     3529#: includes/Fleet/VehiclePostType.php:120
    34023530msgid "Add category"
    34033531msgstr ""
    34043532
    3405 #: includes/Fleet/VehiclePostType.php:119
     3533#: includes/Fleet/VehiclePostType.php:121
    34063534msgid "Edit category"
    34073535msgstr ""
    34083536
    3409 #: includes/Fleet/VehiclePostType.php:120
     3537#: includes/Fleet/VehiclePostType.php:122
    34103538msgid "Search categories"
    34113539msgstr ""
    34123540
    3413 #: includes/Fleet/VehiclePostType.php:143
     3541#: includes/Fleet/VehiclePostType.php:145
    34143542msgid "Vehicle details"
    34153543msgstr ""
    34163544
    3417 #: includes/Fleet/VehiclePostType.php:144
     3545#: includes/Fleet/VehiclePostType.php:146
    34183546#: templates/vehicle-details.php:35
    34193547msgid "Prices"
    34203548msgstr ""
    34213549
    3422 #: includes/Fleet/VehiclePostType.php:145
     3550#: includes/Fleet/VehiclePostType.php:147
    34233551msgid "Photo gallery"
    34243552msgstr ""
    34253553
    3426 #: includes/Fleet/VehiclePostType.php:162
     3554#: includes/Fleet/VehiclePostType.php:164
    34273555msgid "Vehicle type"
    34283556msgstr ""
    34293557
    3430 #: includes/Fleet/VehiclePostType.php:167
     3558#: includes/Fleet/VehiclePostType.php:169
    34313559#: includes/Frontend/VehicleContent.php:53
    34323560msgid "Brand"
    34333561msgstr ""
    34343562
    3435 #: includes/Fleet/VehiclePostType.php:167
     3563#: includes/Fleet/VehiclePostType.php:169
    34363564msgid "e.g. Honda"
    34373565msgstr ""
    34383566
    3439 #: includes/Fleet/VehiclePostType.php:168
     3567#: includes/Fleet/VehiclePostType.php:170
    34403568#: includes/Frontend/VehicleContent.php:56
    34413569msgid "Model year"
    … …  
    34433571
    34443572#. translators: %s: vehicle type, e.g. Car.
    3445 #: includes/Fleet/VehiclePostType.php:178
     3573#: includes/Fleet/VehiclePostType.php:180
    34463574#, php-format
    34473575msgid "%s specifications"
    34483576msgstr ""
    34493577
    3450 #: includes/Fleet/VehiclePostType.php:214
     3578#: includes/Fleet/VehiclePostType.php:216
    34513579#: includes/Frontend/VehicleContent.php:75
    34523580msgid "Per week (7 days)"
    34533581msgstr ""
    34543582
    3455 #: includes/Fleet/VehiclePostType.php:215
     3583#: includes/Fleet/VehiclePostType.php:217
    34563584#: includes/Frontend/VehicleContent.php:76
    34573585msgid "Per month (30 days)"
    34583586msgstr ""
    34593587
    3460 #: includes/Fleet/VehiclePostType.php:220
     3588#: includes/Fleet/VehiclePostType.php:222
    34613589msgid "Leave a price empty if you do not offer that period. Longer rentals automatically use the cheaper weekly or monthly price when it is set."
    34623590msgstr ""
    34633591
    3464 #: includes/Fleet/VehiclePostType.php:231
     3592#: includes/Fleet/VehiclePostType.php:233
    34653593msgid "Empty deposit, minimum and maximum use the defaults from Settings → Rental rules. Maximum 0 means no limit."
    34663594msgstr ""
    34673595
    3468 #: includes/Fleet/VehiclePostType.php:249
     3596#: includes/Fleet/VehiclePostType.php:251
    34693597msgid "Choose photos"
    34703598msgstr ""
    34713599
    3472 #: includes/Fleet/VehiclePostType.php:250
     3600#: includes/Fleet/VehiclePostType.php:252
    34733601msgid "Remove all"
    34743602msgstr ""
    34753603
    3476 #: includes/Fleet/VehiclePostType.php:251
     3604#: includes/Fleet/VehiclePostType.php:253
    34773605msgid "The featured image is the main photo. Add more photos here for the vehicle page."
    34783606msgstr ""
    34793607
    3480 #: includes/Fleet/VehiclePostType.php:285
     3608#: includes/Fleet/VehiclePostType.php:287
    34813609msgid "Save the vehicle first, then add its units."
    34823610msgstr ""
    34833611
    34843612#. translators: %s: price.
    3485 #: includes/Fleet/VehiclePostType.php:416
     3613#: includes/Fleet/VehiclePostType.php:418
    34863614#, php-format
    34873615msgid "%s / day"
    … …  
    34893617
    34903618#. translators: %s: price.
    3491 #: includes/Fleet/VehiclePostType.php:419
     3619#: includes/Fleet/VehiclePostType.php:421
    34923620#, php-format
    34933621msgid "%s / hour"
    34943622msgstr ""
    34953623
    3496 #: includes/Fleet/VehiclePostType.php:421
     3624#: includes/Fleet/VehiclePostType.php:423
    34973625msgid "No price yet"
    34983626msgstr ""
    34993627
    35003628#. translators: 1: ready units, 2: all units.
    3501 #: includes/Fleet/VehiclePostType.php:428
     3629#: includes/Fleet/VehiclePostType.php:430
    35023630#, php-format
    35033631msgid "%1$d ready of %2$d"
    … …  
    39524080msgstr ""
    39534081
    3954 #: includes/Setup/SampleData.php:282
     4082#: includes/Setup/SampleData.php:384
     4083msgid "Included gear (sample)"
     4084msgstr ""
     4085
     4086#: includes/Setup/SampleData.php:455
    39554087msgid "This is sample data from Rentbaik. Change it, or remove all sample data from the Rentbaik dashboard."
    39564088msgstr ""
    39574089
    3958 #: includes/Setup/SampleData.php:286
     4090#: includes/Setup/SampleData.php:461
    39594091msgid "Automatic scooter 125 cc (sample)"
    39604092msgstr ""
    39614093
    3962 #: includes/Setup/SampleData.php:287
     4094#: includes/Setup/SampleData.php:462
    39634095msgid "Easy automatic scooter for the city. Two helmets included."
    39644096msgstr ""
    39654097
    3966 #: includes/Setup/SampleData.php:300
    3967 #: includes/Setup/SampleData.php:337
    3968 #: includes/Setup/SampleData.php:389
     4098#: includes/Setup/SampleData.php:475
     4099#: includes/Setup/SampleData.php:516
     4100#: includes/Setup/SampleData.php:574
    39694101msgid "White"
    39704102msgstr ""
    39714103
    3972 #: includes/Setup/SampleData.php:300
     4104#: includes/Setup/SampleData.php:475
    39734105msgid "Red"
    39744106msgstr ""
    39754107
    3976 #: includes/Setup/SampleData.php:300
    3977 #: includes/Setup/SampleData.php:357
     4108#: includes/Setup/SampleData.php:475
     4109#: includes/Setup/SampleData.php:538
    39784110msgid "Black"
    39794111msgstr ""
    39804112
    3981 #: includes/Setup/SampleData.php:304
     4113#: includes/Setup/SampleData.php:481
    39824114msgid "Comfort scooter 160 cc (sample)"
    39834115msgstr ""
    39844116
    3985 #: includes/Setup/SampleData.php:305
     4117#: includes/Setup/SampleData.php:482
    39864118msgid "Bigger scooter with a large seat and storage, good for longer trips."
    39874119msgstr ""
    39884120
    3989 #: includes/Setup/SampleData.php:317
     4121#: includes/Setup/SampleData.php:494
    39904122msgid "Grey"
    39914123msgstr ""
    39924124
    3993 #: includes/Setup/SampleData.php:317
     4125#: includes/Setup/SampleData.php:494
    39944126msgid "Blue"
    39954127msgstr ""
    39964128
    3997 #: includes/Setup/SampleData.php:321
     4129#: includes/Setup/SampleData.php:500
    39984130msgid "Compact car, automatic (sample)"
    39994131msgstr ""
    40004132
    4001 #: includes/Setup/SampleData.php:322
     4133#: includes/Setup/SampleData.php:501
    40024134msgid "Small and easy to park. Air conditioning, five seats."
    40034135msgstr ""
    40044136
    4005 #: includes/Setup/SampleData.php:337
     4137#: includes/Setup/SampleData.php:516
    40064138msgid "Silver"
    40074139msgstr ""
    40084140
    4009 #: includes/Setup/SampleData.php:341
     4141#: includes/Setup/SampleData.php:522
    40104142msgid "Family car, 7 seats (sample)"
    40114143msgstr ""
    40124144
    4013 #: includes/Setup/SampleData.php:342
     4145#: includes/Setup/SampleData.php:523
    40144146msgid "Room for the whole family and their bags."
    40154147msgstr ""
    40164148
    4017 #: includes/Setup/SampleData.php:361
     4149#: includes/Setup/SampleData.php:544
    40184150msgid "City bike (sample)"
    40194151msgstr ""
    40204152
    4021 #: includes/Setup/SampleData.php:362
     4153#: includes/Setup/SampleData.php:545
    40224154msgid "Comfortable bike with a basket and a lock."
    40234155msgstr ""
    40244156
    4025 #: includes/Setup/SampleData.php:373
     4157#: includes/Setup/SampleData.php:556
    40264158msgid "Green"
    40274159msgstr ""
    40284160
    4029 #: includes/Setup/SampleData.php:377
     4161#: includes/Setup/SampleData.php:562
    40304162msgid "Electric bike (sample)"
    40314163msgstr ""
    40324164
    4033 #: includes/Setup/SampleData.php:378
     4165#: includes/Setup/SampleData.php:563
    40344166msgid "Pedal assist up to 60 km per charge."
    40354167msgstr ""
    40364168
    4037 #: includes/Setup/SampleData.php:402
     4169#: includes/Setup/SampleData.php:587
    40384170msgid "Extra helmet (sample)"
    40394171msgstr ""
    40404172
    4041 #: includes/Setup/SampleData.php:403
     4173#: includes/Setup/SampleData.php:588
    40424174msgid "Clean helmet for a passenger."
    40434175msgstr ""
    40444176
    4045 #: includes/Setup/SampleData.php:410
     4177#: includes/Setup/SampleData.php:595
    40464178msgid "Rain poncho (sample)"
    40474179msgstr ""
    40484180
    4049 #: includes/Setup/SampleData.php:411
     4181#: includes/Setup/SampleData.php:596
    40504182msgid "Yours to keep."
    40514183msgstr ""
    40524184
    4053 #: includes/Setup/SampleData.php:418
     4185#: includes/Setup/SampleData.php:603
    40544186msgid "Child seat (sample)"
    40554187msgstr ""
    40564188
    4057 #: includes/Setup/SampleData.php:419
     4189#: includes/Setup/SampleData.php:604
    40584190msgid "For children from 9 to 18 kg."
    40594191msgstr ""
    40604192
    4061 #: includes/Setup/SampleData.php:486
    4062 #: includes/Setup/SampleData.php:550
     4193#: includes/Setup/SampleData.php:671
     4194#: includes/Setup/SampleData.php:809
    40634195msgid "Sample data"
    40644196msgstr ""
    40654197
    4066 #: includes/Setup/SampleData.php:547
     4198#: includes/Setup/SampleData.php:806
    40674199msgid "Sample"
    40684200msgstr ""
    40694201
    4070 #: includes/Setup/SampleData.php:555
     4202#: includes/Setup/SampleData.php:814
    40714203msgid "Sample booking. It is removed together with the sample data."
     4204msgstr ""
     4205
     4206#: includes/Setup/SampleData.php:888
     4207msgid "Sample payment"
     4208msgstr ""
     4209
     4210#: includes/Setup/SampleData.php:909
     4211msgid "Deposit at pick-up"
     4212msgstr ""
     4213
     4214#: includes/Setup/SampleData.php:917
     4215msgid "Deposit given back"
     4216msgstr ""
     4217
     4218#: includes/Setup/SampleData.php:927
     4219msgid "Refund after cancelling"
     4220msgstr ""
     4221
     4222#: includes/Setup/SampleData.php:959
     4223msgid "Oil change (sample)"
    40724224msgstr ""
    40734225
  • rentbaik/tags/1.0.1/readme.txt

    r3790685 r3790901  
    11=== Rentbaik ===
    22Contributors: rentbaik
    3 Tags: rental, booking, car rental, bike rental, scooter rental
     3Tags: rental, booking, car rental, motorbike rental, scooter rental
    44Requires at least: 6.5
    55Tested up to: 7.1
    66Requires PHP: 8.3
    7 Stable tag: 1.0.0
     7Stable tag: 1.0.1
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    6868= Can I try Rentbaik before adding my own vehicles? =
    6969
    70 Yes. Click **Add sample data** in the setup wizard or on the Rentbaik dashboard. Rentbaik adds example vehicles marked "(sample)", units, extras and three bookings, without sending any email. **Remove sample data** deletes all of it again; your own vehicles and bookings are not touched.
     70Yes. Click **Add sample data** in the setup wizard or on the Rentbaik dashboard. Rentbaik adds example vehicles marked "(sample)" with pictures, units with their own photo, extras, a service day and a month of bookings in every common situation: picked up today, coming back today, late, part paid, deposit still to give back, waiting for a unit, waiting for confirmation with a transfer receipt, cancelled with a refund and not shown. Customer photos and receipts are drawings marked "SAMPLE", not real people or documents. No email is sent. **Remove sample data** deletes all of it again; your own vehicles and bookings are not touched.
     71
     72= Where are the pages for my customers? =
     73
     74Rentbaik creates the pages Rentals, Book a rental and My booking when it is activated. The Rentbaik dashboard lists them with their shortcodes and links to view or edit them, plus an example vehicle page. Add them to your site menu, or place the shortcodes on any page.
    7175
    7276= Do I need WooCommerce or Elementor? =
    … …  
    150154== Changelog ==
    151155
     156= 1.0.1 =
     157* Fix: the booking details panel and the day agenda on the Rentbaik dashboard did not open on sites with "Plain" permalinks ("The booking could not be loaded").
     158* New: "Your pages for customers" card on the Rentbaik dashboard. It lists the Rentals, Book a rental and My booking pages with their shortcodes and View and Edit links, an example vehicle page, and a link to see a sample booking as the customer sees it.
     159* New: labels on the calendar bars and in the day agenda: Late, Today (pick-up or return), Unpaid or Part paid, and Deposit (not given back yet).
     160* Improved: the sample data now fills the current month with 17 bookings in every common situation: completed, returned with the deposit still held, cancelled with a refund, no-show, late, coming back today, picked up today, without a unit, waiting with a transfer receipt, part paid and paid online.
     161* Improved: sample vehicles and units have pictures, and sample customers have photos and transfer receipts. All are drawings marked "SAMPLE", not real people or documents. Remove sample data deletes them too.
     162* Improved: when a vehicle type is switched off, the sample bookings use a similar vehicle.
     163* Translation: complete Indonesian translation.
     164* Tip: if you added sample data with 1.0.0, remove it and add it again to see the new example month.
     165
    152166= 1.0.0 =
    153167* First release.
    … …  
    155169== Upgrade Notice ==
    156170
     171= 1.0.1 =
     172Fixes the booking panel on sites with Plain permalinks. Adds a card with your customer pages, labels on the calendar and a full month of sample data with pictures.
     173
    157174= 1.0.0 =
    158175First release.
  • rentbaik/tags/1.0.1/rentbaik.php

    r3790685 r3790901  
    33 * Plugin Name:       Rentbaik
    44 * Description:       Run a bike, scooter or car rental business in WordPress: fleet by plate number, rates, availability, bookings and local payment options. Works with any theme.
    5  * Version:           1.0.0
     5 * Version:           1.0.1
    66 * Requires at least: 6.5
    77 * Requires PHP:      8.3
    … …  
    1717defined( 'ABSPATH' ) || exit;
    1818
    19 define( 'RENTBAIK_VERSION', '1.0.0' );
     19define( 'RENTBAIK_VERSION', '1.0.1' );
    2020define( 'RENTBAIK_FILE', __FILE__ );
    2121define( 'RENTBAIK_PATH', plugin_dir_path( __FILE__ ) );
  • rentbaik/trunk/assets/admin/admin.css

    r3790685 r3790901  
    158158}
    159159
     160/* Customer pages card */
     161.rentbaik-sitepages__list {
     162        list-style: none;
     163        margin: 8px 0 0;
     164        padding: 0;
     165}
     166
     167.rentbaik-sitepages__item {
     168        display: grid;
     169        grid-template-columns: minmax(0, 1fr) auto;
     170        align-items: center;
     171        gap: 10px;
     172        margin: 0;
     173        padding-block: 10px;
     174        border-block-start: 1px solid var(--rentbaik-line);
     175}
     176
     177.rentbaik-sitepages__item:first-child {
     178        border-block-start: 0;
     179}
     180
     181.rentbaik-sitepages__text span:not(.rentbaik-pill) {
     182        display: block;
     183        color: var(--rentbaik-muted);
     184        overflow-wrap: anywhere;
     185}
     186
     187.rentbaik-sitepages__text code {
     188        display: inline-block;
     189        margin-block-start: 4px;
     190        font-size: 12px;
     191        overflow-wrap: anywhere;
     192}
     193
     194.rentbaik-sitepages__actions {
     195        display: flex;
     196        align-items: center;
     197        gap: 10px;
     198        white-space: nowrap;
     199}
     200
     201.rentbaik-sitepages__try {
     202        display: flex;
     203        align-items: center;
     204        gap: 6px;
     205        margin: 10px 0 0;
     206        padding-block-start: 10px;
     207        border-block-start: 1px solid var(--rentbaik-line);
     208}
     209
     210@media (max-width: 600px) {
     211        .rentbaik-sitepages__item {
     212                grid-template-columns: minmax(0, 1fr);
     213        }
     214}
     215
    160216/* Status list */
    161217.rentbaik-status {
    … …  
    11851241.rentbaik-cal__legend .rentbaik-cal__unpaid {
    11861242        margin: 0;
     1243}
     1244
     1245/* Short labels on a booking bar: late, today, not fully paid, deposit. */
     1246.rentbaik-cal__text {
     1247        flex: 1 1 auto;
     1248}
     1249
     1250.rentbaik-cal__tags {
     1251        flex: none;
     1252        display: inline-flex;
     1253        gap: 3px;
     1254        margin-inline-start: auto;
     1255}
     1256
     1257.rentbaik-cal__tag {
     1258        display: inline-block;
     1259        padding-inline: 6px;
     1260        border-radius: 8px;
     1261        font-size: 10px;
     1262        font-weight: 600;
     1263        line-height: 16px;
     1264        white-space: nowrap;
     1265}
     1266
     1267.rentbaik-cal__tag--alert {
     1268        background: #b32d2e;
     1269        color: #fff;
     1270}
     1271
     1272.rentbaik-cal__tag--warn {
     1273        background: #f5e6ab;
     1274        color: #614a00;
     1275}
     1276
     1277.rentbaik-cal__tag--info {
     1278        background: #135e96;
     1279        color: #fff;
     1280}
     1281
     1282.rentbaik-cal__tag--muted {
     1283        background: rgba(0, 0, 0, 0.07);
     1284        color: #3c434a;
     1285}
     1286
     1287.rentbaik-cal__legend .rentbaik-cal__tag {
     1288        margin-inline-end: 2px;
     1289}
     1290
     1291.rentbaik-agenda__tags .rentbaik-cal__tag {
     1292        vertical-align: middle;
    11871293}
    11881294
  • rentbaik/trunk/assets/admin/admin.js

    r3790685 r3790901  
    148148                                } );
    149149                };
     150                // rest_url() already holds "?rest_route=" when permalinks are plain, so add parameters with "&" then.
     151                var sep = function ( url ) {
     152                        return url.indexOf( '?' ) === -1 ? '?' : '&';
     153                };
    150154                var open = function ( id, from, day ) {
    151155                        var back = window.location.href.split( '#' )[ 0 ];
    152156                        mark( id, '' );
    153                         load( 'booking-' + id, cfg.panelUrl + id + '?back=' + encodeURIComponent( back ), from, 'panelError', day ? function () {
     157                        load( 'booking-' + id, cfg.panelUrl + id + sep( cfg.panelUrl ) + 'back=' + encodeURIComponent( back ), from, 'panelError', day ? function () {
    154158                                // Opened from the day agenda: offer a way back to it.
    155159                                var button = document.createElement( 'button' );
    … …  
    167171                        var vehicle = new URLSearchParams( window.location.search ).get( 'vehicle' ) || '0';
    168172                        mark( 0, day );
    169                         load( 'day-' + day, cfg.dayUrl + day + '?vehicle=' + encodeURIComponent( vehicle ), from, 'dayError' );
     173                        load( 'day-' + day, cfg.dayUrl + day + sep( cfg.dayUrl ) + 'vehicle=' + encodeURIComponent( vehicle ), from, 'dayError' );
    170174                };
    171175
  • rentbaik/trunk/includes/Admin/CalendarView.php

    r3790685 r3790901  
    145145                $photos = Photos::for_bookings( array_map( 'intval', wp_list_pluck( $rows, 'id' ) ) );
    146146                $now    = Time::to_mysql( Time::now() );
     147                $today  = Time::local( Time::now() )->format( 'Y-m-d' );
    147148                $labels = BookingService::status_labels();
    148149                $pay    = BookingService::payment_state_labels();
    … …  
    180181                        $plate         = $unit ? (string) $unit['plate_number'] : '';
    181182                        $state         = BookingService::payment_state( $row );
     183                        $late_hours    = 'overdue' === $tone ? max( 1, (int) floor( ( time() - $return->getTimestamp() ) / HOUR_IN_SECONDS ) ) : 0;
     184                        $flags         = self::flags( $row, $status, $state, $start_day, Time::local( $return )->format( 'Y-m-d' ), $today, $late_hours );
    182185
    183186                        $title = implode(
    … …  
    192195                                                sprintf( __( 'Return: %s', 'rentbaik' ), Format::datetime( (string) $row['return_at'] ) ),
    193196                                                ( $labels[ $status ] ?? $status ) . ' · ' . ( $pay[ $state ] ?? $state ),
     197                                                implode( ' · ', wp_list_pluck( $flags, 'title' ) ),
    194198                                                $customer ? (string) $customer['phone'] : '',
    195199                                        )
    … …  
    214218                                'vehicle'    => $vehicle_label,
    215219                                'plate'      => $plate,
    216                                 'unpaid'     => 'paid' !== $state && ! in_array( $status, array( 'completed' ), true ),
     220                                'unpaid'     => isset( $flags['unpaid'] ),
     221                                'flags'      => $flags,
    217222                                'photo'      => isset( $photos[ (int) $row['id'] ] ) ? Photos::url( $photos[ (int) $row['id'] ], true ) : '',
    218223                                'title'      => $title,
    … …  
    225230                }
    226231                return $list;
     232        }
     233
     234        /**
     235         * Short labels on a booking bar: late, return or pick-up today, not fully paid, deposit not given back.
     236         *
     237         * @param array<string, mixed> $row        Booking row.
     238         * @param string               $status     Booking status.
     239         * @param string               $state      Payment state (unpaid, partial, paid).
     240         * @param string               $start_day  Local pick-up day (Y-m-d).
     241         * @param string               $return_day Local return day (Y-m-d).
     242         * @param string               $today      Local today (Y-m-d).
     243         * @param int                  $late_hours Hours past the return time (0 = not late).
     244         * @return array<string, array{label: string, title: string, tone: string, at: string}> Key => label, full text,
     245         *                                                                                      tone (alert, warn, info, muted)
     246         *                                                                                      and end of the bar (start, end).
     247         */
     248        public static function flags( array $row, string $status, string $state, string $start_day, string $return_day, string $today, int $late_hours ): array {
     249                $flags = array();
     250                if ( $late_hours > 0 ) {
     251                        $flags['late'] = array(
     252                                'label' => __( 'Late', 'rentbaik' ),
     253                                /* translators: %d: number of hours. */
     254                                'title' => sprintf( _n( 'Late by %d hour', 'Late by %d hours', $late_hours, 'rentbaik' ), $late_hours ),
     255                                'tone'  => 'alert',
     256                                'at'    => 'end',
     257                        );
     258                } elseif ( 'active' === $status && $return_day === $today ) {
     259                        $flags['return_today'] = array(
     260                                'label' => '↓ ' . __( 'Today', 'rentbaik' ),
     261                                'title' => __( 'Comes back today', 'rentbaik' ),
     262                                'tone'  => 'info',
     263                                'at'    => 'end',
     264                        );
     265                }
     266                if ( in_array( $status, array( 'pending', 'confirmed' ), true ) && $start_day === $today ) {
     267                        $flags['pickup_today'] = array(
     268                                'label' => '↑ ' . __( 'Today', 'rentbaik' ),
     269                                'title' => __( 'Picked up today', 'rentbaik' ),
     270                                'tone'  => 'info',
     271                                'at'    => 'start',
     272                        );
     273                }
     274                if ( (int) $row['total'] > 0 && 'paid' !== $state && ! in_array( $status, array( 'completed', 'cancelled', 'no_show' ), true ) ) {
     275                        $flags['unpaid'] = array(
     276                                'label' => 'partial' === $state ? __( 'Part paid', 'rentbaik' ) : __( 'Unpaid', 'rentbaik' ),
     277                                'title' => __( 'Not fully paid', 'rentbaik' ),
     278                                // Money to collect now is stronger than a future booking that is paid at pick-up.
     279                                'tone'  => in_array( $status, array( 'active', 'returned' ), true ) ? 'warn' : 'muted',
     280                                'at'    => 'end',
     281                        );
     282                }
     283                if ( in_array( $status, array( 'returned', 'completed' ), true ) && 'held' === (string) $row['deposit_status'] ) {
     284                        $flags['deposit'] = array(
     285                                'label' => __( 'Deposit', 'rentbaik' ),
     286                                'title' => __( 'Deposit not given back yet', 'rentbaik' ),
     287                                'tone'  => 'warn',
     288                                'at'    => 'end',
     289                        );
     290                }
     291                return $flags;
    227292        }
    228293
    … …  
    474539                        echo '<span class="rentbaik-cal__nounit">' . esc_html__( 'No unit', 'rentbaik' ) . '</span>';
    475540                }
    476                 if ( $segment['unpaid'] ) {
    477                         echo '<i class="rentbaik-cal__unpaid" title="' . esc_attr__( 'Not fully paid', 'rentbaik' ) . '"></i>';
     541                $tags = array();
     542                foreach ( (array) ( $segment['flags'] ?? array() ) as $key => $flag ) {
     543                        // Pick-up labels sit on the first piece of a bar that crosses weeks, the others on the last piece.
     544                        if ( ( 'start' === $flag['at'] && ! $segment['cont_in'] ) || ( 'end' === $flag['at'] && ! $segment['cont_out'] ) ) {
     545                                $tags[ $key ] = $flag;
     546                        }
     547                }
     548                if ( ! empty( $tags ) ) {
     549                        echo '<span class="rentbaik-cal__tags">';
     550                        foreach ( $tags as $key => $flag ) {
     551                                echo '<span class="rentbaik-cal__tag rentbaik-cal__tag--' . esc_attr( $flag['tone'] ) . ' rentbaik-cal__tag--' . esc_attr( $key ) . '" title="' . esc_attr( $flag['title'] ) . '">' . esc_html( $flag['label'] ) . '</span>';
     552                        }
     553                        echo '</span>';
    478554                }
    479555                if ( $grip_b ) {
    … …  
    708784                        echo '<li><i class="rentbaik-cal__swatch rentbaik-cal__swatch--' . esc_attr( $key ) . '" aria-hidden="true"></i>' . esc_html( $label ) . '</li>';
    709785                }
    710                 echo '<li><i class="rentbaik-cal__unpaid" aria-hidden="true"></i>' . esc_html__( 'Not fully paid', 'rentbaik' ) . '</li>';
     786                foreach ( array(
     787                        array( 'alert', __( 'Late', 'rentbaik' ), __( 'Late return', 'rentbaik' ) ),
     788                        array( 'info', '↑↓ ' . __( 'Today', 'rentbaik' ), __( 'Picked up or comes back today', 'rentbaik' ) ),
     789                        array( 'warn', __( 'Unpaid', 'rentbaik' ), __( 'Not fully paid', 'rentbaik' ) ),
     790                        array( 'warn', __( 'Deposit', 'rentbaik' ), __( 'Deposit not given back yet', 'rentbaik' ) ),
     791                ) as $tag ) {
     792                        echo '<li><span class="rentbaik-cal__tag rentbaik-cal__tag--' . esc_attr( $tag[0] ) . '" aria-hidden="true">' . esc_html( $tag[1] ) . '</span>' . esc_html( $tag[2] ) . '</li>';
     793                }
    711794                if ( $fleet > 0 ) {
    712795                        echo '<li><span class="rentbaik-cal__occ">' . esc_html( '1/' . $fleet ) . '</span> ' . esc_html__( 'vehicles out / fleet', 'rentbaik' ) . '</li>';
  • rentbaik/trunk/includes/Admin/DashboardPage.php

    r3790685 r3790901  
    215215
    216216                echo '<div class="rentbaik-grid">';
     217                SitePages::render_card();
    217218                if ( $done < count( $steps ) ) {
    218219                        self::render_setup( $steps, $done );
  • rentbaik/trunk/includes/Admin/DayAgenda.php

    r3790685 r3790901  
    229229                echo '<span class="rentbaik-agenda__tags">';
    230230                View::print_status( $status, $labels[ $status ] ?? $status );
    231                 if ( $row['unpaid'] ) {
    232                         echo ' <i class="rentbaik-cal__unpaid" title="' . esc_attr__( 'Not fully paid', 'rentbaik' ) . '"></i><span class="screen-reader-text">' . esc_html__( 'Not fully paid', 'rentbaik' ) . '</span>';
     231                foreach ( (array) ( $row['flags'] ?? array() ) as $key => $flag ) {
     232                        echo ' <span class="rentbaik-cal__tag rentbaik-cal__tag--' . esc_attr( $flag['tone'] ) . ' rentbaik-cal__tag--' . esc_attr( $key ) . '" title="' . esc_attr( $flag['title'] ) . '">' . esc_html( $flag['label'] ) . '</span>';
    233233                }
    234234                echo '</span></button></li>';
  • rentbaik/trunk/includes/Customers/Photos.php

    r3790685 r3790901  
    263263         * @return void
    264264         */
    265         private static function make_thumb( string $path ): void {
     265        public static function make_thumb( string $path ): void {
    266266                $editor = wp_get_image_editor( $path );
    267267                if ( is_wp_error( $editor ) ) {
    … …  
    278278         * @return string
    279279         */
    280         private static function thumb_path( string $path ): string {
     280        public static function thumb_path( string $path ): string {
    281281                $info = pathinfo( $path );
    282282                return $info['dirname'] . '/' . $info['filename'] . '-thumb.' . ( $info['extension'] ?? 'jpg' );
  • rentbaik/trunk/includes/Setup/SampleData.php

    r3790685 r3790901  
    11<?php
    22/**
    3  * Sample data: example vehicles, units, extras and bookings for a first look, removable in one click.
     3 * Sample data: example vehicles, units, extras and a month of bookings for a first look, removable in one click.
    44 *
    55 * @package Rentbaik
    … …  
    88namespace Rentbaik\Setup;
    99
     10use DateTimeImmutable;
    1011use Rentbaik\Availability\BlockRepository;
    1112use Rentbaik\Booking\BookingRepository;
    1213use Rentbaik\Booking\BookingService;
    1314use Rentbaik\Customers\CustomerRepository;
     15use Rentbaik\Customers\Photos;
    1416use Rentbaik\Database\Repository;
    1517use Rentbaik\Fleet\UnitRepository;
    … …  
    3638
    3739        /**
    38          * Post meta that marks a sample vehicle.
     40         * Post meta that marks a sample vehicle (and sample images in the Media Library).
    3941         */
    4042        public const META = '_rentbaik_sample';
    … …  
    8890
    8991        /**
     92         * Payment methods paid before the pick-up (the others are paid at the counter).
     93         */
     94        private const PREPAID = array( 'bank_transfer', 'woocommerce', 'qris' );
     95
     96        /**
     97         * Media Library images made in this request: asset name => attachment ID.
     98         *
     99         * @var array<string, int>
     100         */
     101        private static $media = array();
     102
     103        /**
    90104         * Saved record of the sample data.
    91105         *
    … …  
    101115                        'bookings'  => array(),
    102116                        'customers' => array(),
     117                        'media'     => array(),
     118                        'blocks'    => array(),
    103119                );
    104120                foreach ( $defaults as $key => $empty ) {
    … …  
    144160                $types    = array_keys( Vehicle::enabled_types() );
    145161                $plate    = 100;
    146                 $first    = array();
     162                $models   = array();
     163                $extras   = array();
    147164
    148165                Emails::mute( true );
    … …  
    157174                                }
    158175                                $data['vehicles'][] = $vehicle_id;
    159                                 if ( ! isset( $first[ $definition['type'] ] ) ) {
    160                                         $first[ $definition['type'] ] = $vehicle_id;
    161                                 }
     176                                self::add_vehicle_images( $vehicle_id, $definition );
     177                                $models[ $definition['art'] ] = $vehicle_id;
    162178                                for ( $i = 0; $i < $definition['units']; $i++ ) {
    163179                                        $unit_id = self::create_unit( $vehicle_id, $location, $plate, $definition['colors'][ $i % count( $definition['colors'] ) ] );
    … …  
    165181                                        if ( $unit_id > 0 ) {
    166182                                                $data['units'][] = $unit_id;
     183                                                // A photo of this unit in its own colour.
     184                                                $art_color = $definition['art_colors'][ $i % count( $definition['art_colors'] ) ];
     185                                                $photo     = self::media( $definition['art'] . '-' . $art_color, (string) $definition['title'] );
     186                                                if ( $photo > 0 ) {
     187                                                        UnitRepository::save( $unit_id, array( 'photo_id' => $photo ) );
     188                                                }
    167189                                        }
    168190                                }
    169191                        }
    170192
    171                         foreach ( self::extra_definitions() as $extra ) {
     193                        foreach ( self::extra_definitions() as $key => $extra ) {
    172194                                if ( empty( array_intersect( $extra['vehicle_types'], $types ) ) ) {
    173195                                        continue;
    … …  
    190212                                if ( $extra_id > 0 ) {
    191213                                        $data['extras'][] = $extra_id;
     214                                        $extras[ $key ]   = $extra_id;
    192215                                }
    193216                        }
    194217
    195218                        // Save what exists so far, so a failure below still leaves everything removable.
     219                        $data['media'] = array_values( array_unique( array_merge( $data['media'], array_values( self::$media ) ) ) );
    196220                        update_option( self::OPTION, $data, false );
    197221
    198                         $vehicles = array_values( $first );
    199                         if ( ! empty( $vehicles ) ) {
    200                                 $data = self::create_bookings( $data, $vehicles, $location );
     222                        if ( ! empty( $models ) ) {
     223                                $data = self::create_bookings( $data, $models, $extras, $location );
     224                                update_option( self::OPTION, $data, false );
     225                                $data = self::add_service_block( $data, $models );
    201226                        }
    202227                } finally {
    … …  
    204229                }
    205230
     231                $data['media'] = array_values( array_unique( array_merge( $data['media'], array_values( self::$media ) ) ) );
    206232                update_option( self::OPTION, $data, false );
    207233                Repository::bump_data_version();
    … …  
    222248        public static function remove(): void {
    223249                $data = self::data();
     250
     251                foreach ( $data['blocks'] as $block_id ) {
     252                        BlockRepository::delete( $block_id );
     253                }
    224254
    225255                foreach ( $data['bookings'] as $booking_id ) {
    … …  
    233263                foreach ( $data['customers'] as $customer_id ) {
    234264                        if ( empty( BookingRepository::for_customer( $customer_id ) ) ) {
     265                                // Sample photos and payment proofs in the private folder.
     266                                Photos::remove_for_customer( $customer_id );
    235267                                CustomerRepository::delete( $customer_id );
    236268                        }
    … …  
    270302                }
    271303
     304                foreach ( $data['media'] as $attachment_id ) {
     305                        if ( '1' === (string) get_post_meta( $attachment_id, self::META, true ) ) {
     306                                wp_delete_attachment( $attachment_id, true );
     307                        }
     308                }
     309
    272310                delete_option( self::OPTION );
    273311                Repository::bump_data_version();
     312        }
     313
     314        /**
     315         * Path of a sample illustration shipped with the plugin.
     316         *
     317         * @param string $name Asset name without extension.
     318         * @return string '' when missing.
     319         */
     320        private static function asset( string $name ): string {
     321                $path = RENTBAIK_PATH . 'assets/sample/' . sanitize_file_name( $name ) . '.png';
     322                return file_exists( $path ) ? $path : '';
     323        }
     324
     325        /**
     326         * Media Library image from a sample illustration (made once per request and reused).
     327         *
     328         * @param string $name  Asset name.
     329         * @param string $title Image title.
     330         * @return int Attachment ID (0 when it could not be made).
     331         */
     332        private static function media( string $name, string $title ): int {
     333                if ( isset( self::$media[ $name ] ) ) {
     334                        return self::$media[ $name ];
     335                }
     336                $source = self::asset( $name );
     337                if ( '' === $source ) {
     338                        return 0;
     339                }
     340                $upload = wp_upload_bits( 'rentbaik-sample-' . $name . '.png', null, (string) Photos::filesystem()->get_contents( $source ) );
     341                if ( ! empty( $upload['error'] ) ) {
     342                        return 0;
     343                }
     344                $attachment_id = wp_insert_attachment(
     345                        array(
     346                                'post_mime_type' => 'image/png',
     347                                'post_title'     => $title,
     348                                'post_status'    => 'inherit',
     349                        ),
     350                        $upload['file']
     351                );
     352                if ( ! $attachment_id || is_wp_error( $attachment_id ) ) {
     353                        return 0;
     354                }
     355                require_once ABSPATH . 'wp-admin/includes/image.php';
     356                wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) );
     357                update_post_meta( $attachment_id, self::META, '1' );
     358                update_post_meta( $attachment_id, '_wp_attachment_image_alt', $title );
     359                self::$media[ $name ] = (int) $attachment_id;
     360                return (int) $attachment_id;
     361        }
     362
     363        /**
     364         * Featured image and gallery of a sample vehicle: its colours and the gear that comes with it.
     365         *
     366         * @param int                  $vehicle_id Vehicle post ID.
     367         * @param array<string, mixed> $definition Vehicle definition.
     368         * @return void
     369         */
     370        private static function add_vehicle_images( int $vehicle_id, array $definition ): void {
     371                $title    = (string) $definition['title'];
     372                $featured = self::media( $definition['art'] . '-' . $definition['art_colors'][0], $title );
     373                if ( $featured > 0 ) {
     374                        set_post_thumbnail( $vehicle_id, $featured );
     375                }
     376                $gallery = array();
     377                foreach ( array_slice( $definition['art_colors'], 1 ) as $color ) {
     378                        $gallery[] = self::media( $definition['art'] . '-' . $color, $title );
     379                }
     380                $gear      = array(
     381                        'motorcycle' => 'gear-scooter',
     382                        'car'        => 'gear-car',
     383                );
     384                $gallery[] = self::media( $gear[ $definition['type'] ] ?? 'gear-bike', __( 'Included gear (sample)', 'rentbaik' ) );
     385                $gallery   = array_filter( $gallery );
     386                if ( ! empty( $gallery ) ) {
     387                        update_post_meta( $vehicle_id, '_rentbaik_gallery', implode( ',', $gallery ) );
     388                }
     389        }
     390
     391        /**
     392         * Copy a sample illustration into the private folder.
     393         *
     394         * @param string $name   Asset name.
     395         * @param string $prefix File name prefix (photo, proof).
     396         * @return string File name in the private folder ('' when missing).
     397         */
     398        private static function private_copy( string $name, string $prefix ): string {
     399                $source = self::asset( $name );
     400                if ( '' === $source ) {
     401                        return '';
     402                }
     403                $file = $prefix . '-' . strtolower( wp_generate_password( 24, false ) ) . '.png';
     404                $path = Photos::dir() . '/' . $file;
     405                if ( ! Photos::filesystem()->put_contents( $path, (string) Photos::filesystem()->get_contents( $source ), FS_CHMOD_FILE ) ) {
     406                        return '';
     407                }
     408                Photos::make_thumb( $path );
     409                return $file;
     410        }
     411
     412        /**
     413         * Customer photo taken at the hand-over and the transfer receipt a customer uploaded, as on a real booking.
     414         *
     415         * @param int                  $booking_id Booking ID.
     416         * @param array<string, mixed> $plan       Plan.
     417         * @return void
     418         */
     419        private static function add_booking_files( int $booking_id, array $plan ): void {
     420                $booking = BookingRepository::find( $booking_id );
     421                if ( ! $booking ) {
     422                        return;
     423                }
     424                $add = static function ( string $asset, string $prefix, string $type, string $status, string $when ) use ( $booking_id, $booking ): void {
     425                        $file = self::private_copy( $asset, $prefix );
     426                        if ( '' !== $file ) {
     427                                Photos::insert(
     428                                        array(
     429                                                'customer_id' => (int) $booking['customer_id'],
     430                                                'booking_id'  => $booking_id,
     431                                                'doc_type'    => $type,
     432                                                'file_name'   => $file,
     433                                                'mime_type'   => 'image/png',
     434                                                'status'      => $status,
     435                                                'created_at'  => $when,
     436                                        ),
     437                                        array( '%d', '%d', '%s', '%s', '%s', '%s', '%s' )
     438                                );
     439                        }
     440                };
     441                if ( ! empty( $plan['proof'] ) ) {
     442                        $add( 'receipt-sample', 'proof', 'payment_proof', (string) $plan['proof'], self::created_at( $booking ) );
     443                }
     444                if ( ! empty( $plan['photo'] ) && in_array( $plan['status'], array( 'active', 'returned', 'completed' ), true ) ) {
     445                        $add( 'customer-' . (int) $plan['photo'], 'photo', 'photo', 'valid', (string) $booking['pickup_at'] );
     446                }
    274447        }
    275448
    … …  
    283456                return array(
    284457                        array(
    285                                 'type'    => 'motorcycle',
    286                                 'title'   => __( 'Automatic scooter 125 cc (sample)', 'rentbaik' ),
    287                                 'content' => __( 'Easy automatic scooter for the city. Two helmets included.', 'rentbaik' ) . "\n\n" . $note,
    288                                 'specs'   => array(
     458                                'type'       => 'motorcycle',
     459                                'art'        => 'scooter-125',
     460                                'art_colors' => array( 'white', 'red', 'black' ),
     461                                'title'      => __( 'Automatic scooter 125 cc (sample)', 'rentbaik' ),
     462                                'content'    => __( 'Easy automatic scooter for the city. Two helmets included.', 'rentbaik' ) . "\n\n" . $note,
     463                                'specs'      => array(
    289464                                        'engine_cc'    => 125,
    290465                                        'transmission' => 'automatic',
    291466                                        'helmets'      => 2,
    292467                                ),
    293                                 'rates'   => array(
     468                                'rates'      => array(
    294469                                        'hour' => 4,
    295470                                        'day'  => 15,
    296471                                        'week' => 90,
    297472                                ),
    298                                 'deposit' => 50,
    299                                 'units'   => 3,
    300                                 'colors'  => array( __( 'White', 'rentbaik' ), __( 'Red', 'rentbaik' ), __( 'Black', 'rentbaik' ) ),
     473                                'deposit'    => 50,
     474                                'units'      => 3,
     475                                'colors'     => array( __( 'White', 'rentbaik' ), __( 'Red', 'rentbaik' ), __( 'Black', 'rentbaik' ) ),
    301476                        ),
    302477                        array(
    303                                 'type'    => 'motorcycle',
    304                                 'title'   => __( 'Comfort scooter 160 cc (sample)', 'rentbaik' ),
    305                                 'content' => __( 'Bigger scooter with a large seat and storage, good for longer trips.', 'rentbaik' ) . "\n\n" . $note,
    306                                 'specs'   => array(
     478                                'type'       => 'motorcycle',
     479                                'art'        => 'scooter-160',
     480                                'art_colors' => array( 'grey', 'blue' ),
     481                                'title'      => __( 'Comfort scooter 160 cc (sample)', 'rentbaik' ),
     482                                'content'    => __( 'Bigger scooter with a large seat and storage, good for longer trips.', 'rentbaik' ) . "\n\n" . $note,
     483                                'specs'      => array(
    307484                                        'engine_cc'    => 160,
    308485                                        'transmission' => 'automatic',
    309486                                        'helmets'      => 2,
    310487                                ),
    311                                 'rates'   => array(
     488                                'rates'      => array(
    312489                                        'day'  => 22,
    313490                                        'week' => 130,
    314491                                ),
    315                                 'deposit' => 80,
    316                                 'units'   => 2,
    317                                 'colors'  => array( __( 'Grey', 'rentbaik' ), __( 'Blue', 'rentbaik' ) ),
     492                                'deposit'    => 80,
     493                                'units'      => 2,
     494                                'colors'     => array( __( 'Grey', 'rentbaik' ), __( 'Blue', 'rentbaik' ) ),
    318495                        ),
    319496                        array(
    320                                 'type'    => 'car',
    321                                 'title'   => __( 'Compact car, automatic (sample)', 'rentbaik' ),
    322                                 'content' => __( 'Small and easy to park. Air conditioning, five seats.', 'rentbaik' ) . "\n\n" . $note,
    323                                 'specs'   => array(
     497                                'type'       => 'car',
     498                                'art'        => 'car-compact',
     499                                'art_colors' => array( 'silver', 'white' ),
     500                                'title'      => __( 'Compact car, automatic (sample)', 'rentbaik' ),
     501                                'content'    => __( 'Small and easy to park. Air conditioning, five seats.', 'rentbaik' ) . "\n\n" . $note,
     502                                'specs'      => array(
    324503                                        'seats'        => 5,
    325504                                        'doors'        => 5,
    … …  
    329508                                        'luggage'      => 2,
    330509                                ),
    331                                 'rates'   => array(
     510                                'rates'      => array(
    332511                                        'day'  => 45,
    333512                                        'week' => 270,
    334513                                ),
    335                                 'deposit' => 200,
    336                                 'units'   => 2,
    337                                 'colors'  => array( __( 'Silver', 'rentbaik' ), __( 'White', 'rentbaik' ) ),
     514                                'deposit'    => 200,
     515                                'units'      => 2,
     516                                'colors'     => array( __( 'Silver', 'rentbaik' ), __( 'White', 'rentbaik' ) ),
    338517                        ),
    339518                        array(
    340                                 'type'    => 'car',
    341                                 'title'   => __( 'Family car, 7 seats (sample)', 'rentbaik' ),
    342                                 'content' => __( 'Room for the whole family and their bags.', 'rentbaik' ) . "\n\n" . $note,
    343                                 'specs'   => array(
     519                                'type'       => 'car',
     520                                'art'        => 'car-family',
     521                                'art_colors' => array( 'black' ),
     522                                'title'      => __( 'Family car, 7 seats (sample)', 'rentbaik' ),
     523                                'content'    => __( 'Room for the whole family and their bags.', 'rentbaik' ) . "\n\n" . $note,
     524                                'specs'      => array(
    344525                                        'seats'        => 7,
    345526                                        'doors'        => 5,
    … …  
    349530                                        'luggage'      => 3,
    350531                                ),
    351                                 'rates'   => array(
     532                                'rates'      => array(
    352533                                        'day'  => 65,
    353534                                        'week' => 390,
    354535                                ),
    355                                 'deposit' => 300,
    356                                 'units'   => 1,
    357                                 'colors'  => array( __( 'Black', 'rentbaik' ) ),
     536                                'deposit'    => 300,
     537                                'units'      => 1,
     538                                'colors'     => array( __( 'Black', 'rentbaik' ) ),
    358539                        ),
    359540                        array(
    360                                 'type'    => 'bicycle',
    361                                 'title'   => __( 'City bike (sample)', 'rentbaik' ),
    362                                 'content' => __( 'Comfortable bike with a basket and a lock.', 'rentbaik' ) . "\n\n" . $note,
    363                                 'specs'   => array(
     541                                'type'       => 'bicycle',
     542                                'art'        => 'bike-city',
     543                                'art_colors' => array( 'green' ),
     544                                'title'      => __( 'City bike (sample)', 'rentbaik' ),
     545                                'content'    => __( 'Comfortable bike with a basket and a lock.', 'rentbaik' ) . "\n\n" . $note,
     546                                'specs'      => array(
    364547                                        'frame_size' => 'M',
    365548                                        'gears'      => 7,
    366549                                ),
    367                                 'rates'   => array(
     550                                'rates'      => array(
    368551                                        'hour' => 3,
    369552                                        'day'  => 10,
    370553                                ),
    371                                 'deposit' => 0,
    372                                 'units'   => 2,
    373                                 'colors'  => array( __( 'Green', 'rentbaik' ) ),
     554                                'deposit'    => 0,
     555                                'units'      => 2,
     556                                'colors'     => array( __( 'Green', 'rentbaik' ) ),
    374557                        ),
    375558                        array(
    376                                 'type'    => 'ebike',
    377                                 'title'   => __( 'Electric bike (sample)', 'rentbaik' ),
    378                                 'content' => __( 'Pedal assist up to 60 km per charge.', 'rentbaik' ) . "\n\n" . $note,
    379                                 'specs'   => array(
     559                                'type'       => 'ebike',
     560                                'art'        => 'ebike',
     561                                'art_colors' => array( 'teal' ),
     562                                'title'      => __( 'Electric bike (sample)', 'rentbaik' ),
     563                                'content'    => __( 'Pedal assist up to 60 km per charge.', 'rentbaik' ) . "\n\n" . $note,
     564                                'specs'      => array(
    380565                                        'frame_size' => 'M',
    381566                                        'range_km'   => 60,
    382567                                ),
    383                                 'rates'   => array(
     568                                'rates'      => array(
    384569                                        'hour' => 6,
    385570                                        'day'  => 25,
    386571                                ),
    387                                 'deposit' => 100,
    388                                 'units'   => 2,
    389                                 'colors'  => array( __( 'White', 'rentbaik' ) ),
    390                         ),
    391                 );
    392         }
    393 
    394         /**
    395          * Sample extras.
    396          *
    397          * @return array<int, array<string, mixed>>
     572                                'deposit'    => 100,
     573                                'units'      => 2,
     574                                'colors'     => array( __( 'White', 'rentbaik' ) ),
     575                        ),
     576                );
     577        }
     578
     579        /**
     580         * Sample extras, by key.
     581         *
     582         * @return array<string, array<string, mixed>>
    398583         */
    399584        private static function extra_definitions(): array {
    400585                return array(
    401                         array(
     586                        'helmet'     => array(
    402587                                'name'          => __( 'Extra helmet (sample)', 'rentbaik' ),
    403588                                'description'   => __( 'Clean helmet for a passenger.', 'rentbaik' ),
    … …  
    407592                                'vehicle_types' => array( 'motorcycle', 'bicycle', 'ebike' ),
    408593                        ),
    409                         array(
     594                        'poncho'     => array(
    410595                                'name'          => __( 'Rain poncho (sample)', 'rentbaik' ),
    411596                                'description'   => __( 'Yours to keep.', 'rentbaik' ),
    … …  
    415600                                'vehicle_types' => array( 'motorcycle', 'bicycle', 'ebike' ),
    416601                        ),
    417                         array(
     602                        'child_seat' => array(
    418603                                'name'          => __( 'Child seat (sample)', 'rentbaik' ),
    419604                                'description'   => __( 'For children from 9 to 18 kg.', 'rentbaik' ),
    … …  
    490675
    491676        /**
    492          * Three sample bookings: one picked up today, one returning today and one waiting for confirmation.
     677         * A month of sample bookings in every common situation, around today.
     678         *
     679         * Keys: model (vehicle art), offset (days from today), hour, days, status, source, method (payment), paid
     680         * (none, part, full, refunded), part (share paid), deposit (none, held, returned), proof (pending, valid),
     681         * photo (customer portrait 1-8), extra (extra key), no_unit (taken although no unit is free).
     682         *
     683         * @return array<int, array<string, mixed>>
     684         */
     685        private static function plans(): array {
     686                $keys = array( 'first_name', 'country', 'phone', 'model', 'offset', 'hour', 'days', 'status', 'source', 'method', 'paid', 'part', 'deposit', 'proof', 'photo', 'extra', 'no_unit' );
     687                $rows = array(
     688                        // Finished last week: paid in cash, deposit given back.
     689                        array( 'Emma', 'GB', '15550106', 'scooter-125', -7, 10, 2, 'completed', 'whatsapp', 'cash', 'full', 0, 'returned', '', 1, 'helmet', false ),
     690                        // Finished: paid by card at the counter.
     691                        array( 'Olivia', 'US', '15550108', 'car-compact', -6, 9, 2, 'completed', 'web', 'card', 'full', 0, 'returned', '', 4, 'child_seat', false ),
     692                        // Returned two days ago, the deposit is still to be given back.
     693                        array( 'Lucas', 'FR', '15550103', 'scooter-160', -4, 9, 2, 'returned', 'admin', 'cash', 'full', 0, 'held', '', 3, '', false ),
     694                        // Cancelled by the customer: the transfer was refunded.
     695                        array( 'Liam', 'NZ', '15550107', 'bike-city', -3, 10, 1, 'cancelled', 'web', 'bank_transfer', 'refunded', 0.5, 'none', '', 0, '', false ),
     696                        // Did not come.
     697                        array( 'Wei', 'CN', '15550109', 'ebike', -2, 11, 1, 'no_show', 'web', 'pay_at_pickup', 'none', 0, 'none', '', 0, '', false ),
     698                        // Should have come back yesterday: late, deposit held, rent not paid yet.
     699                        array( 'Noah', 'US', '15550105', 'car-compact', -3, 9, 2, 'active', 'admin', 'pay_at_pickup', 'none', 0, 'held', '', 7, '', false ),
     700                        // On rent, comes back today; half paid with a QR code.
     701                        array( 'Maria', 'DE', '15550101', 'scooter-125', -2, 16, 2, 'active', 'whatsapp', 'qris', 'part', 0.5, 'held', '', 2, 'poncho', false ),
     702                        // Long rental under way, paid in advance by bank transfer.
     703                        array( 'Sofia', 'IT', '15550104', 'car-family', -5, 10, 12, 'active', 'web', 'bank_transfer', 'full', 0, 'held', 'valid', 5, '', false ),
     704                        // Picks up today, pays at the counter.
     705                        array( 'Alex', 'AU', '15550100', 'scooter-125', 0, 10, 3, 'confirmed', 'web', 'pay_at_pickup', 'none', 0, 'none', '', 0, '', false ),
     706                        // Picks up this afternoon, already paid online.
     707                        array( 'Aisha', 'AE', '15550110', 'scooter-160', 0, 15, 2, 'confirmed', 'web', 'woocommerce', 'full', 0, 'none', '', 0, '', false ),
     708                        // Taken by phone although the only family car is out: needs a unit before pick-up.
     709                        array( 'Omar', 'EG', '15550111', 'car-family', 2, 10, 3, 'confirmed', 'admin', 'pay_at_pickup', 'none', 0, 'none', '', 0, '', true ),
     710                        // Waiting for confirmation, transfer receipt uploaded but not checked yet.
     711                        array( 'Kenji', 'JP', '15550102', 'ebike', 3, 11, 4, 'pending', 'web', 'bank_transfer', 'none', 0, 'none', 'pending', 0, '', false ),
     712                        // Confirmed with a part payment by transfer.
     713                        array( 'Priya', 'IN', '15550112', 'car-compact', 6, 10, 5, 'confirmed', 'web', 'bank_transfer', 'part', 0.3, 'none', 'valid', 0, 'child_seat', false ),
     714                        // A week by WhatsApp, pays at pick-up.
     715                        array( 'Mateo', 'MX', '15550113', 'scooter-125', 9, 9, 7, 'confirmed', 'whatsapp', 'pay_at_pickup', 'none', 0, 'none', '', 0, 'helmet', false ),
     716                        // Waiting for confirmation from WhatsApp.
     717                        array( 'Putri', 'ID', '15550114', 'bike-city', 12, 8, 1, 'pending', 'whatsapp', 'pay_at_pickup', 'none', 0, 'none', '', 0, '', false ),
     718                        // Paid online for later this month.
     719                        array( 'Lena', 'SE', '15550115', 'car-compact', 16, 10, 4, 'confirmed', 'web', 'woocommerce', 'full', 0, 'none', '', 0, '', false ),
     720                        // A returning customer books again.
     721                        array( 'Emma', 'GB', '15550106', 'scooter-160', 19, 10, 3, 'pending', 'whatsapp', 'pay_at_pickup', 'none', 0, 'none', '', 0, '', false ),
     722                );
     723                return array_map(
     724                        static function ( array $row ) use ( $keys ): array {
     725                                return array_combine( $keys, $row );
     726                        },
     727                        $rows
     728                );
     729        }
     730
     731        /**
     732         * The planned model, or a similar one when its vehicle type is switched off (a shop with only cars still gets
     733         * every situation).
     734         *
     735         * @param string             $model  Vehicle art.
     736         * @param array<string, int> $models Created models: art => vehicle ID.
     737         * @return string '' when no model exists.
     738         */
     739        private static function model_for( string $model, array $models ): string {
     740                $similar = array(
     741                        'scooter-125' => array( 'scooter-160', 'bike-city', 'ebike' ),
     742                        'scooter-160' => array( 'scooter-125', 'ebike', 'bike-city' ),
     743                        'car-compact' => array( 'car-family', 'scooter-160', 'scooter-125' ),
     744                        'car-family'  => array( 'car-compact', 'scooter-160', 'scooter-125' ),
     745                        'bike-city'   => array( 'ebike', 'scooter-125', 'scooter-160' ),
     746                        'ebike'       => array( 'bike-city', 'scooter-160', 'scooter-125' ),
     747                );
     748                foreach ( array_merge( array( $model ), $similar[ $model ] ?? array(), array_keys( $models ) ) as $candidate ) {
     749                        if ( isset( $models[ $candidate ] ) ) {
     750                                return $candidate;
     751                        }
     752                }
     753                return '';
     754        }
     755
     756        /**
     757         * Days from today for a plan, kept inside the current month where its situation allows it.
     758         *
     759         * @param array<string, mixed> $plan Plan.
     760         * @return int
     761         */
     762        private static function offset_in_month( array $plan ): int {
     763                $today  = new DateTimeImmutable( 'today', wp_timezone() );
     764                $offset = (int) $plan['offset'];
     765                $days   = (int) $plan['days'];
     766                if ( $offset > 0 ) {
     767                        // Later this month: keep the whole rental before the end of the month when there is room.
     768                        $left = (int) $today->format( 't' ) - (int) $today->format( 'j' );
     769                        return max( 1, min( $offset, $left - $days ) );
     770                }
     771                if ( $offset + $days <= 0 ) {
     772                        // Already over: as early as needed, but not before the first day of the month when it fits.
     773                        $first = 1 - (int) $today->format( 'j' );
     774                        return max( $offset, $first ) + $days <= 0 ? max( $offset, $first ) : $offset;
     775                }
     776                return $offset;
     777        }
     778
     779        /**
     780         * Create the sample bookings and bring each to the situation of its plan.
    493781         *
    494782         * @param array<string, int[]> $data        Sample data record.
    495          * @param int[]                $vehicles    Sample vehicle IDs to book.
     783         * @param array<string, int>   $models      Vehicle art => vehicle ID.
     784         * @param array<string, int>   $extras      Extra key => extra ID.
    496785         * @param int                  $location_id Location ID.
    497786         * @return array<string, int[]>
    498787         */
    499         private static function create_bookings( array $data, array $vehicles, int $location_id ): array {
    500                 $plans = array(
    501                         array(
    502                                 'first_name' => 'Alex',
    503                                 'country'    => 'AU',
    504                                 'phone'      => '15550100',
    505                                 'offset'     => 0,
    506                                 'hour'       => 10,
    507                                 'days'       => 3,
    508                                 'status'     => 'confirmed',
    509                                 'source'     => 'web',
    510                         ),
    511                         array(
    512                                 'first_name' => 'Maria',
    513                                 'country'    => 'DE',
    514                                 'phone'      => '15550101',
    515                                 'offset'     => -2,
    516                                 'hour'       => 16,
    517                                 'days'       => 2,
    518                                 'status'     => 'active',
    519                                 'source'     => 'whatsapp',
    520                         ),
    521                         array(
    522                                 'first_name' => 'Kenji',
    523                                 'country'    => 'JP',
    524                                 'phone'      => '15550102',
    525                                 'offset'     => 3,
    526                                 'hour'       => 11,
    527                                 'days'       => 4,
    528                                 'status'     => 'pending',
    529                                 'source'     => 'web',
    530                         ),
    531                 );
    532 
    533                 foreach ( $plans as $index => $plan ) {
    534                         $vehicle_id = $vehicles[ $index % count( $vehicles ) ];
     788        private static function create_bookings( array $data, array $models, array $extras, int $location_id ): array {
     789                foreach ( self::plans() as $plan ) {
     790                        $plan['model'] = self::model_for( (string) $plan['model'], $models );
     791                        if ( '' === $plan['model'] ) {
     792                                continue;
     793                        }
     794                        $offset     = self::offset_in_month( $plan );
    535795                        $booking_id = 0;
    536                         // Move a day later when the location is closed on the planned day.
     796                        // Move a day later when the location is closed on the planned day or no unit is free.
    537797                        for ( $shift = 0; $shift < 7 && $booking_id <= 0; $shift++ ) {
    538                                 $pickup     = Time::local_midnight( $plan['offset'] + $shift )->modify( '+' . $plan['hour'] . ' hours' );
    539                                 $result     = BookingService::create(
    540                                         array(
    541                                                 'vehicle_id'     => $vehicle_id,
    542                                                 'pickup'         => $pickup,
    543                                                 'return'         => $pickup->modify( '+' . $plan['days'] . ' days' ),
    544                                                 'location_id'    => $location_id,
    545                                                 'customer'       => array(
    546                                                         'first_name' => $plan['first_name'],
    547                                                         'last_name'  => __( 'Sample', 'rentbaik' ),
    548                                                         'phone'      => $plan['phone'],
    549                                                         'country'    => $plan['country'],
    550                                                         'notes'      => __( 'Sample data', 'rentbaik' ),
    551                                                 ),
    552                                                 'status'         => 'pending' === $plan['status'] ? 'pending' : 'confirmed',
    553                                                 'source'         => $plan['source'],
    554                                                 'payment_method' => 'pay_at_pickup',
    555                                                 'internal_note'  => __( 'Sample booking. It is removed together with the sample data.', 'rentbaik' ),
    556                                                 'allow_past'     => true,
    557                                         )
     798                                $pickup = Time::local_midnight( $offset + $shift )->modify( '+' . $plan['hour'] . ' hours' );
     799                                $input  = array(
     800                                        'vehicle_id'       => $models[ $plan['model'] ],
     801                                        'pickup'           => $pickup,
     802                                        'return'           => $pickup->modify( '+' . $plan['days'] . ' days' ),
     803                                        'location_id'      => $location_id,
     804                                        'customer'         => array(
     805                                                'first_name' => $plan['first_name'],
     806                                                'last_name'  => __( 'Sample', 'rentbaik' ),
     807                                                'phone'      => $plan['phone'],
     808                                                'country'    => $plan['country'],
     809                                                'notes'      => __( 'Sample data', 'rentbaik' ),
     810                                        ),
     811                                        'status'           => 'pending' === $plan['status'] ? 'pending' : 'confirmed',
     812                                        'source'           => $plan['source'],
     813                                        'payment_method'   => $plan['method'],
     814                                        'internal_note'    => __( 'Sample booking. It is removed together with the sample data.', 'rentbaik' ),
     815                                        'allow_past'       => true,
     816                                        'allow_unassigned' => ! empty( $plan['no_unit'] ),
    558817                                );
     818                                if ( ! empty( $plan['extra'] ) && isset( $extras[ $plan['extra'] ] ) ) {
     819                                        $input['extras'] = array( $extras[ $plan['extra'] ] => 1 );
     820                                }
     821                                $result = BookingService::create( $input );
     822                                // An extra the vehicle type does not allow: try again without it.
     823                                if ( ! is_int( $result ) && isset( $input['extras'] ) ) {
     824                                        unset( $input['extras'] );
     825                                        $result = BookingService::create( $input );
     826                                }
    559827                                $booking_id = is_int( $result ) ? $result : 0;
    560828                        }
    … …  
    567835                                $data['customers'][] = (int) $booking['customer_id'];
    568836                        }
    569                         if ( 'active' === $plan['status'] ) {
    570                                 BookingService::set_status( $booking_id, 'active' );
     837                        self::play_out( $booking_id, $plan );
     838                        self::add_booking_files( $booking_id, $plan );
     839                }
     840                return $data;
     841        }
     842
     843        /**
     844         * When the sample booking was made: three days before the pick-up, never later than an hour ago.
     845         *
     846         * @param array<string, mixed> $booking Booking row.
     847         * @return string UTC (Y-m-d H:i:s).
     848         */
     849        private static function created_at( array $booking ): string {
     850                $pickup = Time::from_utc( (string) $booking['pickup_at'] );
     851                $latest = Time::now()->modify( '-1 hour' );
     852                $made   = $pickup ? $pickup->modify( '-3 days' ) : $latest;
     853                return Time::to_mysql( $made < $latest ? $made : $latest );
     854        }
     855
     856        /**
     857         * Bring a sample booking to the situation of its plan: handed over, paid, returned, cancelled or not shown.
     858         * Dates are set to when it would have happened, so the reports and the calendar look like a real month.
     859         *
     860         * @param int                  $booking_id Booking ID.
     861         * @param array<string, mixed> $plan       Plan.
     862         * @return void
     863         */
     864        private static function play_out( int $booking_id, array $plan ): void {
     865                $booking = BookingRepository::find( $booking_id );
     866                if ( ! $booking ) {
     867                        return;
     868                }
     869                $now     = Time::to_mysql( Time::now() );
     870                $made    = self::created_at( $booking );
     871                $pickup  = min( (string) $booking['pickup_at'], $now );
     872                $return  = min( (string) $booking['return_at'], $now );
     873                $method  = (string) $plan['method'];
     874                $paid    = (string) ( $plan['paid'] ?? 'none' );
     875                $deposit = (string) ( $plan['deposit'] ?? 'none' );
     876                // Money paid in advance arrives two hours after booking; the rest at the counter.
     877                $prepaid = Time::to_mysql( min( Time::now(), Time::from_utc( $made )->modify( '+2 hours' ) ) );
     878                $pay     = static function ( string $type, string $how, int $amount, string $when, string $note = '' ) use ( $booking_id ): void {
     879                        if ( $amount > 0 ) {
     880                                BookingService::add_payment(
     881                                        $booking_id,
     882                                        array(
     883                                                'payment_type' => $type,
     884                                                'method'       => $how,
     885                                                'amount'       => $amount,
     886                                                'paid_at'      => $when,
     887                                                'reference'    => 'cash' === $how ? '' : 'SAMPLE-' . $booking_id,
     888                                                'note'         => '' === $note ? __( 'Sample payment', 'rentbaik' ) : $note,
     889                                        )
     890                                );
     891                        }
     892                };
     893
     894                BookingRepository::patch( $booking_id, array( 'created_at' => $made ) );
     895
     896                $total = (int) $booking['total'];
     897                $share = 'full' === $paid ? $total : (int) round( $total * (float) ( $plan['part'] ?? 0 ) );
     898                if ( in_array( $paid, array( 'full', 'part', 'refunded' ), true ) && in_array( $method, self::PREPAID, true ) ) {
     899                        $pay( 'payment', $method, $share, $prepaid );
     900                }
     901
     902                if ( in_array( $plan['status'], array( 'active', 'returned', 'completed' ), true ) ) {
     903                        BookingService::set_status( $booking_id, 'active' );
     904                        BookingRepository::patch( $booking_id, array( 'actual_pickup_at' => $pickup ) );
     905                        if ( in_array( $paid, array( 'full', 'part' ), true ) && ! in_array( $method, self::PREPAID, true ) ) {
     906                                $pay( 'payment', $method, $share, $pickup );
     907                        }
     908                        if ( 'none' !== $deposit ) {
     909                                $pay( 'deposit', 'cash', (int) $booking['deposit_amount'], $pickup, __( 'Deposit at pick-up', 'rentbaik' ) );
     910                        }
     911                }
     912
     913                if ( in_array( $plan['status'], array( 'returned', 'completed' ), true ) ) {
     914                        BookingService::set_status( $booking_id, 'returned' );
     915                        BookingRepository::patch( $booking_id, array( 'actual_return_at' => $return ) );
     916                        if ( 'returned' === $deposit ) {
     917                                $pay( 'deposit_refund', 'cash', (int) $booking['deposit_amount'], $return, __( 'Deposit given back', 'rentbaik' ) );
     918                        }
     919                        if ( 'completed' === $plan['status'] ) {
     920                                BookingService::set_status( $booking_id, 'completed' );
     921                        }
     922                }
     923
     924                if ( 'cancelled' === $plan['status'] ) {
     925                        BookingService::set_status( $booking_id, 'cancelled' );
     926                        if ( 'refunded' === $paid ) {
     927                                $pay( 'refund', $method, $share, max( $prepaid, Time::to_mysql( Time::now()->modify( '-1 day' ) ) ), __( 'Refund after cancelling', 'rentbaik' ) );
     928                        }
     929                }
     930                if ( 'no_show' === $plan['status'] ) {
     931                        BookingService::set_status( $booking_id, 'no_show' );
     932                }
     933        }
     934
     935        /**
     936         * Service for a scooter over the next two days, so the calendar shows a unit that cannot be rented.
     937         *
     938         * @param array<string, int[]> $data   Sample data record.
     939         * @param array<string, int>   $models Vehicle art => vehicle ID.
     940         * @return array<string, int[]>
     941         */
     942        private static function add_service_block( array $data, array $models ): array {
     943                $vehicle_id = $models['scooter-160'] ?? ( $models['scooter-125'] ?? 0 );
     944                if ( $vehicle_id <= 0 ) {
     945                        return $data;
     946                }
     947                $start = Time::to_mysql( Time::local_midnight( 1 )->modify( '+8 hours' ) );
     948                $end   = Time::to_mysql( Time::local_midnight( 3 )->modify( '+8 hours' ) );
     949                $units = array();
     950                foreach ( $data['units'] as $unit_id ) {
     951                        $unit = UnitRepository::find( $unit_id );
     952                        if ( $unit && (int) $unit['vehicle_id'] === $vehicle_id ) {
     953                                $units[] = $unit_id;
     954                        }
     955                }
     956                $busy = BlockRepository::busy_units( $units, $start, $end );
     957                foreach ( $units as $unit_id ) {
     958                        if ( ! in_array( $unit_id, $busy, true ) ) {
     959                                $block_id = BlockRepository::add( $unit_id, $start, $end, 'maintenance', 0, null, __( 'Oil change (sample)', 'rentbaik' ) );
     960                                if ( $block_id > 0 ) {
     961                                        $data['blocks'][] = $block_id;
     962                                }
     963                                break;
    571964                        }
    572965                }
    … …  
    580973         * @return int
    581974         */
    582         private static function price( float $usd ): int {
     975        public static function price( float $usd ): int {
    583976                if ( $usd <= 0 ) {
    584977                        return 0;
    … …  
    586979                $currency = Format::currency();
    587980                $amount   = $usd * (float) ( self::USD_RATES[ $currency ] ?? 1 );
    588                 $step     = $amount >= 100 ? 10 ** ( (int) floor( log10( $amount ) ) - 1 ) : 1;
    589                 $amount   = max( $step, round( $amount / $step ) * $step );
     981                // Small amounts keep halves (5.50), larger ones are rounded to two significant digits (88,000).
     982                $step   = $amount >= 100 ? 10 ** ( (int) floor( log10( $amount ) ) - 1 ) : ( $amount < 10 ? 0.5 : 1 );
     983                $amount = max( $step, round( $amount / $step ) * $step );
    590984                return (int) round( $amount * ( 10 ** Format::decimals() ) );
    591985        }
  • rentbaik/trunk/languages/rentbaik.pot

    r3790685 r3790901  
    33msgid ""
    44msgstr ""
    5 "Project-Id-Version: Rentbaik 1.0.0\n"
     5"Project-Id-Version: Rentbaik 1.0.1\n"
    66"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/rentbaik\n"
    77"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
    … …  
    1010"Content-Type: text/plain; charset=UTF-8\n"
    1111"Content-Transfer-Encoding: 8bit\n"
    12 "POT-Creation-Date: 2026-11-03T05:38:00+00:00\n"
     12"POT-Creation-Date: 2026-11-08T10:09:24+00:00\n"
    1313"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
    1414"X-Generator: WP-CLI 2.12.0\n"
    … …  
    1818#. Author of the plugin
    1919#: rentbaik.php
    20 #: includes/Admin/Menu.php:46
    21 #: includes/Admin/Menu.php:47
     20#: includes/Admin/Menu.php:44
     21#: includes/Admin/Menu.php:45
    2222#: includes/Admin/View.php:31
    2323#: includes/Privacy/Privacy.php:170
    … …  
    114114msgstr ""
    115115
    116 #: includes/Admin/Admin.php:47
    117 #: includes/Admin/Menu.php:122
     116#: includes/Admin/Admin.php:48
     117#: includes/Admin/Menu.php:132
    118118#: includes/Admin/SettingsPage.php:480
    119119msgid "Settings"
    120120msgstr ""
    121121
    122 #: includes/Admin/Admin.php:53
     122#: includes/Admin/Admin.php:54
    123123#: includes/Admin/DashboardPage.php:186
    124 #: includes/Admin/Menu.php:58
     124#: includes/Admin/Menu.php:56
    125125msgid "Dashboard"
    126126msgstr ""
    … …  
    177177#: includes/Admin/BookingsPage.php:331
    178178#: includes/Admin/BookingsPage.php:732
     179#: includes/Admin/CalendarView.php:285
     180#: includes/Admin/CalendarView.php:790
    179181msgid "Deposit"
    180182msgstr ""
    … …  
    305307#: includes/Admin/ExtrasPage.php:59
    306308#: includes/Admin/ExtrasPage.php:111
    307 #: includes/Fleet/VehiclePostType.php:393
     309#: includes/Fleet/VehiclePostType.php:395
    308310msgid "Price"
    309311msgstr ""
    … …  
    311313#: includes/Admin/BookingMove.php:191
    312314#: includes/Admin/BookingsPage.php:162
    313 #: includes/Admin/CalendarView.php:521
     315#: includes/Admin/CalendarView.php:597
    314316msgid "Unit"
    315317msgstr ""
    … …  
    518520#: includes/Admin/BookingPanel.php:201
    519521#: includes/Admin/ExtrasPage.php:52
    520 #: includes/Admin/Menu.php:86
    521 #: includes/Admin/Menu.php:87
     522#: includes/Admin/Menu.php:96
     523#: includes/Admin/Menu.php:97
    522524#: templates/booking-form.php:96
    523525msgid "Extras"
    … …  
    688690#: includes/Admin/CustomersPage.php:63
    689691#: includes/Admin/CustomersPage.php:117
    690 #: includes/Admin/Menu.php:66
    691 #: includes/Admin/Menu.php:67
     692#: includes/Admin/Menu.php:64
     693#: includes/Admin/Menu.php:65
    692694msgid "Bookings"
    693695msgstr ""
    … …  
    782784
    783785#: includes/Admin/BookingsPage.php:190
    784 #: includes/Admin/CalendarView.php:703
     786#: includes/Admin/CalendarView.php:779
    785787#: includes/Booking/BookingService.php:44
    786788#: includes/Frontend/BookingForm.php:311
    … …  
    825827
    826828#: includes/Admin/BookingsPage.php:286
    827 #: includes/Admin/DashboardPage.php:390
     829#: includes/Admin/DashboardPage.php:391
    828830#: includes/Admin/ExtrasPage.php:79
     831#: includes/Admin/SitePages.php:162
    829832#: includes/Admin/UnitsTable.php:368
    830833msgid "Edit"
    … …  
    925928#: includes/Admin/SetupWizard.php:455
    926929#: includes/Admin/UnitsTable.php:54
    927 #: includes/Fleet/VehiclePostType.php:392
     930#: includes/Fleet/VehiclePostType.php:394
    928931#: includes/Frontend/VehicleContent.php:49
    929932msgid "Type"
    … …  
    10821085
    10831086#: includes/Admin/CalendarView.php:117
    1084 #: includes/Admin/Menu.php:75
    1085 #: includes/Admin/Menu.php:76
     1087#: includes/Admin/Menu.php:85
     1088#: includes/Admin/Menu.php:86
    10861089#: includes/Admin/UnitsPage.php:59
    1087 #: includes/Fleet/VehiclePostType.php:394
     1090#: includes/Fleet/VehiclePostType.php:396
    10881091msgid "Units"
    10891092msgstr ""
    … …  
    10941097
    10951098#: includes/Admin/CalendarView.php:124
    1096 #: includes/Admin/DashboardPage.php:312
     1099#: includes/Admin/CalendarView.php:260
     1100#: includes/Admin/CalendarView.php:268
     1101#: includes/Admin/CalendarView.php:788
     1102#: includes/Admin/DashboardPage.php:313
    10971103msgid "Today"
    10981104msgstr ""
    … …  
    11101116
    11111117#. translators: %s: date and time.
    1112 #: includes/Admin/CalendarView.php:190
     1118#: includes/Admin/CalendarView.php:193
    11131119#: includes/Notifications/WhatsApp.php:74
    11141120#, php-format
    … …  
    11171123
    11181124#. translators: %s: date and time.
    1119 #: includes/Admin/CalendarView.php:192
     1125#: includes/Admin/CalendarView.php:195
    11201126#: includes/Notifications/WhatsApp.php:79
    11211127#, php-format
    … …  
    11231129msgstr ""
    11241130
     1131#: includes/Admin/CalendarView.php:252
     1132#: includes/Admin/CalendarView.php:787
     1133msgid "Late"
     1134msgstr ""
     1135
     1136#. translators: %d: number of hours.
     1137#: includes/Admin/CalendarView.php:254
     1138#, php-format
     1139msgid "Late by %d hour"
     1140msgid_plural "Late by %d hours"
     1141msgstr[0] ""
     1142msgstr[1] ""
     1143
     1144#: includes/Admin/CalendarView.php:261
     1145msgid "Comes back today"
     1146msgstr ""
     1147
     1148#: includes/Admin/CalendarView.php:269
     1149msgid "Picked up today"
     1150msgstr ""
     1151
     1152#: includes/Admin/CalendarView.php:276
     1153#: includes/Booking/BookingService.php:600
     1154msgid "Part paid"
     1155msgstr ""
     1156
     1157#: includes/Admin/CalendarView.php:276
     1158#: includes/Admin/CalendarView.php:789
     1159#: includes/Booking/BookingService.php:599
     1160msgid "Unpaid"
     1161msgstr ""
     1162
     1163#: includes/Admin/CalendarView.php:277
     1164#: includes/Admin/CalendarView.php:789
     1165msgid "Not fully paid"
     1166msgstr ""
     1167
     1168#: includes/Admin/CalendarView.php:286
     1169#: includes/Admin/CalendarView.php:790
     1170msgid "Deposit not given back yet"
     1171msgstr ""
     1172
    11251173#. translators: %s: date.
    1126 #: includes/Admin/CalendarView.php:366
    1127 #: includes/Admin/CalendarView.php:535
     1174#: includes/Admin/CalendarView.php:431
     1175#: includes/Admin/CalendarView.php:611
    11281176#, php-format
    11291177msgid "Show the agenda of %s"
    … …  
    11311179
    11321180#. translators: 1: vehicles out, 2: fleet size.
    1133 #: includes/Admin/CalendarView.php:371
     1181#: includes/Admin/CalendarView.php:436
    11341182#, php-format
    11351183msgid "%1$d of %2$d vehicles out"
    11361184msgstr ""
    11371185
    1138 #: includes/Admin/CalendarView.php:374
     1186#: includes/Admin/CalendarView.php:439
    11391187msgid "Closed"
    11401188msgstr ""
    11411189
    11421190#. translators: %s: date.
    1143 #: includes/Admin/CalendarView.php:385
     1191#: includes/Admin/CalendarView.php:450
    11441192#, php-format
    11451193msgid "Add booking on %s"
    … …  
    11471195
    11481196#. translators: %d: number of bookings not shown.
    1149 #: includes/Admin/CalendarView.php:397
     1197#: includes/Admin/CalendarView.php:462
    11501198#, php-format
    11511199msgid "+%d more"
    11521200msgstr ""
    11531201
    1154 #: includes/Admin/CalendarView.php:457
     1202#: includes/Admin/CalendarView.php:522
    11551203msgid "Drag to change the pick-up date"
    11561204msgstr ""
    11571205
    1158 #: includes/Admin/CalendarView.php:474
     1206#: includes/Admin/CalendarView.php:539
    11591207msgid "No unit"
    11601208msgstr ""
    11611209
    1162 #: includes/Admin/CalendarView.php:477
    1163 #: includes/Admin/CalendarView.php:710
    1164 #: includes/Admin/DayAgenda.php:232
    1165 msgid "Not fully paid"
    1166 msgstr ""
    1167 
    1168 #: includes/Admin/CalendarView.php:480
     1210#: includes/Admin/CalendarView.php:556
    11691211msgid "Drag to change the return date"
    11701212msgstr ""
    11711213
    11721214#. translators: %d: number of units.
    1173 #: includes/Admin/CalendarView.php:554
     1215#: includes/Admin/CalendarView.php:630
    11741216#, php-format
    11751217msgid "%d unit"
    … …  
    11781220msgstr[1] ""
    11791221
    1180 #: includes/Admin/CalendarView.php:570
     1222#: includes/Admin/CalendarView.php:646
    11811223msgid "No unit yet"
    11821224msgstr ""
    11831225
    1184 #: includes/Admin/CalendarView.php:571
     1226#: includes/Admin/CalendarView.php:647
    11851227msgid "drag to a unit"
    11861228msgstr ""
    11871229
    1188 #: includes/Admin/CalendarView.php:670
     1230#: includes/Admin/CalendarView.php:746
    11891231#: includes/Admin/UnitsPage.php:166
    11901232#: includes/Admin/UnitsPage.php:222
    1191 #: includes/Admin/UnitsPage.php:388
     1233#: includes/Admin/UnitsPage.php:389
    11921234msgid "Service"
    11931235msgstr ""
    11941236
    1195 #: includes/Admin/CalendarView.php:670
     1237#: includes/Admin/CalendarView.php:746
    11961238#: includes/Admin/UnitsPage.php:167
    1197 #: includes/Admin/UnitsPage.php:388
     1239#: includes/Admin/UnitsPage.php:389
    11981240msgid "Blocked"
    11991241msgstr ""
    12001242
    1201 #: includes/Admin/CalendarView.php:688
     1243#: includes/Admin/CalendarView.php:764
    12021244msgid "No units yet. Add the units (plate numbers) of your vehicles first."
    12031245msgstr ""
    12041246
    1205 #: includes/Admin/CalendarView.php:702
     1247#: includes/Admin/CalendarView.php:778
    12061248#: includes/Booking/BookingService.php:43
    12071249msgid "Waiting"
    12081250msgstr ""
    12091251
    1210 #: includes/Admin/CalendarView.php:704
     1252#: includes/Admin/CalendarView.php:780
    12111253#: includes/Booking/BookingService.php:45
    12121254#: includes/Fleet/UnitRepository.php:41
    … …  
    12151257msgstr ""
    12161258
    1217 #: includes/Admin/CalendarView.php:705
     1259#: includes/Admin/CalendarView.php:781
     1260#: includes/Admin/CalendarView.php:787
    12181261#: includes/Admin/UnitsTable.php:312
    12191262msgid "Late return"
    12201263msgstr ""
    12211264
    1222 #: includes/Admin/CalendarView.php:706
     1265#: includes/Admin/CalendarView.php:782
    12231266msgid "Returned / completed"
    12241267msgstr ""
    12251268
    1226 #: includes/Admin/CalendarView.php:712
     1269#: includes/Admin/CalendarView.php:788
     1270msgid "Picked up or comes back today"
     1271msgstr ""
     1272
     1273#: includes/Admin/CalendarView.php:795
    12271274msgid "vehicles out / fleet"
    12281275msgstr ""
    12291276
    12301277#. translators: %d: number of pick-ups.
    1231 #: includes/Admin/CalendarView.php:736
     1278#: includes/Admin/CalendarView.php:819
    12321279#, php-format
    12331280msgid "%d pick-up"
    … …  
    12371284
    12381285#. translators: %d: number of returns.
    1239 #: includes/Admin/CalendarView.php:738
     1286#: includes/Admin/CalendarView.php:821
    12401287#, php-format
    12411288msgid "%d return"
    … …  
    12451292
    12461293#. translators: %d: number of bookings waiting for confirmation.
    1247 #: includes/Admin/CalendarView.php:740
     1294#: includes/Admin/CalendarView.php:823
    12481295#, php-format
    12491296msgid "%d waiting"
    … …  
    12531300
    12541301#: includes/Admin/CustomersPage.php:58
    1255 #: includes/Admin/Menu.php:95
    1256 #: includes/Admin/Menu.php:96
     1302#: includes/Admin/Menu.php:105
     1303#: includes/Admin/Menu.php:106
    12571304msgid "Customers"
    12581305msgstr ""
    … …  
    13581405
    13591406#: includes/Admin/DashboardPage.php:65
    1360 #: includes/Fleet/VehiclePostType.php:146
     1407#: includes/Fleet/VehiclePostType.php:148
    13611408msgid "Units (plate numbers)"
    13621409msgstr ""
    … …  
    15051552msgstr ""
    15061553
    1507 #: includes/Admin/DashboardPage.php:235
     1554#: includes/Admin/DashboardPage.php:236
    15081555msgid "Pick-ups today"
    15091556msgstr ""
    15101557
    1511 #: includes/Admin/DashboardPage.php:236
     1558#: includes/Admin/DashboardPage.php:237
    15121559msgid "Returns today"
    15131560msgstr ""
    15141561
    1515 #: includes/Admin/DashboardPage.php:237
     1562#: includes/Admin/DashboardPage.php:238
    15161563msgid "Late returns"
    15171564msgstr ""
    15181565
    1519 #: includes/Admin/DashboardPage.php:238
     1566#: includes/Admin/DashboardPage.php:239
    15201567msgid "Waiting for confirmation"
    15211568msgstr ""
    15221569
    15231570#. translators: %d: number of more bookings.
    1524 #: includes/Admin/DashboardPage.php:261
     1571#: includes/Admin/DashboardPage.php:262
    15251572#, php-format
    15261573msgid "+ %d more"
    15271574msgstr ""
    15281575
    1529 #: includes/Admin/DashboardPage.php:265
     1576#: includes/Admin/DashboardPage.php:266
    15301577msgid "Review and confirm"
    15311578msgstr ""
    15321579
    1533 #: includes/Admin/DashboardPage.php:267
     1580#: includes/Admin/DashboardPage.php:268
    15341581msgid "Everything is back on time."
    15351582msgstr ""
    15361583
    1537 #: includes/Admin/DashboardPage.php:267
     1584#: includes/Admin/DashboardPage.php:268
    15381585msgid "Nothing planned."
    15391586msgstr ""
    15401587
    1541 #: includes/Admin/DashboardPage.php:283
     1588#: includes/Admin/DashboardPage.php:284
    15421589msgid "Fleet right now"
    15431590msgstr ""
    15441591
    1545 #: includes/Admin/DashboardPage.php:283
     1592#: includes/Admin/DashboardPage.php:284
    15461593#: includes/Admin/UnitsPage.php:94
    15471594msgid "All units"
    15481595msgstr ""
    15491596
    1550 #: includes/Admin/DashboardPage.php:285
     1597#: includes/Admin/DashboardPage.php:286
    15511598msgid "No units yet."
    15521599msgstr ""
    15531600
    15541601#. translators: %d: percentage of the fleet on rent.
    1555 #: includes/Admin/DashboardPage.php:290
     1602#: includes/Admin/DashboardPage.php:291
    15561603#, php-format
    15571604msgid "%d%% on rent"
    15581605msgstr ""
    15591606
    1560 #: includes/Admin/DashboardPage.php:291
     1607#: includes/Admin/DashboardPage.php:292
    15611608msgid "Fleet status"
    15621609msgstr ""
    15631610
    1564 #: includes/Admin/DashboardPage.php:312
     1611#: includes/Admin/DashboardPage.php:313
    15651612msgid "vs yesterday"
    15661613msgstr ""
    15671614
    1568 #: includes/Admin/DashboardPage.php:313
     1615#: includes/Admin/DashboardPage.php:314
    15691616msgid "Last 7 days"
    15701617msgstr ""
    15711618
    1572 #: includes/Admin/DashboardPage.php:313
     1619#: includes/Admin/DashboardPage.php:314
    15731620msgid "vs the 7 days before"
    15741621msgstr ""
    15751622
    1576 #: includes/Admin/DashboardPage.php:314
     1623#: includes/Admin/DashboardPage.php:315
    15771624msgid "This month"
    15781625msgstr ""
    15791626
    1580 #: includes/Admin/DashboardPage.php:314
     1627#: includes/Admin/DashboardPage.php:315
    15811628msgid "vs the same days last month"
    15821629msgstr ""
    15831630
    1584 #: includes/Admin/DashboardPage.php:316
     1631#: includes/Admin/DashboardPage.php:317
    15851632msgid "Income received"
    15861633msgstr ""
    15871634
    1588 #: includes/Admin/DashboardPage.php:326
     1635#: includes/Admin/DashboardPage.php:327
    15891636msgid "Rental payments recorded in Rentbaik, without deposits."
    15901637msgstr ""
    15911638
    1592 #: includes/Admin/DashboardPage.php:347
     1639#: includes/Admin/DashboardPage.php:348
    15931640msgid "Welcome to Rentbaik"
    15941641msgstr ""
    15951642
    1596 #: includes/Admin/DashboardPage.php:348
     1643#: includes/Admin/DashboardPage.php:349
    15971644msgid "Answer a few simple questions and your booking page is ready. Not sure yet? Try it with sample data first."
    15981645msgstr ""
    15991646
    1600 #: includes/Admin/DashboardPage.php:349
     1647#: includes/Admin/DashboardPage.php:350
    16011648msgid "About 5 minutes"
    16021649msgstr ""
    16031650
    1604 #: includes/Admin/DashboardPage.php:349
     1651#: includes/Admin/DashboardPage.php:350
    16051652msgid "Every step can be skipped"
    16061653msgstr ""
    16071654
    1608 #: includes/Admin/DashboardPage.php:349
     1655#: includes/Admin/DashboardPage.php:350
    16091656msgid "Change everything later"
    16101657msgstr ""
    16111658
    1612 #: includes/Admin/DashboardPage.php:351
     1659#: includes/Admin/DashboardPage.php:352
    16131660msgid "Start the setup wizard"
    16141661msgstr ""
    16151662
    1616 #: includes/Admin/DashboardPage.php:356
     1663#: includes/Admin/DashboardPage.php:357
    16171664msgid "Hide this"
    16181665msgstr ""
    16191666
    1620 #: includes/Admin/DashboardPage.php:370
     1667#: includes/Admin/DashboardPage.php:371
    16211668msgid "Get ready to take bookings"
    16221669msgstr ""
    16231670
    16241671#. translators: 1: completed steps, 2: total steps.
    1625 #: includes/Admin/DashboardPage.php:372
     1672#: includes/Admin/DashboardPage.php:373
    16261673#, php-format
    16271674msgid "%1$d of %2$d done"
    16281675msgstr ""
    16291676
    1630 #: includes/Admin/DashboardPage.php:374
     1677#: includes/Admin/DashboardPage.php:375
    16311678msgid "Prefer step by step? Open the setup wizard."
    16321679msgstr ""
    16331680
    1634 #: includes/Admin/DashboardPage.php:390
     1681#: includes/Admin/DashboardPage.php:391
    16351682msgid "Set up"
    16361683msgstr ""
    16371684
    1638 #: includes/Admin/DashboardPage.php:404
     1685#: includes/Admin/DashboardPage.php:405
    16391686msgid "System status"
    16401687msgstr ""
    16411688
    1642 #: includes/Admin/DashboardPage.php:407
     1689#: includes/Admin/DashboardPage.php:408
    16431690msgid "OK"
    16441691msgstr ""
    16451692
    1646 #: includes/Admin/DashboardPage.php:407
     1693#: includes/Admin/DashboardPage.php:408
    16471694msgid "Check"
    16481695msgstr ""
    16491696
    1650 #: includes/Admin/DashboardPage.php:407
     1697#: includes/Admin/DashboardPage.php:408
    16511698msgid "Optional"
    16521699msgstr ""
    … …  
    16991746#: includes/Admin/ExtrasPage.php:187
    17001747#: includes/Admin/UnitsPage.php:51
    1701 #: includes/Admin/UnitsPage.php:429
     1748#: includes/Admin/UnitsPage.php:430
    17021749msgid "You are not allowed to manage the fleet."
    17031750msgstr ""
    … …  
    17631810#: includes/Admin/ExtrasPage.php:112
    17641811#: includes/Admin/SettingsPage.php:110
    1765 #: includes/Fleet/VehiclePostType.php:213
     1812#: includes/Fleet/VehiclePostType.php:215
    17661813#: includes/Frontend/VehicleContent.php:74
    17671814msgid "Per day"
    … …  
    18101857msgstr ""
    18111858
    1812 #: includes/Admin/Menu.php:57
     1859#: includes/Admin/Menu.php:55
    18131860msgid "Rentbaik Dashboard"
    18141861msgstr ""
    18151862
    1816 #: includes/Admin/Menu.php:104
    1817 #: includes/Fleet/VehiclePostType.php:115
     1863#: includes/Admin/Menu.php:76
     1864#: includes/Admin/Menu.php:77
     1865#: includes/Admin/SetupWizard.php:78
     1866#: includes/Fleet/VehiclePostType.php:80
     1867#: includes/Fleet/VehiclePostType.php:90
     1868#: includes/Fleet/VehiclePostType.php:91
     1869msgid "Vehicles"
     1870msgstr ""
     1871
     1872#: includes/Admin/Menu.php:114
     1873#: includes/Fleet/VehiclePostType.php:117
    18181874msgid "Vehicle categories"
    18191875msgstr ""
    18201876
    1821 #: includes/Admin/Menu.php:105
    1822 #: includes/Fleet/VehiclePostType.php:117
     1877#: includes/Admin/Menu.php:115
     1878#: includes/Fleet/VehiclePostType.php:119
    18231879msgid "Categories"
    18241880msgstr ""
    18251881
    1826 #: includes/Admin/Menu.php:112
     1882#: includes/Admin/Menu.php:122
    18271883#: includes/Admin/SetupWizard.php:140
    18281884#: includes/Admin/SetupWizard.php:162
    … …  
    18301886msgstr ""
    18311887
    1832 #: includes/Admin/Menu.php:113
     1888#: includes/Admin/Menu.php:123
    18331889#: includes/Admin/SettingsPage.php:480
    18341890msgid "Setup wizard"
    18351891msgstr ""
    18361892
    1837 #: includes/Admin/Menu.php:121
     1893#: includes/Admin/Menu.php:131
    18381894msgid "Rentbaik Settings"
     1895msgstr ""
     1896
     1897#: includes/Admin/PrivateCheck.php:111
     1898msgid "Customer files are not protected yet."
     1899msgstr ""
     1900
     1901#: includes/Admin/PrivateCheck.php:112
     1902msgid "Your web server opens files in the Rentbaik private folder directly. Customer photos and payment receipts have random names that cannot be guessed, but to block the folder completely add this rule to your nginx configuration (or ask your host):"
     1903msgstr ""
     1904
     1905#: includes/Admin/PrivateCheck.php:114
     1906msgid "Check again"
     1907msgstr ""
     1908
     1909#: includes/Admin/PrivateCheck.php:124
     1910msgid "You are not allowed to do this."
     1911msgstr ""
     1912
     1913#: includes/Admin/PrivateCheck.php:129
     1914msgid "The private folder is protected."
     1915msgstr ""
     1916
     1917#: includes/Admin/PrivateCheck.php:131
     1918msgid "The check could not reach your site. Please try again later."
    18391919msgstr ""
    18401920
    … …  
    19202000
    19212001#: includes/Admin/SettingsPage.php:111
    1922 #: includes/Fleet/VehiclePostType.php:212
     2002#: includes/Fleet/VehiclePostType.php:214
    19232003#: includes/Frontend/VehicleContent.php:73
    19242004msgid "Per hour"
    … …  
    20262106
    20272107#: includes/Admin/SettingsPage.php:175
    2028 #: includes/Fleet/VehiclePostType.php:228
     2108#: includes/Fleet/VehiclePostType.php:230
    20292109msgid "Minimum rental (hours)"
    20302110msgstr ""
    20312111
    20322112#: includes/Admin/SettingsPage.php:177
    2033 #: includes/Fleet/VehiclePostType.php:229
     2113#: includes/Fleet/VehiclePostType.php:231
    20342114msgid "Maximum rental (days)"
    20352115msgstr ""
    … …  
    20642144
    20652145#: includes/Admin/SettingsPage.php:194
    2066 #: includes/Fleet/VehiclePostType.php:227
     2146#: includes/Fleet/VehiclePostType.php:229
    20672147msgid "Security deposit"
    20682148msgstr ""
    … …  
    23362416
    23372417#: includes/Admin/SettingsPage.php:587
     2418#: includes/Admin/SitePages.php:159
    23382419msgid "View"
    23392420msgstr ""
    … …  
    24822563#: includes/Admin/SetupWizard.php:76
    24832564msgid "Pick-up place"
    2484 msgstr ""
    2485 
    2486 #: includes/Admin/SetupWizard.php:78
    2487 #: includes/Fleet/VehiclePostType.php:80
    2488 #: includes/Fleet/VehiclePostType.php:90
    2489 #: includes/Fleet/VehiclePostType.php:91
    2490 msgid "Vehicles"
    24912565msgstr ""
    24922566
    … …  
    27172791
    27182792#: includes/Admin/SetupWizard.php:521
     2793#: includes/Admin/SitePages.php:159
     2794#: includes/Admin/SitePages.php:177
    27192795#: templates/booking-form.php:69
    27202796msgid "(opens in a new tab)"
    … …  
    27982874msgstr ""
    27992875
     2876#: includes/Admin/SitePages.php:32
     2877msgid "Customers choose dates and see the free vehicles with the price."
     2878msgstr ""
     2879
     2880#: includes/Admin/SitePages.php:33
     2881msgid "Booking form in three steps; the confirmation and the booking details open here too."
     2882msgstr ""
     2883
     2884#: includes/Admin/SitePages.php:34
     2885msgid "Customers look up their booking with its number and their email or phone."
     2886msgstr ""
     2887
     2888#: includes/Admin/SitePages.php:56
     2889msgid "Vehicle pages"
     2890msgstr ""
     2891
     2892#. translators: %s: vehicle name.
     2893#: includes/Admin/SitePages.php:58
     2894#, php-format
     2895msgid "Every vehicle has its own page with photos, prices, availability and a booking button. Example: %s"
     2896msgstr ""
     2897
     2898#: includes/Admin/SitePages.php:126
     2899msgid "Your pages for customers"
     2900msgstr ""
     2901
     2902#: includes/Admin/SitePages.php:128
     2903msgid "Change pages"
     2904msgstr ""
     2905
     2906#: includes/Admin/SitePages.php:131
     2907msgid "Rentbaik made these pages when it was activated. Add them to your site menu, or put the shortcodes on any page or in any page builder."
     2908msgstr ""
     2909
     2910#. translators: %d: number of pages.
     2911#: includes/Admin/SitePages.php:136
     2912#, php-format
     2913msgid "%d page is missing."
     2914msgid_plural "%d pages are missing."
     2915msgstr[0] ""
     2916msgstr[1] ""
     2917
     2918#: includes/Admin/SitePages.php:139
     2919msgid "Create them in Settings → Display"
     2920msgstr ""
     2921
     2922#: includes/Admin/SitePages.php:148
     2923msgid "Not published"
     2924msgstr ""
     2925
     2926#: includes/Admin/SitePages.php:151
     2927msgid "Missing"
     2928msgstr ""
     2929
     2930#. translators: %s: booking number.
     2931#: includes/Admin/SitePages.php:174
     2932#, php-format
     2933msgid "See sample booking %s as the customer sees it"
     2934msgstr ""
     2935
    28002936#: includes/Admin/UnitsPage.php:59
    28012937msgid "Every real vehicle with its plate number, kilometres and status."
    … …  
    28052941#: includes/Admin/UnitsPage.php:93
    28062942#: includes/Admin/UnitsPage.php:142
    2807 #: includes/Fleet/VehiclePostType.php:296
     2943#: includes/Fleet/VehiclePostType.php:298
    28082944msgid "Add unit"
    28092945msgstr ""
    … …  
    29513087
    29523088#. translators: %s: unit status.
    2953 #: includes/Admin/UnitsPage.php:344
     3089#: includes/Admin/UnitsPage.php:345
    29543090#, php-format
    29553091msgid "Status set to %s (bulk action)."
    … …  
    29573093
    29583094#. translators: %d: number of units.
    2959 #: includes/Admin/UnitsPage.php:347
     3095#: includes/Admin/UnitsPage.php:348
    29603096#, php-format
    29613097msgid "%d unit updated."
    … …  
    29643100msgstr[1] ""
    29653101
    2966 #: includes/Admin/UnitsPage.php:371
     3102#: includes/Admin/UnitsPage.php:372
    29673103msgid "Choose a start and an end; the end must be after the start."
    29683104msgstr ""
    29693105
    2970 #: includes/Admin/UnitsPage.php:376
     3106#: includes/Admin/UnitsPage.php:377
    29713107msgid "Dates blocked. Note: this unit already has a booking or block in that period."
    29723108msgstr ""
    29733109
    2974 #: includes/Admin/UnitsPage.php:378
     3110#: includes/Admin/UnitsPage.php:379
    29753111msgid "Dates blocked."
    29763112msgstr ""
    29773113
    29783114#. translators: 1: block type, 2: start, 3: end.
    2979 #: includes/Admin/UnitsPage.php:387
     3115#: includes/Admin/UnitsPage.php:388
    29803116#, php-format
    29813117msgid "Blocked (%1$s): %2$s – %3$s."
    … …  
    29833119
    29843120#. translators: 1: start, 2: end.
    2985 #: includes/Admin/UnitsPage.php:410
     3121#: includes/Admin/UnitsPage.php:411
    29863122#, php-format
    29873123msgid "Block removed: %1$s – %2$s."
    29883124msgstr ""
    29893125
    2990 #: includes/Admin/UnitsPage.php:411
     3126#: includes/Admin/UnitsPage.php:412
    29913127msgid "Block removed."
    29923128msgstr ""
    … …  
    30773213
    30783214#: includes/Admin/UnitsTable.php:330
    3079 #: includes/Fleet/VehiclePostType.php:267
     3215#: includes/Fleet/VehiclePostType.php:269
    30803216msgid "No units yet. A unit is one real vehicle with its own plate number."
    30813217msgstr ""
    … …  
    32183354msgstr ""
    32193355
    3220 #: includes/Booking/BookingService.php:599
    3221 msgid "Unpaid"
    3222 msgstr ""
    3223 
    3224 #: includes/Booking/BookingService.php:600
    3225 msgid "Part paid"
    3226 msgstr ""
    3227 
    32283356#: includes/Capabilities.php:70
    32293357msgid "Rental Manager"
    … …  
    33953523msgstr ""
    33963524
    3397 #: includes/Fleet/VehiclePostType.php:116
     3525#: includes/Fleet/VehiclePostType.php:118
    33983526msgid "Vehicle category"
    33993527msgstr ""
    34003528
    3401 #: includes/Fleet/VehiclePostType.php:118
     3529#: includes/Fleet/VehiclePostType.php:120
    34023530msgid "Add category"
    34033531msgstr ""
    34043532
    3405 #: includes/Fleet/VehiclePostType.php:119
     3533#: includes/Fleet/VehiclePostType.php:121
    34063534msgid "Edit category"
    34073535msgstr ""
    34083536
    3409 #: includes/Fleet/VehiclePostType.php:120
     3537#: includes/Fleet/VehiclePostType.php:122
    34103538msgid "Search categories"
    34113539msgstr ""
    34123540
    3413 #: includes/Fleet/VehiclePostType.php:143
     3541#: includes/Fleet/VehiclePostType.php:145
    34143542msgid "Vehicle details"
    34153543msgstr ""
    34163544
    3417 #: includes/Fleet/VehiclePostType.php:144
     3545#: includes/Fleet/VehiclePostType.php:146
    34183546#: templates/vehicle-details.php:35
    34193547msgid "Prices"
    34203548msgstr ""
    34213549
    3422 #: includes/Fleet/VehiclePostType.php:145
     3550#: includes/Fleet/VehiclePostType.php:147
    34233551msgid "Photo gallery"
    34243552msgstr ""
    34253553
    3426 #: includes/Fleet/VehiclePostType.php:162
     3554#: includes/Fleet/VehiclePostType.php:164
    34273555msgid "Vehicle type"
    34283556msgstr ""
    34293557
    3430 #: includes/Fleet/VehiclePostType.php:167
     3558#: includes/Fleet/VehiclePostType.php:169
    34313559#: includes/Frontend/VehicleContent.php:53
    34323560msgid "Brand"
    34333561msgstr ""
    34343562
    3435 #: includes/Fleet/VehiclePostType.php:167
     3563#: includes/Fleet/VehiclePostType.php:169
    34363564msgid "e.g. Honda"
    34373565msgstr ""
    34383566
    3439 #: includes/Fleet/VehiclePostType.php:168
     3567#: includes/Fleet/VehiclePostType.php:170
    34403568#: includes/Frontend/VehicleContent.php:56
    34413569msgid "Model year"
    … …  
    34433571
    34443572#. translators: %s: vehicle type, e.g. Car.
    3445 #: includes/Fleet/VehiclePostType.php:178
     3573#: includes/Fleet/VehiclePostType.php:180
    34463574#, php-format
    34473575msgid "%s specifications"
    34483576msgstr ""
    34493577
    3450 #: includes/Fleet/VehiclePostType.php:214
     3578#: includes/Fleet/VehiclePostType.php:216
    34513579#: includes/Frontend/VehicleContent.php:75
    34523580msgid "Per week (7 days)"
    34533581msgstr ""
    34543582
    3455 #: includes/Fleet/VehiclePostType.php:215
     3583#: includes/Fleet/VehiclePostType.php:217
    34563584#: includes/Frontend/VehicleContent.php:76
    34573585msgid "Per month (30 days)"
    34583586msgstr ""
    34593587
    3460 #: includes/Fleet/VehiclePostType.php:220
     3588#: includes/Fleet/VehiclePostType.php:222
    34613589msgid "Leave a price empty if you do not offer that period. Longer rentals automatically use the cheaper weekly or monthly price when it is set."
    34623590msgstr ""
    34633591
    3464 #: includes/Fleet/VehiclePostType.php:231
     3592#: includes/Fleet/VehiclePostType.php:233
    34653593msgid "Empty deposit, minimum and maximum use the defaults from Settings → Rental rules. Maximum 0 means no limit."
    34663594msgstr ""
    34673595
    3468 #: includes/Fleet/VehiclePostType.php:249
     3596#: includes/Fleet/VehiclePostType.php:251
    34693597msgid "Choose photos"
    34703598msgstr ""
    34713599
    3472 #: includes/Fleet/VehiclePostType.php:250
     3600#: includes/Fleet/VehiclePostType.php:252
    34733601msgid "Remove all"
    34743602msgstr ""
    34753603
    3476 #: includes/Fleet/VehiclePostType.php:251
     3604#: includes/Fleet/VehiclePostType.php:253
    34773605msgid "The featured image is the main photo. Add more photos here for the vehicle page."
    34783606msgstr ""
    34793607
    3480 #: includes/Fleet/VehiclePostType.php:285
     3608#: includes/Fleet/VehiclePostType.php:287
    34813609msgid "Save the vehicle first, then add its units."
    34823610msgstr ""
    34833611
    34843612#. translators: %s: price.
    3485 #: includes/Fleet/VehiclePostType.php:416
     3613#: includes/Fleet/VehiclePostType.php:418
    34863614#, php-format
    34873615msgid "%s / day"
    … …  
    34893617
    34903618#. translators: %s: price.
    3491 #: includes/Fleet/VehiclePostType.php:419
     3619#: includes/Fleet/VehiclePostType.php:421
    34923620#, php-format
    34933621msgid "%s / hour"
    34943622msgstr ""
    34953623
    3496 #: includes/Fleet/VehiclePostType.php:421
     3624#: includes/Fleet/VehiclePostType.php:423
    34973625msgid "No price yet"
    34983626msgstr ""
    34993627
    35003628#. translators: 1: ready units, 2: all units.
    3501 #: includes/Fleet/VehiclePostType.php:428
     3629#: includes/Fleet/VehiclePostType.php:430
    35023630#, php-format
    35033631msgid "%1$d ready of %2$d"
    … …  
    39524080msgstr ""
    39534081
    3954 #: includes/Setup/SampleData.php:282
     4082#: includes/Setup/SampleData.php:384
     4083msgid "Included gear (sample)"
     4084msgstr ""
     4085
     4086#: includes/Setup/SampleData.php:455
    39554087msgid "This is sample data from Rentbaik. Change it, or remove all sample data from the Rentbaik dashboard."
    39564088msgstr ""
    39574089
    3958 #: includes/Setup/SampleData.php:286
     4090#: includes/Setup/SampleData.php:461
    39594091msgid "Automatic scooter 125 cc (sample)"
    39604092msgstr ""
    39614093
    3962 #: includes/Setup/SampleData.php:287
     4094#: includes/Setup/SampleData.php:462
    39634095msgid "Easy automatic scooter for the city. Two helmets included."
    39644096msgstr ""
    39654097
    3966 #: includes/Setup/SampleData.php:300
    3967 #: includes/Setup/SampleData.php:337
    3968 #: includes/Setup/SampleData.php:389
     4098#: includes/Setup/SampleData.php:475
     4099#: includes/Setup/SampleData.php:516
     4100#: includes/Setup/SampleData.php:574
    39694101msgid "White"
    39704102msgstr ""
    39714103
    3972 #: includes/Setup/SampleData.php:300
     4104#: includes/Setup/SampleData.php:475
    39734105msgid "Red"
    39744106msgstr ""
    39754107
    3976 #: includes/Setup/SampleData.php:300
    3977 #: includes/Setup/SampleData.php:357
     4108#: includes/Setup/SampleData.php:475
     4109#: includes/Setup/SampleData.php:538
    39784110msgid "Black"
    39794111msgstr ""
    39804112
    3981 #: includes/Setup/SampleData.php:304
     4113#: includes/Setup/SampleData.php:481
    39824114msgid "Comfort scooter 160 cc (sample)"
    39834115msgstr ""
    39844116
    3985 #: includes/Setup/SampleData.php:305
     4117#: includes/Setup/SampleData.php:482
    39864118msgid "Bigger scooter with a large seat and storage, good for longer trips."
    39874119msgstr ""
    39884120
    3989 #: includes/Setup/SampleData.php:317
     4121#: includes/Setup/SampleData.php:494
    39904122msgid "Grey"
    39914123msgstr ""
    39924124
    3993 #: includes/Setup/SampleData.php:317
     4125#: includes/Setup/SampleData.php:494
    39944126msgid "Blue"
    39954127msgstr ""
    39964128
    3997 #: includes/Setup/SampleData.php:321
     4129#: includes/Setup/SampleData.php:500
    39984130msgid "Compact car, automatic (sample)"
    39994131msgstr ""
    40004132
    4001 #: includes/Setup/SampleData.php:322
     4133#: includes/Setup/SampleData.php:501
    40024134msgid "Small and easy to park. Air conditioning, five seats."
    40034135msgstr ""
    40044136
    4005 #: includes/Setup/SampleData.php:337
     4137#: includes/Setup/SampleData.php:516
    40064138msgid "Silver"
    40074139msgstr ""
    40084140
    4009 #: includes/Setup/SampleData.php:341
     4141#: includes/Setup/SampleData.php:522
    40104142msgid "Family car, 7 seats (sample)"
    40114143msgstr ""
    40124144
    4013 #: includes/Setup/SampleData.php:342
     4145#: includes/Setup/SampleData.php:523
    40144146msgid "Room for the whole family and their bags."
    40154147msgstr ""
    40164148
    4017 #: includes/Setup/SampleData.php:361
     4149#: includes/Setup/SampleData.php:544
    40184150msgid "City bike (sample)"
    40194151msgstr ""
    40204152
    4021 #: includes/Setup/SampleData.php:362
     4153#: includes/Setup/SampleData.php:545
    40224154msgid "Comfortable bike with a basket and a lock."
    40234155msgstr ""
    40244156
    4025 #: includes/Setup/SampleData.php:373
     4157#: includes/Setup/SampleData.php:556
    40264158msgid "Green"
    40274159msgstr ""
    40284160
    4029 #: includes/Setup/SampleData.php:377
     4161#: includes/Setup/SampleData.php:562
    40304162msgid "Electric bike (sample)"
    40314163msgstr ""
    40324164
    4033 #: includes/Setup/SampleData.php:378
     4165#: includes/Setup/SampleData.php:563
    40344166msgid "Pedal assist up to 60 km per charge."
    40354167msgstr ""
    40364168
    4037 #: includes/Setup/SampleData.php:402
     4169#: includes/Setup/SampleData.php:587
    40384170msgid "Extra helmet (sample)"
    40394171msgstr ""
    40404172
    4041 #: includes/Setup/SampleData.php:403
     4173#: includes/Setup/SampleData.php:588
    40424174msgid "Clean helmet for a passenger."
    40434175msgstr ""
    40444176
    4045 #: includes/Setup/SampleData.php:410
     4177#: includes/Setup/SampleData.php:595
    40464178msgid "Rain poncho (sample)"
    40474179msgstr ""
    40484180
    4049 #: includes/Setup/SampleData.php:411
     4181#: includes/Setup/SampleData.php:596
    40504182msgid "Yours to keep."
    40514183msgstr ""
    40524184
    4053 #: includes/Setup/SampleData.php:418
     4185#: includes/Setup/SampleData.php:603
    40544186msgid "Child seat (sample)"
    40554187msgstr ""
    40564188
    4057 #: includes/Setup/SampleData.php:419
     4189#: includes/Setup/SampleData.php:604
    40584190msgid "For children from 9 to 18 kg."
    40594191msgstr ""
    40604192
    4061 #: includes/Setup/SampleData.php:486
    4062 #: includes/Setup/SampleData.php:550
     4193#: includes/Setup/SampleData.php:671
     4194#: includes/Setup/SampleData.php:809
    40634195msgid "Sample data"
    40644196msgstr ""
    40654197
    4066 #: includes/Setup/SampleData.php:547
     4198#: includes/Setup/SampleData.php:806
    40674199msgid "Sample"
    40684200msgstr ""
    40694201
    4070 #: includes/Setup/SampleData.php:555
     4202#: includes/Setup/SampleData.php:814
    40714203msgid "Sample booking. It is removed together with the sample data."
     4204msgstr ""
     4205
     4206#: includes/Setup/SampleData.php:888
     4207msgid "Sample payment"
     4208msgstr ""
     4209
     4210#: includes/Setup/SampleData.php:909
     4211msgid "Deposit at pick-up"
     4212msgstr ""
     4213
     4214#: includes/Setup/SampleData.php:917
     4215msgid "Deposit given back"
     4216msgstr ""
     4217
     4218#: includes/Setup/SampleData.php:927
     4219msgid "Refund after cancelling"
     4220msgstr ""
     4221
     4222#: includes/Setup/SampleData.php:959
     4223msgid "Oil change (sample)"
    40724224msgstr ""
    40734225
  • rentbaik/trunk/readme.txt

    r3790685 r3790901  
    11=== Rentbaik ===
    22Contributors: rentbaik
    3 Tags: rental, booking, car rental, bike rental, scooter rental
     3Tags: rental, booking, car rental, motorbike rental, scooter rental
    44Requires at least: 6.5
    55Tested up to: 7.1
    66Requires PHP: 8.3
    7 Stable tag: 1.0.0
     7Stable tag: 1.0.1
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    6868= Can I try Rentbaik before adding my own vehicles? =
    6969
    70 Yes. Click **Add sample data** in the setup wizard or on the Rentbaik dashboard. Rentbaik adds example vehicles marked "(sample)", units, extras and three bookings, without sending any email. **Remove sample data** deletes all of it again; your own vehicles and bookings are not touched.
     70Yes. Click **Add sample data** in the setup wizard or on the Rentbaik dashboard. Rentbaik adds example vehicles marked "(sample)" with pictures, units with their own photo, extras, a service day and a month of bookings in every common situation: picked up today, coming back today, late, part paid, deposit still to give back, waiting for a unit, waiting for confirmation with a transfer receipt, cancelled with a refund and not shown. Customer photos and receipts are drawings marked "SAMPLE", not real people or documents. No email is sent. **Remove sample data** deletes all of it again; your own vehicles and bookings are not touched.
     71
     72= Where are the pages for my customers? =
     73
     74Rentbaik creates the pages Rentals, Book a rental and My booking when it is activated. The Rentbaik dashboard lists them with their shortcodes and links to view or edit them, plus an example vehicle page. Add them to your site menu, or place the shortcodes on any page.
    7175
    7276= Do I need WooCommerce or Elementor? =
    … …  
    150154== Changelog ==
    151155
     156= 1.0.1 =
     157* Fix: the booking details panel and the day agenda on the Rentbaik dashboard did not open on sites with "Plain" permalinks ("The booking could not be loaded").
     158* New: "Your pages for customers" card on the Rentbaik dashboard. It lists the Rentals, Book a rental and My booking pages with their shortcodes and View and Edit links, an example vehicle page, and a link to see a sample booking as the customer sees it.
     159* New: labels on the calendar bars and in the day agenda: Late, Today (pick-up or return), Unpaid or Part paid, and Deposit (not given back yet).
     160* Improved: the sample data now fills the current month with 17 bookings in every common situation: completed, returned with the deposit still held, cancelled with a refund, no-show, late, coming back today, picked up today, without a unit, waiting with a transfer receipt, part paid and paid online.
     161* Improved: sample vehicles and units have pictures, and sample customers have photos and transfer receipts. All are drawings marked "SAMPLE", not real people or documents. Remove sample data deletes them too.
     162* Improved: when a vehicle type is switched off, the sample bookings use a similar vehicle.
     163* Translation: complete Indonesian translation.
     164* Tip: if you added sample data with 1.0.0, remove it and add it again to see the new example month.
     165
    152166= 1.0.0 =
    153167* First release.
    … …  
    155169== Upgrade Notice ==
    156170
     171= 1.0.1 =
     172Fixes the booking panel on sites with Plain permalinks. Adds a card with your customer pages, labels on the calendar and a full month of sample data with pictures.
     173
    157174= 1.0.0 =
    158175First release.
  • rentbaik/trunk/rentbaik.php

    r3790685 r3790901  
    33 * Plugin Name:       Rentbaik
    44 * Description:       Run a bike, scooter or car rental business in WordPress: fleet by plate number, rates, availability, bookings and local payment options. Works with any theme.
    5  * Version:           1.0.0
     5 * Version:           1.0.1
    66 * Requires at least: 6.5
    77 * Requires PHP:      8.3
    … …  
    1717defined( 'ABSPATH' ) || exit;
    1818
    19 define( 'RENTBAIK_VERSION', '1.0.0' );
     19define( 'RENTBAIK_VERSION', '1.0.1' );
    2020define( 'RENTBAIK_FILE', __FILE__ );
    2121define( 'RENTBAIK_PATH', plugin_dir_path( __FILE__ ) );
Note: See TracChangeset for help on using the changeset viewer.