';
- $result = $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=$bikeNum AND (action NOT LIKE '%CREDIT%') ORDER BY time DESC LIMIT 10");
- while ($row = $result->fetch_assoc()) {
- $time = strtotime($row['time']);
- $historyInfo .= '
' . $row['bikeNum'] . ' - ';
- if ($row['standName'] != null) {
- $historyInfo .= $row['standName'];
- } else {
- $historyInfo .= '' . $row['userName'];
- $result2 = $db->query('SELECT time FROM history WHERE bikeNum=' . $row['bikeNum'] . ' AND userId=' . $row['userId'] . " AND action='RENT' ORDER BY time DESC");
- $row2 = $result2->fetch_assoc();
- $historyInfo .= ': ' . date('d/m H:i', strtotime($row2['time'])) . '';
- }
- $result2 = $db->query("SELECT note FROM notes WHERE bikeNum='" . $row['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) {
- $historyInfo .= ' (' . $note . ')';
- }
-
- $historyInfo .= '
';
- }
- $historyInfo .= '
';
- }
- 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#}
+
+
+
+
+
{{ 'Last usage'|trans }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'Note'|trans }}:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{% 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',
];