From f4ec05c878eedf24d97edd243fef1117d288ba81 Mon Sep 17 00:00:00 2001 From: Sveneld Date: Wed, 1 Jan 2025 13:23:57 +0100 Subject: [PATCH] Stands Controller with view of existing stands Report Controller Fix some issues --- actions-web.php | 50 ------ command.php | 18 --- config/routes.php | 21 ++- public/js/admin.js | 150 ++++++++++++++---- src/Controller/AdminController.php | 1 + src/Controller/Api/BikeController.php | 2 +- src/Controller/Api/ReportController.php | 68 ++++++++ src/Controller/Api/StandController.php | 38 +++++ src/Controller/Api/UserController.php | 4 +- src/Controller/PersonalStatsController.php | 13 +- src/Controller/ScanController.php | 3 +- src/EventListener/ControllerEventListener.php | 3 + src/Repository/HistoryRepository.php | 38 +++++ src/Repository/StandRepository.php | 3 +- templates/admin/index.html.twig | 18 +-- templates/admin/report.html.twig | 56 +++++++ templates/admin/stands.html.twig | 29 ++++ templates/admin/user.edit.html.twig | 38 +++++ templates/admin/user.html.twig | 37 +---- translations/messages+intl-icu.en.php | 6 + 20 files changed, 433 insertions(+), 163 deletions(-) create mode 100644 src/Controller/Api/ReportController.php create mode 100644 src/Controller/Api/StandController.php create mode 100644 templates/admin/report.html.twig create mode 100644 templates/admin/stands.html.twig create mode 100644 templates/admin/user.edit.html.twig diff --git a/actions-web.php b/actions-web.php index 5a7a8ccd..0c70dfa6 100644 --- a/actions-web.php +++ b/actions-web.php @@ -84,32 +84,6 @@ function listbikes($stand) response($bicycles, 0, array('notes' => $notes, 'stacktopbike' => $stacktopbike), 0); } -function liststands() -{ - global $db; - - response(_('not implemented'), 0, '', 0); - exit; - $result = $db->query('SELECT standId,standName,standDescription,standPhoto,serviceTag,placeName,longitude,latitude FROM stands ORDER BY standName'); - while ($row = $result->fetch_assoc()) { - $bikenum = $row['bikeNum']; - $result2 = $db->query("SELECT note FROM notes WHERE bikeNum='$bikenum' AND deleted IS NULL ORDER BY time DESC"); - $note = ''; - while ($row = $result2->fetch_assoc()) { - $note .= $row['note'] . '; '; - } - $note = substr($note, 0, strlen($note) - 2); // remove last two chars - comma and space - if ($note) { - $bicycles[] = '*' . $bikenum; // bike with note / issue - $notes[] = $note; - } else { - $bicycles[] = $bikenum; - $notes[] = ''; - } - } - response($stands, 0, '', 0); -} - function removenote($userId, $bikeNum) { global $db; @@ -301,30 +275,6 @@ function trips($userId, $bike = 0) echo json_encode($jsoncontent); // TODO change to response function } -function getuserstats() -{ - global $db; - $result = $db->query('SELECT users.userId,username,count(action) AS count FROM users LEFT JOIN history ON users.userId=history.userId WHERE history.userId IS NOT NULL GROUP BY username ORDER BY count DESC'); - while ($row = $result->fetch_assoc()) { - $result2 = $db->query("SELECT count(action) AS rentals FROM history WHERE action='RENT' AND userId=" . $row['userId']); - $row2 = $result2->fetch_assoc(); - $result2 = $db->query("SELECT count(action) AS returns FROM history WHERE action='RETURN' AND userId=" . $row['userId']); - $row3 = $result2->fetch_assoc(); - $jsoncontent[] = array('userid' => $row['userId'], 'username' => $row['username'], 'count' => $row['count'], 'rentals' => $row2['rentals'], 'returns' => $row3['returns']); - } - echo json_encode($jsoncontent); // TODO change to response function -} - -function getusagestats() -{ - global $db; - $result = $db->query("SELECT count(action) AS count,DATE(time) AS day,action FROM history WHERE userId IS NOT NULL AND action IN ('RENT','RETURN') GROUP BY day,action ORDER BY day DESC LIMIT 60"); - while ($row = $result->fetch_assoc()) { - $jsoncontent[] = array('day' => $row['day'], 'count' => $row['count'], 'action' => $row['action']); - } - echo json_encode($jsoncontent); // TODO change to response function -} - function validatecoupon($userid, $coupon) { global $db, $creditSystem; diff --git a/command.php b/command.php index 6a23acb4..81f16bc4 100644 --- a/command.php +++ b/command.php @@ -100,24 +100,6 @@ checkbikeno($bikeno); revert($userid,$bikeno); break; - case "stands": #"operationId": "stand.get", - logrequest($userid,$action); - $auth->refreshSession(); - checkprivileges($userid); - liststands(); - break; - case "userstats": - logrequest($userid,$action); - $auth->refreshSession(); - checkprivileges($userid); - getuserstats(); - break; - case "usagestats": - logrequest($userid,$action); - $auth->refreshSession(); - checkprivileges($userid); - getusagestats(); - break; case "trips": logrequest($userid,$action); $auth->refreshSession(); diff --git a/config/routes.php b/config/routes.php index 4e6545d0..ba2005e2 100644 --- a/config/routes.php +++ b/config/routes.php @@ -8,9 +8,9 @@ $routes->add('command', '/command.php') ->controller([\BikeShare\Controller\CommandController::class, 'index']); $routes->add('scan', '/scan.php/{action}/{id}') + ->requirements(['id' => '\d+']) + ->requirements(['action' => 'rent|return']) ->controller([\BikeShare\Controller\ScanController::class, 'index']); - $routes->add('admin_old', '/admin.php') - ->controller([\BikeShare\Controller\AdminController::class, 'index']); $routes->add('admin', '/admin') ->controller([\BikeShare\Controller\AdminController::class, 'index']); $routes->add('register', '/register.php') @@ -28,14 +28,19 @@ $routes->add('reset_password', '/resetPassword') ->controller([\BikeShare\Controller\SecurityController::class, 'resetPassword']); + $routes->add('api_stand_index', '/api/stand') + ->methods(['GET']) + ->controller([\BikeShare\Controller\Api\StandController::class, 'index']); $routes->add('api_bike_index', '/api/bike') ->methods(['GET']) ->controller([\BikeShare\Controller\Api\BikeController::class, 'index']); $routes->add('api_bike_item', '/api/bike/{bikeNumber}') + ->requirements(['bikeNumber' => '\d+']) ->methods(['GET']) ->controller([\BikeShare\Controller\Api\BikeController::class, 'item']); $routes->add('api_bike_last_usage', '/api/bikeLastUsage/{bikeNumber}') ->methods(['GET']) + ->requirements(['bikeNumber' => '\d+']) ->controller([\BikeShare\Controller\Api\BikeController::class, 'lastUsage']); $routes->add('api_coupon_index', '/api/coupon') ->methods(['GET']) @@ -51,15 +56,27 @@ ->controller([\BikeShare\Controller\Api\UserController::class, 'index']); $routes->add('api_user_item', '/api/user/{userId}') ->methods(['GET']) + ->requirements(['userId' => '\d+']) ->controller([\BikeShare\Controller\Api\UserController::class, 'item']); $routes->add('api_user_item_update', '/api/user/{userId}') ->methods(['PUT']) + ->requirements(['userId' => '\d+']) ->controller([\BikeShare\Controller\Api\UserController::class, 'update']); $routes->add('api_credit_add', '/api/credit') ->methods(['PUT']) ->controller([\BikeShare\Controller\Api\CreditController::class, 'add']); + $routes->add('api_report_daily', '/api/report/daily') + ->methods(['GET']) + ->controller([\BikeShare\Controller\Api\ReportController::class, 'daily']); + $routes->add('api_report_users', '/api/report/user/{year}') + ->methods(['GET']) + ->defaults(['year' => date('Y')]) + ->requirements(['year' => '\d+']) + ->controller([\BikeShare\Controller\Api\ReportController::class, 'user']); $routes->add('personal_stats_year', '/personalStats/year/{year}') ->methods(['GET']) + ->defaults(['year' => date('Y')]) + ->requirements(['year' => '\d+']) ->controller([\BikeShare\Controller\PersonalStatsController::class, 'yearStats']); }; \ No newline at end of file diff --git a/public/js/admin.js b/public/js/admin.js index 9eb2af72..47b78d17 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -69,7 +69,7 @@ $(document).ready(function () { stands(); break; case "#reports": - userstats(); + usagestats(); break; case "#credit": if (window.ga) ga('send', 'event', 'buttons', 'click', 'admin-couponlist'); @@ -189,12 +189,53 @@ function last(bikeNumber) { }); } -function stands() { +function generateStandCards(data) { + const $container = $("#standsconsole"); + const $template = $("#stand-card-template"); + $container.empty(); + + $.each(data, function (index, item) { + const $card = $template.clone().removeAttr("id").removeClass("d-none"); + + $card.find(".stand-name").text(item.standName); + + const $photo = $card.find(".stand-photo"); + if (item.standPhoto) { + $photo.attr("src", item.standPhoto).removeClass("d-none"); + } else { + $photo.addClass("d-none"); + } + + $card.find(".stand-description").text(item.standDescription); + + if (parseInt(item.latitude) !== 0 && parseInt(item.longitude) !== 0) { + const googleMapsUrl = `https://www.google.com/maps?q=${item.latitude},${item.longitude}`; + $card.find(".stand-location") + .removeClass("d-none") + .attr("href", googleMapsUrl); + } + + if (item.standName.toLowerCase().includes("servis")) { + $card.find(".service-stand").removeClass("d-none"); + } else if (item.standName.toLowerCase().includes("zruseny")) { + $card.find(".removed-stand").removeClass("d-none"); + } + + $container.append($card); + }); +} + +function stands(standId) { $.ajax({ - url: "command.php?action=stands" - }).done(function (jsonresponse) { - jsonobject = $.parseJSON(jsonresponse); - handleresponse("standsconsole", jsonobject); + url: "/api/stand" + (standId ? "/" + standId : ""), + method: "GET", + dataType: "json", + success: function(response) { + generateStandCards(response); + }, + error: function(xhr, status, error) { + console.error("Error fetching stand data:", error); + } }); } @@ -207,7 +248,9 @@ function userlist() { dataSrc: '', cache: true }, - dom: 'lrtip', + layout: { + topEnd: null //disable default searchField + }, columns: [ { data: 'username', @@ -220,15 +263,18 @@ function userlist() { }, { data: 'privileges', - name: 'privileges' + name: 'privileges', + type: 'num' }, { data: 'userLimit', - name: 'userLimit' + name: 'userLimit', + type: 'num' }, { data: 'credit', name: 'credit', + type: 'num-fmt', visible: creditenabled === 1, render: function(data, type, row) { return `${data} ${creditcurrency}`; @@ -260,37 +306,73 @@ function userlist() { } function userstats() { - var code = ""; - $.ajax({ - url: "command.php?action=userstats" - }).done(function (jsonresponse) { - jsonobject = $.parseJSON(jsonresponse); - if (jsonobject.length > 0) code = ''; - for (var i = 0, len = jsonobject.length; i < len; i++) { - code = code + ''; + $('#report-daily-table').addClass('d-none').closest('#stats-report-table_wrapper').addClass('d-none'); + $('#report-user-year').removeClass('d-none'); + let table = $('#report-user-table').removeClass('d-none').DataTable({ + destroy: true, + paging: false, + info: false, + searching: false, + ajax: { + url: '/api/report/user/', + dataSrc: '', + cache: true, + }, + order: [[3, 'desc']], + columns: [ + { + data: 'username', + name: 'username', + }, + { + data: 'rentCount', + }, + { + data: 'returnCount', + }, + { + data: 'totalActionCount', + } + ], + error: function(xhr, error, code) { + console.error('Error loading daily report data:', error); } - if (jsonobject.length > 0) code = code + '
UserActionsRentalsReturns
' + jsonobject[i]["username"] + '' + jsonobject[i]["count"] + '' + jsonobject[i]["rentals"] + '' + jsonobject[i]["returns"] + '
'; - $('#reportsconsole').html(code); - $('#userstatstable').dataTable({ - "paging": false, - "ordering": false, - "info": false - }); + }); + + $('#year').on('change', function() { + table.ajax.url('/api/report/user/' + $('#year').val()); + table.ajax.reload(); }); } function usagestats() { - var code = ""; - $.ajax({ - url: "command.php?action=usagestats" - }).done(function (jsonresponse) { - jsonobject = $.parseJSON(jsonresponse); - if (jsonobject.length > 0) code = ''; - for (var i = 0, len = jsonobject.length; i < len; i++) { - code = code + ''; + $('#report-user-table').addClass('d-none').closest('#report-user-table_wrapper').addClass('d-none'); + $('#report-user-year').addClass('d-none'); + $('#report-daily-table').removeClass('d-none').DataTable({ + destroy: true, + paging: false, + info: false, + searching: false, + ajax: { + url: '/api/report/daily', + dataSrc: '', + cache: true, + }, + order: [[0, 'desc']], + columns: [ + { + data: 'day', + }, + { + data: 'rentCount', + }, + { + data: 'returnCount', + } + ], + error: function(xhr, error, code) { + console.error('Error loading user report data:', error); } - if (jsonobject.length > 0) code = code + '
DayActionCount
' + jsonobject[i]["day"] + '' + jsonobject[i]["action"] + '' + jsonobject[i]["count"] + '
'; - $('#reportsconsole').html(code); }); } diff --git a/src/Controller/AdminController.php b/src/Controller/AdminController.php index b0c1f65f..6fa2da24 100644 --- a/src/Controller/AdminController.php +++ b/src/Controller/AdminController.php @@ -37,6 +37,7 @@ public function index( [ 'configuration' => $configuration, 'creditSystem' => $creditSystem, + 'currentYear' => date('Y'), ] ); } diff --git a/src/Controller/Api/BikeController.php b/src/Controller/Api/BikeController.php index 04c38e4b..1d7cce52 100644 --- a/src/Controller/Api/BikeController.php +++ b/src/Controller/Api/BikeController.php @@ -36,7 +36,7 @@ public function index( } /** - * @Route("/api/bike/{bikeNumber}", name="api_bike_item", methods={"GET"}) + * @Route("/api/bike/{bikeNumber}", name="api_bike_item", methods={"GET"}, requirements: {"bikeNumber"="\d+"}) */ public function item( $bikeNumber, diff --git a/src/Controller/Api/ReportController.php b/src/Controller/Api/ReportController.php new file mode 100644 index 00000000..ef19443a --- /dev/null +++ b/src/Controller/Api/ReportController.php @@ -0,0 +1,68 @@ +isGranted('ROLE_ADMIN')) { + $logger->info( + 'User tried to access admin page without permission', + [ + 'user' => $this->getUser()->getUserIdentifier(), + ] + ); + + return $this->json([], Response::HTTP_FORBIDDEN); + } + + $stats = $historyRepository->dailyStats(); + + return $this->json($stats); + } + /** + * @Route("/report/user/{year}", name="api_report_user", , requirements: ['year' => '\d+'] methods={"GET"}) + */ + public function user( + $year = 1900, + HistoryRepository $historyRepository, + LoggerInterface $logger + ): Response { + if (!$this->isGranted('ROLE_ADMIN')) { + $logger->info( + 'User tried to access admin page without permission', + [ + 'user' => $this->getUser()->getUserIdentifier(), + ] + ); + + return $this->json([], Response::HTTP_FORBIDDEN); + } + if ($year === 1900) { + $year = (int)date('Y'); + } elseif ( + $year > (int)date('Y') + || $year < 2010 + ) { + return $this->json([], Response::HTTP_BAD_REQUEST); + } + + $stats = $historyRepository->userStats((int)$year); + + return $this->json($stats); + } +} diff --git a/src/Controller/Api/StandController.php b/src/Controller/Api/StandController.php new file mode 100644 index 00000000..0d6ece7e --- /dev/null +++ b/src/Controller/Api/StandController.php @@ -0,0 +1,38 @@ +isGranted('ROLE_ADMIN')) { + $logger->info( + 'User tried to access admin page without permission', + [ + 'user' => $this->getUser()->getUserIdentifier(), + ] + ); + + return $this->json([], Response::HTTP_FORBIDDEN); + } + + $bikes = $standRepository->findAll(); + + return $this->json($bikes); + } +} diff --git a/src/Controller/Api/UserController.php b/src/Controller/Api/UserController.php index 3d9af52a..8a43e157 100644 --- a/src/Controller/Api/UserController.php +++ b/src/Controller/Api/UserController.php @@ -38,7 +38,7 @@ public function index( } /** - * @Route("/api/user/{userId}", name="api_user_item", methods={"GET"}) + * @Route("/api/user/{userId}", name="api_user_item", methods={"GET"}, requirements: {"userId"="\d+"}) */ public function item( $userId, @@ -66,7 +66,7 @@ public function item( } /** - * @Route("/api/user/{userId}", name="api_user_item_update", methods={"PUT"}) + * @Route("/api/user/{userId}", name="api_user_item_update", methods={"PUT"}, requirements: {"userId"="\d+"}) */ public function update( $userId, diff --git a/src/Controller/PersonalStatsController.php b/src/Controller/PersonalStatsController.php index 4a7d6135..ff2ac09f 100644 --- a/src/Controller/PersonalStatsController.php +++ b/src/Controller/PersonalStatsController.php @@ -14,14 +14,23 @@ class PersonalStatsController extends AbstractController { /** - * @Route("/personalStats/year/{year}", name="personal_stats_year", methods={"GET"}) + * @Route("/personalStats/year/{year}", name="personal_stats_year", methods={"GET"}, requirements: {"year"="\d+"}) */ public function yearStats( - $year, + $year = 1900, StatsRepository $statsRepository, StandRepository $standRepository, User $user ): Response { + if ($year === 1900) { + $year = (int)date('Y'); + } elseif ( + $year > (int)date('Y') + || $year < 2010 + ) { + return new Response('', Response::HTTP_BAD_REQUEST); + } + $userId = $user->findUserIdByNumber($this->getUser()->getUserIdentifier()); $stats = $statsRepository->getUserStatsForYear((int)$userId, (int)$year); $stands = $standRepository->findAll(); diff --git a/src/Controller/ScanController.php b/src/Controller/ScanController.php index f4907a6f..0f162b64 100644 --- a/src/Controller/ScanController.php +++ b/src/Controller/ScanController.php @@ -6,6 +6,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Annotation\Route; class ScanController extends AbstractController { @@ -17,7 +18,7 @@ public function __construct(Kernel $kernel) } /** - * @Route("/scan.php/{action}/{id}", name="scan") + * @Route("/scan.php/{action}/{id}", name="scan", requirements={"action"="rent|return"}, requirements: {"id"="\d+"}) */ public function index( Request $request diff --git a/src/EventListener/ControllerEventListener.php b/src/EventListener/ControllerEventListener.php index 88326461..d3f343af 100644 --- a/src/EventListener/ControllerEventListener.php +++ b/src/EventListener/ControllerEventListener.php @@ -11,6 +11,7 @@ class ControllerEventListener { private const LOGGED_ROUTES = [ + 'api_stand_index', 'api_bike_index', 'api_bike_item', 'api_bike_last_usage', @@ -21,6 +22,8 @@ class ControllerEventListener 'api_user_item', 'api_user_item_update', 'api_credit_add', + 'api_report_daily', + 'api_report_user', ]; private DbInterface $db; diff --git a/src/Repository/HistoryRepository.php b/src/Repository/HistoryRepository.php index 879c084f..177cfe35 100644 --- a/src/Repository/HistoryRepository.php +++ b/src/Repository/HistoryRepository.php @@ -32,4 +32,42 @@ public function addItem( VALUES ($userId, $bikeNum, '$action', '$parameter') "); } + + public function dailyStats(): array + { + $result = $this->db->query( + "SELECT + DATE(time) AS day, + SUM(CASE WHEN action = 'RENT' THEN 1 ELSE 0 END) AS rentCount, + SUM(CASE WHEN action = 'RETURN' THEN 1 ELSE 0 END) AS returnCount + FROM history + WHERE userId IS NOT NULL + AND action IN ('RENT','RETURN') + GROUP BY day + ORDER BY day DESC + LIMIT 60 + ")->fetchAllAssoc(); + + return $result; + } + + public function userStats(int $year): array + { + $result = $this->db->query( + "SELECT + users.userId, + username, + SUM(CASE WHEN action = 'RENT' THEN 1 ELSE 0 END) AS rentCount, + SUM(CASE WHEN action = 'RETURN' THEN 1 ELSE 0 END) AS returnCount, + COUNT(action) AS totalActionCount + FROM users + LEFT JOIN history ON users.userId=history.userId + WHERE history.userId IS NOT NULL + AND YEAR(time) = " . $year . " + GROUP BY username + ORDER BY totalActionCount DESC + ")->fetchAllAssoc(); + + return $result; + } } diff --git a/src/Repository/StandRepository.php b/src/Repository/StandRepository.php index 1b4fe732..0a1c1428 100644 --- a/src/Repository/StandRepository.php +++ b/src/Repository/StandRepository.php @@ -28,7 +28,8 @@ public function findAll(): array placeName, longitude, latitude - FROM stands' + FROM stands + ORDER BY standName' )->fetchAllAssoc(); $stands = []; diff --git a/templates/admin/index.html.twig b/templates/admin/index.html.twig index 6cb8977c..1ce06650 100644 --- a/templates/admin/index.html.twig +++ b/templates/admin/index.html.twig @@ -71,11 +71,7 @@ {% include ('admin/fleet.html.twig') %}
-
-
-
-
-
+ {% include ('admin/stands.html.twig') %}
{% include ('admin/user.html.twig') %} @@ -86,17 +82,7 @@
{% endif %}
-
-
- - -
-
-
+ {% include ('admin/report.html.twig') %}
diff --git a/templates/admin/report.html.twig b/templates/admin/report.html.twig new file mode 100644 index 00000000..99e00e93 --- /dev/null +++ b/templates/admin/report.html.twig @@ -0,0 +1,56 @@ +{% block report %} +
+
+ + +
+
+
+ + + + + + + + + + +
{{ 'Day'|trans }}{{ 'Rent'|trans }}{{ 'Return'|trans }}
+
+
+
+
+ +
+ +
+
+
+ + + + + + + + + + + +
{{ 'User'|trans }}{{ 'Rents'|trans }}{{ 'Returns'|trans }}{{ 'Total Actions'|trans }}
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/admin/stands.html.twig b/templates/admin/stands.html.twig new file mode 100644 index 00000000..59271548 --- /dev/null +++ b/templates/admin/stands.html.twig @@ -0,0 +1,29 @@ +{% block stands %} +
+
+
+ +
+ {# stand card#} +
+
+
+ + + +
+ +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/admin/user.edit.html.twig b/templates/admin/user.edit.html.twig new file mode 100644 index 00000000..5c125c0d --- /dev/null +++ b/templates/admin/user.edit.html.twig @@ -0,0 +1,38 @@ +{% block user_edit %} +
+
+ + +
+
+ + +
+ {% if configuration.get('connectors')['sms'] %} +
+ + +
+ {% endif %} +
+ + +
+
+ + +
+ + + + {% if creditSystem.isEnabled %} + or + {% set multipliers = [1, 5, 10] %} + {% for multiplier in multipliers %} + + {% endfor %} + {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/templates/admin/user.html.twig b/templates/admin/user.html.twig index 44b8401d..cb119688 100644 --- a/templates/admin/user.html.twig +++ b/templates/admin/user.html.twig @@ -1,40 +1,5 @@ {% block user %} -
-
- - -
-
- - -
- {% if configuration.get('connectors')['sms'] %} -
- - -
- {% endif %} -
- - -
-
- - -
- - - - {% if creditSystem.isEnabled %} - or - {% set multipliers = [1, 5, 10] %} - {% for multiplier in multipliers %} - - {% endfor %} - {% endif %} -
+ {% include ('admin/user.edit.html.twig') %}
diff --git a/translations/messages+intl-icu.en.php b/translations/messages+intl-icu.en.php index da3dc585..57440d2d 100644 --- a/translations/messages+intl-icu.en.php +++ b/translations/messages+intl-icu.en.php @@ -15,6 +15,7 @@ 'Login' => 'Login', 'Keep me logged in' => 'Keep me logged in', 'Choose bike number and rent bicycle. You will receive a code to unlock the bike and the new code to set.' => 'Choose bike number and rent bicycle. You will receive a code to unlock the bike and the new code to set.', + 'Day' => 'Day', 'Rent' => 'Rent', 'Describe problem' => 'Describe problem', 'Return this bicycle to the selected stand.' => 'Return this bicycle to the selected stand.', @@ -387,4 +388,9 @@ =0 { day } =1 { days } }', + 'Rents' => 'Rents', + 'Returns' => 'Returns', + 'Total Actions' => 'Total Actions', + 'Year' => 'Year', + 'View on Google Maps' => 'View on Google Maps', ];