diff --git a/actions-sms.php b/actions-sms.php index 757771f..8377498 100644 --- a/actions-sms.php +++ b/actions-sms.php @@ -650,7 +650,7 @@ function note($number,$bikeNum,$message) function last($number,$bike) { - + //@see \BikeShare\Repository\BikeRepository::findItemLastUsage global $db, $smsSender, $user; $userId = $user->findUserIdByNumber($number); $bikeNum = intval($bike); diff --git a/actions-web.php b/actions-web.php index d59ba28..ea32f60 100644 --- a/actions-web.php +++ b/actions-web.php @@ -122,76 +122,6 @@ function removenote($userId, $bikeNum) response(_('Note for bike') . ' ' . $bikeNum . ' ' . _('deleted') . '.'); } -function last($userId, $bike = 0) -{ - global $db; - $bikeNum = intval($bike); - if ($bikeNum) { - - $result = $db->query("SELECT note FROM notes WHERE bikeNum='$bikeNum' AND deleted IS NULL ORDER BY time DESC"); - $note = ''; - while ($row = $result->fetch_assoc()) { - $note .= $row['note'] . '; '; - } - $note = substr($note, 0, strlen($note) - 2); // remove last two chars - comma and space - if ($note) { - $note = _('Bike note:') . ' ' . $note; - } - - $historyInfo = '

' . _('Bike') . ' ' . $bikeNum . ' ' . _('history') . ':

' . $note . ''; - } else { - $result = $db->query("SELECT bikeNum FROM bikes WHERE currentUser<>''"); - $inuse = $result->num_rows; - $result = $db->query('SELECT bikeNum,userName,standName,users.userId FROM bikes LEFT JOIN users ON bikes.currentUser=users.userId LEFT JOIN stands ON bikes.currentStand=stands.standId ORDER BY bikeNum'); - $total = $result->num_rows; - $historyInfo = '

' . _('Current network usage:') . '

'; - $historyInfo .= '

' . sprintf(ngettext('%d bicycle', '%d bicycles', $total), $total) . ', ' . $inuse . ' ' . _('in use') . '

'; - } - response($historyInfo, 0, '', 0); -} - function userbikes($userId) { global $db, $auth; diff --git a/command.php b/command.php index ed2fed7..49b3a31 100644 --- a/command.php +++ b/command.php @@ -84,13 +84,6 @@ checkbikeno($bikeno); checkstandname($stand); $rentSystem->returnBike($userid, $bikeno, $stand, $note, TRUE); break; - case "where": - logrequest($userid,$action); - $auth->refreshSession(); - $bikeno=trim($_GET["bikeno"]); - checkbikeno($bikeno); - where($userid,$bikeno); - break; case "removenote": logrequest($userid,$action); $auth->refreshSession(); @@ -107,18 +100,6 @@ checkbikeno($bikeno); revert($userid,$bikeno); break; - case "last": - logrequest($userid,$action); - $auth->refreshSession(); - checkprivileges($userid); - if ($_GET["bikeno"]) - { - $bikeno=trim($_GET["bikeno"]); - checkbikeno($bikeno); - last($userid,$bikeno); - } - else last($userid); - break; case "stands": #"operationId": "stand.get", logrequest($userid,$action); $auth->refreshSession(); diff --git a/config/routes.php b/config/routes.php index d2d64bf..2b2f6ed 100644 --- a/config/routes.php +++ b/config/routes.php @@ -27,4 +27,15 @@ ->controller([\BikeShare\Controller\SecurityController::class, 'logout']); $routes->add('reset_password', '/resetPassword') ->controller([\BikeShare\Controller\SecurityController::class, 'resetPassword']); + + $routes->add('api_bike_index', '/api/bike') + ->methods(['GET']) + ->controller([\BikeShare\Controller\Api\BikeController::class, 'index']); + $routes->add('api_bike_item', '/api/bike/{bikeNumber}') + ->methods(['GET']) + ->controller([\BikeShare\Controller\Api\BikeController::class, 'item']); + $routes->add('api_bike_last_usage', '/api/bikeLastUsage/{bikeNumber}') + ->methods(['GET']) + ->controller([\BikeShare\Controller\Api\BikeController::class, 'lastUsage']); + }; \ No newline at end of file diff --git a/config/services.php b/config/services.php index a42290d..9bb7a31 100644 --- a/config/services.php +++ b/config/services.php @@ -111,4 +111,7 @@ $services->alias(CodeGeneratorInterface::class, CodeGenerator::class); $services->alias(PhonePurifierInterface::class, PhonePurifier::class); + + $services->load('BikeShare\\EventListener\\', '../src/EventListener') + ->tag('kernel.event_listener'); }; \ No newline at end of file diff --git a/public/js/admin.js b/public/js/admin.js index 77a4b1e..8af3ef8 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -4,15 +4,17 @@ $(document).ready(function () { $("#edituser").hide(); $("#where").click(function () { if (window.ga) ga('send', 'event', 'buttons', 'click', 'admin-where'); - where(); + bikeInfo($('#bikeNumber').val()); }); - $("#revert").click(function () { + $("#fleetconsole").on('click', '.bike-revert', function (event) { if (window.ga) ga('send', 'event', 'buttons', 'click', 'admin-revert'); - revert(); + revert($(this).data('bike-number')); + event.preventDefault(); }); - $("#last").click(function () { + $('#fleetconsole').on('click', '.bike-last-usage', function (event) { if (window.ga) ga('send', 'event', 'buttons', 'click', 'admin-last'); - last(); + last($(this).data('bike-number')); + event.preventDefault(); }); $("#stands").click(function () { if (window.ga) ga('send', 'event', 'buttons', 'click', 'admin-stands'); @@ -66,7 +68,7 @@ $(document).ready(function () { addcredit(10); return false; }); - last(); + bikeInfo(); }); function handleresponse(elementid, jsonobject, display) { @@ -76,24 +78,110 @@ function handleresponse(elementid, jsonobject, display) { } } -function where() { - if (window.ga) ga('send', 'event', 'bikes', 'where', $('#adminparam').val()); +function generateBikeCards(data) { + const $container = $("#fleetconsole"); + const $template = $("#bike-card-template"); + $container.empty(); + + $.each(data, function (index, item) { + const $card = $template.clone().removeAttr("id").removeClass("d-none"); + + const $bikeCard = $card.find(".bike-card"); + if (item.userName !== null) { + $bikeCard.addClass("bg-success text-white border-success"); + } else if (item.notes !== null) { + $bikeCard.addClass("bg-warning text-dark border-warning"); + } else { + $bikeCard.addClass("bg-light text-dark border-light"); + } + $card.attr("data-bike-number", item.bikeNum); + $card.find(".bike-last-usage").attr("data-bike-number", item.bikeNum); + $card.find(".bike-revert").attr("data-bike-number", item.bikeNum); + + $card.find(".bike-number").text(item.bikeNum); + + const $standInfo = $card.find(".stand-info"); + const $rentInfo = $card.find(".rent-info"); + if (item.userName !== null) { + $standInfo.addClass("d-none"); + $rentInfo.removeClass("d-none"); + $rentInfo.find(".user-name").text(item.userName); + $rentInfo.find(".rent-time").text(item.rentTime || "Unknown time"); + } else { + $standInfo.removeClass("d-none").find('.stand-name').text(item.standName); + $rentInfo.addClass("d-none"); + } + + if (item.isServiceStand == 1) { + $standInfo.find(".service-stand").removeClass("d-none"); + } + + const $noteInfo = $card.find(".note-info"); + if (item.notes) { + $noteInfo.removeClass("d-none"); + $noteInfo.find(".note-text").text(item.notes); + } else { + $noteInfo.addClass("d-none"); + } + + $container.append($card); + }); +} + +function bikeInfo(bikeNumber) { + if (window.ga) ga('send', 'event', 'bikes', 'where', bikeNumber); + $.ajax({ - url: "command.php?action=where&bikeno=" + $('#adminparam').val() - }).done(function (jsonresponse) { - jsonobject = $.parseJSON(jsonresponse); - handleresponse("fleetconsole", jsonobject); + url: "/api/bike" + (bikeNumber ? "/" + bikeNumber : ""), + method: "GET", + dataType: "json", + success: function(response) { + generateBikeCards(response); + }, + error: function(xhr, status, error) { + console.error("Error fetching bike data:", error); + } }); } -function last() { - if (window.ga) ga('send', 'event', 'bikes', 'last', $('#adminparam').val()); +function last(bikeNumber) { + if (window.ga) ga('send', 'event', 'bikes', 'last', bikeNumber); + $.ajax({ - url: "command.php?action=last&bikeno=" + $('#adminparam').val() - }).done(function (jsonresponse) { - jsonobject = $.parseJSON(jsonresponse); - handleresponse("fleetconsole", jsonobject); + url: "/api/bikeLastUsage/" + bikeNumber, + method: "GET", + dataType: "json", + success: function(data) { + $container = $("#bikeLastUsage .modal-body"); + $container.empty(); + + if (data.notes !== '') { + const $bikeUsageNotesTemplate = $("#bike-card-last_usage_notes_template"); + const $notes = $bikeUsageNotesTemplate.clone().removeClass("d-none"); + $notes.find("#note").text(data.notes); + $container.append($notes); + } + + $.each(data.history, function (index, item) { + const $template = $("#bike-card-last_usage_template"); + const $history = $template.clone().removeClass("d-none"); + $history.find("#time").text(item.time); + $history.find("#standName").text(item.standName); + $history.find("#userName").text(item.userName); + $history.find("#parameter").text(item.parameter); + $history.find("#action i").addClass("d-none"); + $history.find("#action ." + item.action).removeClass("d-none"); + + $container.append($history); + }); + + $('#bikeLastUsage').modal() + }, + error: function(xhr, status, error) { + console.error("Error fetching bike data:", error); + } }); + } function stands() { @@ -276,9 +364,9 @@ function sellcoupon(coupon) { } function trips() { - if (window.ga) ga('send', 'event', 'bikes', 'trips', $('#adminparam').val()); + if (window.ga) ga('send', 'event', 'bikes', 'trips', $('#bikeNumber').val()); $.ajax({ - url: "command.php?action=trips&bikeno=" + $('#adminparam').val() + url: "command.php?action=trips&bikeno=" + $('#bikeNumber').val() }).done(function (jsonresponse) { jsonobject = $.parseJSON(jsonresponse); if (jsonobject.error == 1) { @@ -311,10 +399,10 @@ function trips() { }); } -function revert() { - if (window.ga) ga('send', 'event', 'bikes', 'revert', $('#adminparam').val()); +function revert(bikeNumber) { + if (window.ga) ga('send', 'event', 'bikes', 'revert', bikeNumber); $.ajax({ - url: "command.php?action=revert&bikeno=" + $('#adminparam').val() + url: "command.php?action=revert&bikeno=" + bikeNumber }).done(function (jsonresponse) { jsonobject = $.parseJSON(jsonresponse); handleresponse("fleetconsole", jsonobject); diff --git a/src/Controller/AdminController.php b/src/Controller/AdminController.php index 2b7ba3e..b0c1f65 100644 --- a/src/Controller/AdminController.php +++ b/src/Controller/AdminController.php @@ -8,7 +8,6 @@ use BikeShare\Credit\CreditSystemInterface; use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; class AdminController extends AbstractController @@ -18,7 +17,6 @@ class AdminController extends AbstractController * @Route("/admin.php", name="admin_old") */ public function index( - Request $request, Configuration $configuration, CreditSystemInterface $creditSystem, LoggerInterface $logger @@ -27,8 +25,7 @@ public function index( $logger->info( 'User tried to access admin page without permission', [ - 'user' => $this->getUser()->getNumber(), - 'ip' => $request->getClientIp(), + 'user' => $this->getUser()->getUserIdentifier(), ] ); diff --git a/src/Controller/Api/BikeController.php b/src/Controller/Api/BikeController.php new file mode 100644 index 0000000..4ff5b8c --- /dev/null +++ b/src/Controller/Api/BikeController.php @@ -0,0 +1,93 @@ +isGranted('ROLE_ADMIN')) { + $logger->info( + 'User tried to access admin page without permission', + [ + 'user' => $this->getUser()->getUserIdentifier(), + ] + ); + + return $this->json([], Response::HTTP_FORBIDDEN); + } + + $bikes = $bikeRepository->findAll(); + + return $this->json($bikes); + } + + /** + * @Route("/bike/{bikeNumber}", name="api_bike_item", methods={"GET"}) + */ + public function item( + $bikeNumber, + BikeRepository $bikeRepository, + 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 (empty($bikeNumber) || !is_numeric($bikeNumber)) { + return $this->json([], Response::HTTP_BAD_REQUEST); + } + + $bikes = $bikeRepository->findItem((int)$bikeNumber); + + return $this->json($bikes); + } + + /** + * @Route("/bikeLastUsage/{bikeNumber}", name="api_bike_last_usage", methods={"GET"}) + */ + public function lastUsage( + $bikeNumber, + BikeRepository $bikeRepository, + 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 (empty($bikeNumber) || !is_numeric($bikeNumber)) { + return $this->json([], Response::HTTP_BAD_REQUEST); + } + + $bikes = $bikeRepository->findItemLastUsage((int)$bikeNumber); + + return $this->json($bikes); + } +} diff --git a/src/EventListener/ControllerEventListener.php b/src/EventListener/ControllerEventListener.php new file mode 100644 index 0000000..82d7a94 --- /dev/null +++ b/src/EventListener/ControllerEventListener.php @@ -0,0 +1,49 @@ +db = $db; + $this->security = $security; + } + + public function __invoke(ControllerEvent $event): void + { + if (!$event->isMainRequest()) { + return; + } + if (!in_array($event->getRequest()->attributes->get('_route'), self::LOGGED_ROUTES)) { + return; + } + + $number = $this->security->getUser()->getUserIdentifier(); + + $this->db->query(" + INSERT INTO received + SET sms_uuid='', + sender='$number', + receive_time='" . date('Y-m-d H:i:s') . "', + sms_text='" . $event->getRequest()->getRequestUri() . "', + ip='" . $event->getRequest()->getClientIp() . "' + "); + $this->db->commit(); + } +} diff --git a/src/Repository/BikeRepository.php b/src/Repository/BikeRepository.php new file mode 100644 index 0000000..f495685 --- /dev/null +++ b/src/Repository/BikeRepository.php @@ -0,0 +1,154 @@ +db = $db; + } + + public function findAll(): array + { + $bikes = $this->db->query( + 'SELECT + bikes.bikeNum, + currentUser as userId, + userName, + standName, + (standName REGEXP \'SERVIS$\') AS isServiceStand, + GROUP_CONCAT(note SEPARATOR \'; \') as notes, + null as rentTime + FROM bikes + LEFT JOIN users ON bikes.currentUser=users.userId + LEFT JOIN stands ON bikes.currentStand=stands.standId + LEFT JOIN notes ON bikes.bikeNum=notes.bikeNum AND notes.deleted IS NULL + GROUP BY bikeNum + ORDER BY bikeNum' + )->fetchAllAssoc(); + + foreach ($bikes as &$bike) { + /** + * Should be optimized to one query + */ + if (!empty($bike['userName'])) { + $historyInfo = $this->db->query(' + SELECT time + FROM history + WHERE bikeNum=' . $bike['bikeNum'] . ' + AND userId=' . $bike['userId'] . ' + AND action=\'RENT\' + ORDER BY time DESC + LIMIT 1 + ')->fetchAssoc(); + + $bike['rentTime'] = date('d/m H:i', strtotime($historyInfo['time'])); + } + } + unset($bike); + + return $bikes; + } + + public function findItem(int $bikeNumber): array + { + $bikes = $this->db->query( + 'SELECT + bikes.bikeNum, + currentUser as userId, + userName, + standName, + (standName REGEXP \'SERVIS$\') AS isServiceStand, + GROUP_CONCAT(note SEPARATOR \'; \') as notes, + null as rentTime + FROM bikes + LEFT JOIN users ON bikes.currentUser=users.userId + LEFT JOIN stands ON bikes.currentStand=stands.standId + LEFT JOIN notes ON bikes.bikeNum=notes.bikeNum AND notes.deleted IS NULL + WHERE bikes.bikeNum = ' . $bikeNumber . ' + GROUP BY bikeNum' + )->fetchAllAssoc(); + + foreach ($bikes as &$bike) { + /** + * Should be optimized to one query + */ + if (!empty($bike['userName'])) { + $historyInfo = $this->db->query(' + SELECT time + FROM history + WHERE bikeNum=' . $bike['bikeNum'] . ' + AND userId=' . $bike['userId'] . ' + AND action=\'RENT\' + ORDER BY time DESC + LIMIT 1 + ')->fetchAssoc(); + + $bike['rentTime'] = date('d/m H:i', strtotime($historyInfo['time'])); + } + } + unset($bike); + + return $bikes; + } + + public function findItemLastUsage(int $bikeNumber): array + { + $notes = $this->db->query(' + SELECT + GROUP_CONCAT(note ORDER BY time SEPARATOR \'; \') as notes + FROM notes + WHERE bikeNum=' . $bikeNumber . ' + AND deleted IS NULL + GROUP BY bikeNum + ')->fetchAssoc(); + + $history = $this->db->query(' + SELECT + userName, + parameter, + standName, + action, + time + FROM history + JOIN users ON history.userid=users.userid + LEFT JOIN stands ON stands.standid=history.parameter + WHERE bikenum= ' . $bikeNumber . ' + AND action NOT LIKE \'%CREDIT%\' + ORDER BY time DESC + LIMIT 10 + ')->fetchAllAssoc(); + + $result = []; + $result['notes'] = $notes['notes'] ?? ''; + $result['history'] = []; + foreach ($history as $row) { + $historyItem = []; + $historyItem['time'] = date('d/m H:i', strtotime($row['time'])); + $historyItem['action'] = $row['action']; + + if ($row['standName'] != null) { + $historyItem['standName'] = $row['standName']; + if (strpos($row['parameter'], '|')) { + $revertCode = explode('|', $row['parameter']); + $revertCode = $revertCode[1]; + } + if ($row['action'] == 'REVERT') { + $historyItem['parameter'] = str_pad($revertCode, 4, '0', STR_PAD_LEFT); + } + } else { + $historyItem['userName'] = $row['userName']; + $historyItem['parameter'] = str_pad($row['parameter'], 4, '0', STR_PAD_LEFT); + } + $result['history'][] = $historyItem; + } + + return $result; + } +} diff --git a/templates/admin/fleet.html.twig b/templates/admin/fleet.html.twig new file mode 100644 index 0000000..ebbaabb --- /dev/null +++ b/templates/admin/fleet.html.twig @@ -0,0 +1,83 @@ +{% block fleet %} +
+
+
+ +
+ +
+
+
+
+
+ +
+ {# bike card#} +
+
+
+
{{ 'Bike'|trans }}:
+
+ {{ 'Stand'|trans }}: + +
+

{{ 'Note'|trans }}:

+

{{ 'Rented by'|trans }}: {{ 'at'|trans }}:

+
+ +
+
+ + {# bike last usage modal#} + +
+ +
+
+
+ +
+
+
+
+ + + + + + + + + + +
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/admin/index.html.twig b/templates/admin/index.html.twig index dcd78f7..0c37524 100644 --- a/templates/admin/index.html.twig +++ b/templates/admin/index.html.twig @@ -69,23 +69,7 @@
-
-
- - - - -
- -
-
-
+ {% include ('admin/fleet.html.twig') %}
diff --git a/translations/messages+intl-icu.en.php b/translations/messages+intl-icu.en.php index f30c824..ae5d121 100644 --- a/translations/messages+intl-icu.en.php +++ b/translations/messages+intl-icu.en.php @@ -101,7 +101,7 @@ 'does not exist. If you need help, send:' => '', 'Stand name' => '', 'has not been recognized. Stands are marked by CAPITALLETTERS.' => '', - 'Stand' => '', + 'Stand' => 'Stand', 'does not exist.' => '', 'Error. More arguments needed, use command' => '', 'Your remaining credit:' => '', @@ -123,7 +123,7 @@ 'does not exist' => '', 'at' => '', 'used by' => '', - 'Note' => '', + 'Note' => 'Note', 'by' => '', 'returned to stand' => '', 'Make sure you set code to' => '', @@ -165,7 +165,7 @@ 'found. Revert not successful!' => '', 'Contact information conflict: This number already registered:' => '', 'Contact information is in incorrect format. Use:' => '', - 'User' => '', + 'User' => 'User', 'added. They need to read email and agree to rules before using the system.' => '', 'Sorry, this command is only available for the privileged users.' => '', 'account activation' => '', @@ -342,4 +342,5 @@ 'Status' => '', 'Mark as sold' => '', 'Rent bike {bikeNum} on stand {standName}' => 'Rent bike {bikeNum} on stand {standName}', + 'Rent time' => 'Rent time', ];