From 411023db08cac23d73f2e0e8311d74e8228eb093 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arkadiusz=20Kury=C5=82owicz?= Date: Fri, 22 Nov 2024 10:33:38 +0100 Subject: [PATCH] Base version of Rate Our Service --- npm-package/css/layout.css | 50 ++- src/Serwisant/SerwisantCp/Actions/Repairs.php | 50 ++- src/Serwisant/SerwisantCp/Actions/Tickets.php | 49 ++- src/Serwisant/SerwisantCp/RoutesCp.php | 406 ++++++------------ .../queries/customer/repairs.graphql | 7 + .../queries/customer/schema.graphql | 25 ++ .../queries/customer/setRating.graphql | 11 + .../queries/customer/tickets.graphql | 7 + .../SerwisantCp/queries/public/schema.graphql | 18 + src/Serwisant/SerwisantCp/translations/pl.yml | 10 + .../SerwisantCp/views/repair.html.twig | 6 +- .../SerwisantCp/views/shared/rating.html.twig | 59 +++ .../SerwisantCp/views/ticket.html.twig | 3 + 13 files changed, 412 insertions(+), 289 deletions(-) create mode 100644 src/Serwisant/SerwisantCp/queries/customer/setRating.graphql create mode 100644 src/Serwisant/SerwisantCp/views/shared/rating.html.twig diff --git a/npm-package/css/layout.css b/npm-package/css/layout.css index ecc1e4e..f5a9ae6 100644 --- a/npm-package/css/layout.css +++ b/npm-package/css/layout.css @@ -177,4 +177,52 @@ textarea { .select2-ctl-dropdown li { font-size: .9rem !important; -} \ No newline at end of file +} + +/* rating */ + +.rating { + display: flex; + flex-direction: row-reverse; + justify-content: center +} + +.rating>input { + display: none +} + +.rating>label { + position: relative; + width: 1em; + font-size: 30px; + font-weight: 300; + color: #FFD600; + cursor: pointer +} + +.rating>span { + position: relative; + width: 1em; + font-size: 30px; + font-weight: 300; + color: #FFD600; +} + +.rating>label::before { + content: "\2605"; + position: absolute; + opacity: 0 +} + +.rating>label:hover:before, +.rating>label:hover~label:before { + opacity: 1 !important +} + +.rating>input:checked~label:before { + opacity: 1 +} + +.rating:hover>input:checked~label:before { + opacity: 0.4 +} diff --git a/src/Serwisant/SerwisantCp/Actions/Repairs.php b/src/Serwisant/SerwisantCp/Actions/Repairs.php index fb80cd6..364237b 100644 --- a/src/Serwisant/SerwisantCp/Actions/Repairs.php +++ b/src/Serwisant/SerwisantCp/Actions/Repairs.php @@ -11,11 +11,14 @@ use Serwisant\SerwisantApi\Types\SchemaCustomer\RepairsFilter; use Serwisant\SerwisantApi\Types\SchemaCustomer\RepairsFilterType; +use Serwisant\SerwisantApi\Types\SchemaCustomer\RepairTransportType; use Serwisant\SerwisantApi\Types\SchemaCustomer\RepairsSort; use Serwisant\SerwisantApi\Types\SchemaCustomer\RepairInput; use Serwisant\SerwisantApi\Types\SchemaCustomer\AddressUpdateInput; use Serwisant\SerwisantApi\Types\SchemaCustomer\PrintType; -use Serwisant\SerwisantApi\Types\SchemaCustomer\RepairTransportType; + +use Serwisant\SerwisantApi\Types\SchemaCustomer\RatingSubjectType; +use Serwisant\SerwisantApi\Types\SchemaCustomer\RatingInput; class Repairs extends Action { @@ -43,19 +46,17 @@ public function index() return $this->renderPage('repairs.html.twig', $variables); } - public function show($id) + public function show($id, $rating_errors = []) { $this->checkModuleActive(); - $filter = new RepairsFilter(['type' => RepairsFilterType::ID, 'ID' => $id]); - $result = $this->apiCustomer()->customerQuery()->repairs(1, null, $filter, null, ['single' => true]); - if (count($result->items) !== 1) { - throw new ExceptionNotFound(__CLASS__, __LINE__); - } + $repair = $this->fetchRepair($id); $variables = [ - 'repair' => $result->items[0], - 'pageTitle' => $result->items[0]->rma, + 'repair' => $repair, + 'pageTitle' => $repair->rma, + 'form_params' => $this->request->request, + 'rating_errors' => $rating_errors, ]; return $this->renderPage('repair.html.twig', $variables); @@ -212,6 +213,37 @@ public function create() } } + public function rate($id) + { + $this->checkModuleActive(); + + $repair = $this->fetchRepair($id); + + if (false === $repair->isRateable) { + throw new ExceptionNotFound(__CLASS__, __LINE__); + } + + $rating_input = new RatingInput($this->request->get('rating', [])); + + $result = $this->apiCustomer()->customerMutation()->setRating($id, RatingSubjectType::REPAIR, $rating_input); + + if ($result->errors) { + return $this->show($id, $result->errors); + } else { + return $this->redirectTo(['repair', ['id' => $id]]); + } + } + + private function fetchRepair($id) + { + $filter = new RepairsFilter(['type' => RepairsFilterType::ID, 'ID' => $id]); + $result = $this->apiCustomer()->customerQuery()->repairs(1, null, $filter, null, ['single' => true]); + if (count($result->items) !== 1) { + throw new ExceptionNotFound(__CLASS__, __LINE__); + } + return $result->items[0]; + } + private function checkModuleActive() { $this->checkPanelActive(); diff --git a/src/Serwisant/SerwisantCp/Actions/Tickets.php b/src/Serwisant/SerwisantCp/Actions/Tickets.php index 94c438a..0f4adb3 100644 --- a/src/Serwisant/SerwisantCp/Actions/Tickets.php +++ b/src/Serwisant/SerwisantCp/Actions/Tickets.php @@ -7,13 +7,15 @@ use Serwisant\SerwisantCp\ExceptionNotFound; use Serwisant\SerwisantApi\Types\SchemaCustomer\AddressUpdateInput; -use Serwisant\SerwisantApi\Types\SchemaCustomer\RepairsFilterType; use Serwisant\SerwisantApi\Types\SchemaCustomer\TicketsFilter; use Serwisant\SerwisantApi\Types\SchemaCustomer\TicketsFilterType; use Serwisant\SerwisantApi\Types\SchemaCustomer\TicketsSort; use Serwisant\SerwisantApi\Types\SchemaCustomer\TicketInput; use Serwisant\SerwisantApi\Types\SchemaCustomer\PrintType; +use Serwisant\SerwisantApi\Types\SchemaCustomer\RatingSubjectType; +use Serwisant\SerwisantApi\Types\SchemaCustomer\RatingInput; + class Tickets extends Action { use Traits\Devices; @@ -39,19 +41,17 @@ public function index() return $this->renderPage('tickets.html.twig', $variables); } - public function show($id) + public function show($id, $rating_errors = []) { $this->checkModuleActive(); - $filter = new TicketsFilter(['type' => RepairsFilterType::ID, 'ID' => $id]); - $result = $this->apiCustomer()->customerQuery()->tickets(1, null, $filter, null, ['single' => true]); - if (count($result->items) !== 1) { - throw new ExceptionNotFound(__CLASS__, __LINE__); - } + $ticket = $this->fetchTicket($id); $variables = [ - 'ticket' => $result->items[0], - 'pageTitle' => $result->items[0]->number, + 'ticket' => $ticket, + 'pageTitle' => $ticket->number, + 'form_params' => $this->request->request, + 'rating_errors' => $rating_errors, ]; return $this->renderPage('ticket.html.twig', $variables); @@ -167,6 +167,37 @@ public function create() } } + public function rate($id) + { + $this->checkModuleActive(); + + $ticket = $this->fetchTicket($id); + + if (false === $ticket->isRateable) { + throw new ExceptionNotFound(__CLASS__, __LINE__); + } + + $rating_input = new RatingInput($this->request->get('rating', [])); + + $result = $this->apiCustomer()->customerMutation()->setRating($id, RatingSubjectType::TICKET, $rating_input); + + if ($result->errors) { + return $this->show($id, $result->errors); + } else { + return $this->redirectTo(['ticket', ['id' => $id]]); + } + } + + private function fetchTicket($id) + { + $filter = new TicketsFilter(['type' => TicketsFilterType::ID, 'ID' => $id]); + $result = $this->apiCustomer()->customerQuery()->tickets(1, null, $filter, null, ['single' => true]); + if (count($result->items) !== 1) { + throw new ExceptionNotFound(__CLASS__, __LINE__); + } + return $result->items[0]; + } + private function checkModuleActive() { $this->checkPanelActive(); diff --git a/src/Serwisant/SerwisantCp/RoutesCp.php b/src/Serwisant/SerwisantCp/RoutesCp.php index a8aaefb..def33cb 100644 --- a/src/Serwisant/SerwisantCp/RoutesCp.php +++ b/src/Serwisant/SerwisantCp/RoutesCp.php @@ -24,25 +24,43 @@ public function getRoutes() return $cp; } + private function routeRequirements($template_binding, $cp) + { + return $cp + ->before($this->expectAccessTokens()) + ->before($this->expectAuthenticated()) + ->assert('token', $this->tokenAssertion()) + ->convert('token', $this->tokenConverter()) + ->bind($template_binding); + } + + private function routeIdRequirements($template_binding, $cp) + { + return $this->routeRequirements($template_binding, $cp)->assert('id', $this->hashIdAssertion()); + } + + private function routeNoAuthRequirements($template_binding, $cp) + { + return $cp + ->before($this->expectAccessTokens()) + ->assert('token', $this->tokenAssertion()) + ->convert('token', $this->tokenConverter()) + ->bind($template_binding); + } + /** * @param $cp * @return void */ private function calendarRoutes($cp): void { - $cp->get('/calendar/schedule_dates', function (Request $request, Token $token) { + $this->routeRequirements('calendar_schedule_dates', $cp->get('/calendar/schedule_dates', function (Request $request, Token $token) { return (new Actions\Calendar($this->app, $request, $token))->scheduleDates(); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->bind('calendar_schedule_dates'); + })); - $cp->get('/calendar/ticket_dates', function (Request $request, Token $token) { + $this->routeRequirements('calendar_ticket_dates', $cp->get('/calendar/ticket_dates', function (Request $request, Token $token) { return (new Actions\Calendar($this->app, $request, $token))->ticketDates(); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->bind('calendar_ticket_dates'); + })); } /** @@ -52,34 +70,21 @@ private function calendarRoutes($cp): void */ private function devicesRoutes($cp): void { - $cp->get('/devices', function (Request $request, Token $token) { + $this->routeRequirements('devices', $cp->get('/devices', function (Request $request, Token $token) { return (new Actions\Devices($this->app, $request, $token))->index(); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->bind('devices'); + })); - $cp->get('/device/{id}', function (Request $request, Token $token, $id) { + $this->routeIdRequirements('device', $cp->get('/device/{id}', function (Request $request, Token $token, $id) { return (new Actions\Devices($this->app, $request, $token))->show($id); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->assert('id', $this->hashIdAssertion()) - ->bind('device'); + })); - $cp->get('/devices/create', function (Request $request, Token $token) { + $this->routeRequirements('new_device', $cp->get('/devices/create', function (Request $request, Token $token) { return (new Actions\Devices($this->app, $request, $token))->new(); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->bind('new_device'); + })); - $cp->post('/devices/create', function (Request $request, Token $token) { + $this->routeRequirements('create_device', $cp->post('/devices/create', function (Request $request, Token $token) { return (new Actions\Devices($this->app, $request, $token))->create(); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->bind('create_device'); + })); } /** @@ -88,50 +93,29 @@ private function devicesRoutes($cp): void */ private function messagesRoutes($cp): void { - $cp->get('/messages', function (Request $request, Token $token) { + $this->routeRequirements('messages', $cp->get('/messages', function (Request $request, Token $token) { return (new Actions\Messages($this->app, $request, $token))->index(); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->bind('messages'); + })); - $cp->get('/message/{id}', function (Request $request, Token $token, $id) { + $this->routeIdRequirements('message', $cp->get('/message/{id}', function (Request $request, Token $token, $id) { return (new Actions\Messages($this->app, $request, $token))->show($id); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->assert('id', $this->hashIdAssertion()) - ->bind('message'); + })); - $cp->get('/messages/create', function (Request $request, Token $token) { + $this->routeRequirements('new_message', $cp->get('/messages/create', function (Request $request, Token $token) { return (new Actions\Messages($this->app, $request, $token))->new(); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->bind('new_message'); + })); - $cp->post('/messages/create', function (Request $request, Token $token) { + $this->routeRequirements('create_message', $cp->post('/messages/create', function (Request $request, Token $token) { return (new Actions\Messages($this->app, $request, $token))->create(); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->bind('create_message'); + })); - $cp->get('/message/{id}/create', function (Request $request, Token $token, $id) { + $this->routeIdRequirements('new_message_reply', $cp->get('/message/{id}/create', function (Request $request, Token $token, $id) { return (new Actions\Messages($this->app, $request, $token))->newReply($id); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->assert('id', $this->hashIdAssertion()) - ->bind('new_message_reply'); + })); - $cp->post('/message/{id}/create', function (Request $request, Token $token, $id) { + $this->routeIdRequirements('create_message_reply', $cp->post('/message/{id}/create', function (Request $request, Token $token, $id) { return (new Actions\Messages($this->app, $request, $token))->createReply($id); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->assert('id', $this->hashIdAssertion()) - ->bind('create_message_reply'); + })); } /** @@ -140,27 +124,17 @@ private function messagesRoutes($cp): void */ private function miscRoutes($cp): void { - $cp->get('/', function (Request $request, Token $token) { + $this->routeRequirements('dashboard', $cp->get('/', function (Request $request, Token $token) { return (new Actions\Dashboard($this->app, $request, $token))->index(); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->bind('dashboard'); + })); - $cp->get('/ac/vendor', function (Request $request, Token $token) { + $this->routeRequirements('autocomplete_vendor', $cp->get('/ac/vendor', function (Request $request, Token $token) { return (new Actions\Autocomplete($this->app, $request, $token))->vendor(); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion()) - ->convert('token', $this->tokenConverter()) - ->bind('autocomplete_vendor'); + })); - $cp->get('/ac/model', function (Request $request, Token $token) { + $this->routeRequirements('autocomplete_model', $cp->get('/ac/model', function (Request $request, Token $token) { return (new Actions\Autocomplete($this->app, $request, $token))->model(); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->bind('autocomplete_model'); + })); } /** @@ -170,42 +144,29 @@ private function miscRoutes($cp): void */ private function ticketsRoutes($cp): void { - $cp->get('/tickets', function (Request $request, Token $token) { + $this->routeRequirements('tickets', $cp->get('/tickets', function (Request $request, Token $token) { return (new Actions\Tickets($this->app, $request, $token))->index(); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->bind('tickets'); + })); - $cp->get('/ticket/{id}', function (Request $request, Token $token, $id) { + $this->routeIdRequirements('ticket', $cp->get('/ticket/{id}', function (Request $request, Token $token, $id) { return (new Actions\Tickets($this->app, $request, $token))->show($id); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->assert('id', $this->hashIdAssertion()) - ->bind('ticket'); + })); - $cp->get('/ticket/{id}/print', function (Request $request, Token $token, $id) { + $this->routeIdRequirements('ticket_print', $cp->get('/ticket/{id}/print', function (Request $request, Token $token, $id) { return (new Actions\Tickets($this->app, $request, $token))->print($id); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->assert('id', $this->hashIdAssertion()) - ->bind('ticket_print'); + })); - $cp->get('/tickets/create', function (Request $request, Token $token) { + $this->routeRequirements('new_ticket', $cp->get('/tickets/create', function (Request $request, Token $token) { return (new Actions\Tickets($this->app, $request, $token))->new(); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->bind('new_ticket'); + })); - $cp->post('/tickets/create', function (Request $request, Token $token) { + $this->routeRequirements('create_ticket', $cp->post('/tickets/create', function (Request $request, Token $token) { return (new Actions\Tickets($this->app, $request, $token))->create(); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->bind('create_ticket'); + })); + + $this->routeIdRequirements('ticket_rate', $cp->post('/tickets/{id}/rate', function (Request $request, Token $token, $id) { + return (new Actions\Tickets($this->app, $request, $token))->rate($id); + })); } /** @@ -216,78 +177,45 @@ private function ticketsRoutes($cp): void */ private function repairsRoutes($cp): void { - $cp->get('/repairs', function (Request $request, Token $token) { + $this->routeRequirements('repairs', $cp->get('/repairs', function (Request $request, Token $token) { return (new Actions\Repairs($this->app, $request, $token))->index(); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->bind('repairs'); + })); - $cp->get('/repairs/create', function (Request $request, Token $token) { + $this->routeRequirements('new_repair', $cp->get('/repairs/create', function (Request $request, Token $token) { return (new Actions\Repairs($this->app, $request, $token))->new(); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->bind('new_repair'); + })); - $cp->post('/repairs/create', function (Request $request, Token $token) { + $this->routeRequirements('create_repair', $cp->post('/repairs/create', function (Request $request, Token $token) { return (new Actions\Repairs($this->app, $request, $token))->create(); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->bind('create_repair'); + })); - $cp->get('/repair/{id}', function (Request $request, Token $token, $id) { + $this->routeIdRequirements('repair', $cp->get('/repair/{id}', function (Request $request, Token $token, $id) { return (new Actions\Repairs($this->app, $request, $token))->show($id); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->assert('id', $this->hashIdAssertion()) - ->bind('repair'); + })); - $cp->get('/repair/{id}/print/{type}', function (Request $request, Token $token, $id, $type) { + $this->routeIdRequirements('repair_print', $cp->get('/repair/{id}/print/{type}', function (Request $request, Token $token, $id, $type) { return (new Actions\Repairs($this->app, $request, $token))->print($id, $type); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->assert('id', $this->hashIdAssertion()) - ->assert('type', '[a-z]{5,10}') - ->bind('repair_print'); - - $cp->post('/repair/{id}/accept', function (Request $request, Token $token, $id) { + }))->assert('type', '[a-z]{5,10}'); + + $this->routeIdRequirements('repair_rate', $cp->post('/repair/{id}/rate', function (Request $request, Token $token, $id) { + return (new Actions\Repairs($this->app, $request, $token))->rate($id); + })); + + $this->routeIdRequirements('repair_accept', $cp->post('/repair/{id}/accept', function (Request $request, Token $token, $id) { return (new Actions\RepairDecision($this->app, $request, $token))->accept($id); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->assert('id', $this->hashIdAssertion()) - ->before($this->expectJson()) - ->bind('repair_accept'); - - $cp->post('/repair/{id}/accept/{offer_id}', function (Request $request, Token $token, $id, $offer_id) { + }))->before($this->expectJson()); + + $this->routeIdRequirements('repair_accept_offer', $cp->post('/repair/{id}/accept/{offer_id}', function (Request $request, Token $token, $id, $offer_id) { return (new Actions\RepairDecision($this->app, $request, $token))->acceptOffer($id, $offer_id); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->assert('id', $this->hashIdAssertion()) - ->assert('offer_id', '\w+') - ->before($this->expectJson()) - ->bind('repair_accept_offer'); - - $cp->post('/repair/{id}/reject', function (Request $request, Token $token, $id) { + }))->assert('offer_id', '\w+')->before($this->expectJson()); + + $this->routeIdRequirements('repair_reject', $cp->post('/repair/{id}/reject', function (Request $request, Token $token, $id) { return (new Actions\RepairDecision($this->app, $request, $token))->reject($id); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->assert('id', $this->hashIdAssertion()) - ->before($this->expectJson()) - ->bind('repair_reject'); - - $cp->get('/repairs/submit_prompt', function (Request $request, Token $token) { + }))->before($this->expectJson()); + + $this->routeRequirements('repair_prompt', $cp->get('/repairs/submit_prompt', function (Request $request, Token $token) { return (new Actions\Repairs($this->app, $request, $token))->submitPrompt(); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->bind('repair_prompt'); + })); } /** @@ -296,24 +224,17 @@ private function repairsRoutes($cp): void */ private function temporaryFilesRoutes($cp): void { - $cp->post('/temporary_file', function (Request $request, Token $token) { + $this->routeRequirements('temporary_file_create', $cp->post('/temporary_file', function (Request $request, Token $token) { return (new Actions\TemporaryFile($this->app, $request, $token))->create(); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()); + })); - $cp->delete('/temporary_file', function (Request $request, Token $token) { + $this->routeRequirements('temporary_file_delete', $cp->delete('/temporary_file', function (Request $request, Token $token) { return 'OK'; - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()); + })); - $cp->get('/temporary_file', function (Request $request, Token $token) { + $this->routeRequirements('temporary_file', $cp->get('/temporary_file', function (Request $request, Token $token) { return (new Actions\TemporaryFile($this->app, $request, $token))->show(); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->bind('temporary_file'); + })); } /** @@ -322,131 +243,80 @@ private function temporaryFilesRoutes($cp): void */ private function userRoutes($cp): void { - $cp->get('/login', function (Request $request, Token $token) { + $this->routeNoAuthRequirements('new_session', $cp->get('/login', function (Request $request, Token $token) { return (new Actions\Login($this->app, $request, $token))->new(); - }) - ->before($this->expectAccessTokens()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->bind('new_session'); + })); - $cp->post('/login/resolve', function (Request $request, Token $token) { + $this->routeNoAuthRequirements('new_session_resolve_login', $cp->post('/login/resolve', function (Request $request, Token $token) { return (new Actions\Login($this->app, $request, $token))->resolveCredential(); - }) - ->before($this->expectAccessTokens()) - ->before($this->expectJson()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->bind('new_session_resolve_login'); + }))->before($this->expectJson()); - $cp->post('/login', function (Request $request, Token $token) { + $this->routeNoAuthRequirements('', $cp->post('/login', function (Request $request, Token $token) { return (new Actions\Login($this->app, $request, $token))->create(); - }) - ->before($this->expectAccessTokens()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()); + })); - $cp->get('/logout', function (Request $request, Token $token) { + $this->routeRequirements('destroy_session', $cp->get('/logout', function (Request $request, Token $token) { return (new Actions\Login($this->app, $request, $token))->destroy(); - }) - ->before($this->expectAccessTokens()) - ->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->bind('destroy_session'); + })); - $cp->get('/signup', function (Request $request, Token $token) { + $this->routeNoAuthRequirements('new_signup', $cp->get('/signup', function (Request $request, Token $token) { return (new Actions\Signup($this->app, $request, $token))->new(); - }) - ->before($this->expectAccessTokens()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->bind('new_signup'); + })); - $cp->post('/signup', function (Request $request, Token $token) { + $this->routeNoAuthRequirements('', $cp->post('/signup', function (Request $request, Token $token) { return (new Actions\Signup($this->app, $request, $token))->create(); - }) - ->before($this->expectAccessTokens()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()); + })); - $cp->get('/access_request/ask/{customer}', function (Request $request, Token $token, string $customer) { + $this->routeNoAuthRequirements('new_access_request', $cp->get('/access_request/ask/{customer}', function (Request $request, Token $token, string $customer) { return (new Actions\AccessRequest($this->app, $request, $token))->ask($customer); - }) - ->before($this->expectAccessTokens()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->bind('new_access_request'); + })); - $cp->post('/access_request/ask/{customer}', function (Request $request, Token $token, string $customer) { + $this->routeNoAuthRequirements('', $cp->post('/access_request/ask/{customer}', function (Request $request, Token $token, string $customer) { return (new Actions\AccessRequest($this->app, $request, $token))->sendLink($customer); - }) - ->before($this->expectAccessTokens()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()); + })); - $cp->get('/access_request/create', function (Request $request, Token $token) { + $this->routeNoAuthRequirements('', $cp->get('/access_request/create', function (Request $request, Token $token) { return (new Actions\AccessRequest($this->app, $request, $token))->new(); - }) - ->before($this->expectAccessTokens()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()); + })); - $cp->post('/access_request/create', function (Request $request, Token $token) { + $this->routeNoAuthRequirements('create_access_request', $cp->post('/access_request/create', function (Request $request, Token $token) { return (new Actions\AccessRequest($this->app, $request, $token))->create(); - }) - ->before($this->expectAccessTokens()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->bind('create_access_request'); + })); - $cp->get('/signup/{signup_token}', function (Request $request, Token $token, string $signup_token) { + $this->routeNoAuthRequirements('', $cp->get('/signup/{signup_token}', function (Request $request, Token $token, string $signup_token) { return (new Actions\Signup($this->app, $request, $token))->confirm($signup_token); - }) - ->before($this->expectAccessTokens()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()); + })); - $cp->get('/reset_password', function (Request $request, Token $token) { + $this->routeNoAuthRequirements('new_password_reset', $cp->get('/reset_password', function (Request $request, Token $token) { return (new Actions\PasswordReset($this->app, $request, $token))->new(); - }) - ->before($this->expectAccessTokens()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->bind('new_password_reset'); + })); - $cp->post('/reset_password', function (Request $request, Token $token) { + $this->routeNoAuthRequirements('', $cp->post('/reset_password', function (Request $request, Token $token) { return (new Actions\PasswordReset($this->app, $request, $token))->create(); - }) - ->before($this->expectAccessTokens()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()); + })); - $cp->get('/set_password', function (Request $request, Token $token) { + $this->routeNoAuthRequirements('', $cp->get('/set_password', function (Request $request, Token $token) { return (new Actions\PasswordReset($this->app, $request, $token))->newPassword(); - }) - ->before($this->expectAccessTokens()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()); + })); - $cp->post('/set_password', function (Request $request, Token $token) { + $this->routeNoAuthRequirements('set_password', $cp->post('/set_password', function (Request $request, Token $token) { return (new Actions\PasswordReset($this->app, $request, $token))->createPassword(); - }) - ->before($this->expectAccessTokens()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->bind('set_password'); + })); - $cp->get('/viewer', function (Request $request, Token $token) { + $this->routeRequirements('viewer', $cp->get('/viewer', function (Request $request, Token $token) { return (new Actions\Viewer($this->app, $request, $token))->edit(); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->bind('viewer'); + })); - $cp->post('/viewer', function (Request $request, Token $token) { + $this->routeRequirements('viewer_update', $cp->post('/viewer', function (Request $request, Token $token) { return (new Actions\Viewer($this->app, $request, $token))->update(); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->bind('viewer_update'); + })); - $cp->get('/viewer/set_password', function (Request $request, Token $token) { + $this->routeRequirements('viewer_password', $cp->get('/viewer/set_password', function (Request $request, Token $token) { return (new Actions\ViewerPassword($this->app, $request, $token))->edit(); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()) - ->bind('viewer_password'); + })); - $cp->post('/viewer/set_password', function (Request $request, Token $token) { + $this->routeRequirements('viewer_password_update', $cp->post('/viewer/set_password', function (Request $request, Token $token) { return (new Actions\ViewerPassword($this->app, $request, $token))->update(); - }) - ->before($this->expectAccessTokens())->before($this->expectAuthenticated()) - ->assert('token', $this->tokenAssertion())->convert('token', $this->tokenConverter()); + })); } } \ No newline at end of file diff --git a/src/Serwisant/SerwisantCp/queries/customer/repairs.graphql b/src/Serwisant/SerwisantCp/queries/customer/repairs.graphql index 6eef939..f17252c 100644 --- a/src/Serwisant/SerwisantCp/queries/customer/repairs.graphql +++ b/src/Serwisant/SerwisantCp/queries/customer/repairs.graphql @@ -176,6 +176,13 @@ query repairs($limit: Int, $page: Int, $filter: RepairsFilter, $sort: RepairsSor displayName __typename } + isRateable + rating { + comment + stars + date + __typename + } __typename } } diff --git a/src/Serwisant/SerwisantCp/queries/customer/schema.graphql b/src/Serwisant/SerwisantCp/queries/customer/schema.graphql index 4036852..d431b9f 100644 --- a/src/Serwisant/SerwisantCp/queries/customer/schema.graphql +++ b/src/Serwisant/SerwisantCp/queries/customer/schema.graphql @@ -104,6 +104,7 @@ type CustomerMutation { createTicket(devices: [HashID!], options: TicketCreationOptions, temporaryFiles: [HashID!], ticket: TicketInput!): TicketCreationResult markMessageRead(message: HashID!): Boolean print(ID: HashID!, type: PrintType!): TemporaryFileCreationResult + setRating(rating: RatingInput!, subject: HashID!, subjectType: RatingSubjectType!): RatingResult updateViewer(addresses: [AddressUpdateInput!], agreements: [CustomerAgreementUpdateInput!], customer: CustomerUpdateInput): ViewerUpdateResult updateViewerPassword(currentPassword: String!, password: String!, passwordConfirmation: String!): ViewerPasswordUpdateResult } @@ -261,6 +262,16 @@ type Priority { type: PriorityType! } +type Rating { + comment: String + date: Date! + stars: Int! +} + +type RatingResult { + errors: [Error!] +} + type Repair { ID: HashID! additionalItems: [RepairItem!]! @@ -281,12 +292,14 @@ type Repair { displayName: String! "Files attached to repair. For :service schema it includes private and public files, for otcher schemas only public files are included" files: [File!]! + isRateable: Boolean! issue: String model: String offers: [RepairOffer!] parcels: [Parcel!] priceEstimated: Decimal priceEstimatedTaxRate: Decimal + rating: Rating rma: String! secretToken: SecretToken! serial: String @@ -445,10 +458,12 @@ type Ticket { devices: [Device!]! employee: Employee files: [File!]! + isRateable: Boolean! issue: String! number: String! payment: TicketPayment! priority: Priority! + rating: Rating status: TicketStatus! type: Dictionary } @@ -908,6 +923,11 @@ enum PriorityType { TICKET } +enum RatingSubjectType { + REPAIR + TICKET +} + enum RepairOfferItemType { "Diagnosis part, inserted in automated way if present" DIAGNOSIS @@ -1175,6 +1195,11 @@ input PrioritiesFilter { type: PriorityType } +input RatingInput { + comment: String + stars: Int! +} + input RepairCreationOptions { skipFloodValidation: Boolean = false } diff --git a/src/Serwisant/SerwisantCp/queries/customer/setRating.graphql b/src/Serwisant/SerwisantCp/queries/customer/setRating.graphql new file mode 100644 index 0000000..ddaf221 --- /dev/null +++ b/src/Serwisant/SerwisantCp/queries/customer/setRating.graphql @@ -0,0 +1,11 @@ +mutation setRating($subject: HashID!, $subjectType: RatingSubjectType!, $rating: RatingInput!) { + setRating(subject: $subject, subjectType: $subjectType, rating: $rating) { + __typename + errors { + __typename + argument + code + message + } + } +} \ No newline at end of file diff --git a/src/Serwisant/SerwisantCp/queries/customer/tickets.graphql b/src/Serwisant/SerwisantCp/queries/customer/tickets.graphql index 11a3975..60e62ab 100644 --- a/src/Serwisant/SerwisantCp/queries/customer/tickets.graphql +++ b/src/Serwisant/SerwisantCp/queries/customer/tickets.graphql @@ -123,6 +123,13 @@ query tickets($limit: Int, $page: Int, $filter: TicketsFilter, $sort: TicketsSor street } } + isRateable + rating { + __typename + stars + comment + date + } } } } \ No newline at end of file diff --git a/src/Serwisant/SerwisantCp/queries/public/schema.graphql b/src/Serwisant/SerwisantCp/queries/public/schema.graphql index 0a7280e..5937463 100644 --- a/src/Serwisant/SerwisantCp/queries/public/schema.graphql +++ b/src/Serwisant/SerwisantCp/queries/public/schema.graphql @@ -217,6 +217,7 @@ type PublicMutation { resetPassword(loginOrEmail: String!, subject: PasswordResetSubject!): PasswordResetResult "Use a token sent by `resetPassword` to set a new password." setPassword(password: String!, passwordConfirmation: String!, resetToken: String!): PasswordSetResult + setRating(rating: RatingInput!, token: String!): RatingResult } type PublicQuery { @@ -246,6 +247,16 @@ type PublicQuery { viewer: Viewer! } +type Rating { + comment: String + date: Date! + stars: Int! +} + +type RatingResult { + errors: [Error!] +} + type Repair { advanceAmount: Decimal! "Address where service should pick up a repair item. Empty if `collectionType` type is `PERSONAL`" @@ -263,11 +274,13 @@ type Repair { displayName: String! "Files attached to repair. For :service schema it includes private and public files, for otcher schemas only public files are included" files: [File!]! + isRateable: Boolean! issue: String model: String offers: [RepairOffer!] priceEstimated: Decimal priceEstimatedTaxRate: Decimal + rating: Rating rma: String! secretToken: SecretToken! serial: String @@ -1081,3 +1094,8 @@ input PhoneInput { countryPrefix: String! number: String! } + +input RatingInput { + comment: String + stars: Int! +} diff --git a/src/Serwisant/SerwisantCp/translations/pl.yml b/src/Serwisant/SerwisantCp/translations/pl.yml index 3afb8e4..47d7acb 100644 --- a/src/Serwisant/SerwisantCp/translations/pl.yml +++ b/src/Serwisant/SerwisantCp/translations/pl.yml @@ -52,6 +52,9 @@ pl: customer_rate_limit: >- Nie możesz teraz dodać nowej naprawy. Przed dodaniem kolejnej odczekaj chwilę. + rating: + stars: + greater_than_or_equal_to: Wybierz od jednej do pięciu gwiazdek entitles: loginOrEmail: Login lub adres email resetToken: Link resetowania hasła @@ -75,6 +78,8 @@ pl: copyOfSaleDocument: Kopia dokumentu sprzedaży customFields: value: Pole tekstowe + rating: + stars: Ocena twig_extensions: progressed_about: 'Wykonano w %{percent}%' prev_page: Poprzednia @@ -177,6 +182,11 @@ pl: messages: Wiadomości devices: Urządzenia viewer_password: Zmień hasło + shared: + rating: + title: Oceń wykonaną usługę wskazując od jednej do pięciu gwiazdek + comment: Możesz zostawić dodatkowy komentarz dla serwisu + submit: Prześlij ocenę payment_types: INSTANT_FIXED: 'Płatność po zakończeniu, za zlecenie' INSTANT_TIME: 'Płatność po zakończeniu, stawka godzinowa' diff --git a/src/Serwisant/SerwisantCp/views/repair.html.twig b/src/Serwisant/SerwisantCp/views/repair.html.twig index 90f1a2a..2372747 100644 --- a/src/Serwisant/SerwisantCp/views/repair.html.twig +++ b/src/Serwisant/SerwisantCp/views/repair.html.twig @@ -12,10 +12,12 @@ {% endif %} -
+{{ form_errors(_self, rating_errors) }} +{% include 'shared/rating.html.twig' with {'isRateable': repair.isRateable, 'formUrl': path('repair_rate', {'id': repair.ID}), 'rating': repair.rating} %} - {% include 'repair/panel_change_status.html.twig' with {'parent': 'cp'} %} +{% include 'repair/panel_change_status.html.twig' with {'parent': 'cp'} %} +
{% include 'repair/panel_info.html.twig' %} diff --git a/src/Serwisant/SerwisantCp/views/shared/rating.html.twig b/src/Serwisant/SerwisantCp/views/shared/rating.html.twig new file mode 100644 index 0000000..f3234d1 --- /dev/null +++ b/src/Serwisant/SerwisantCp/views/shared/rating.html.twig @@ -0,0 +1,59 @@ +{% if isRateable %} +
+
{{ t(_self, 'title') }}
+
+
+
+
+
+
+
+ + + + + + + + + + + +
+
+
+ {{ form_field({type: 'textarea', argument: 'rating.comment', caption: t(_self, 'comment'), value: (rating ? rating.comment : '')}, form_params, rating_errors) }} +
+ +
+
+
+
+
+
+{% elseif rating %} +
+
{{ t(_self, 'title') }}
+
+
+
+
+ {{ rating.stars >= 5 ? "★" : "☆" }} + {{ rating.stars >= 4 ? "★" : "☆" }} + {{ rating.stars >= 3 ? "★" : "☆" }} + {{ rating.stars >= 2 ? "★" : "☆" }} + {{ rating.stars >= 1 ? "★" : "☆" }} +
+
+
+

{{ rating.comment }}

+
+ +
+
+
+
+
+{% endif %} diff --git a/src/Serwisant/SerwisantCp/views/ticket.html.twig b/src/Serwisant/SerwisantCp/views/ticket.html.twig index 9cf9acf..5c18935 100644 --- a/src/Serwisant/SerwisantCp/views/ticket.html.twig +++ b/src/Serwisant/SerwisantCp/views/ticket.html.twig @@ -5,6 +5,9 @@
+{{ form_errors(_self, rating_errors) }} +{% include 'shared/rating.html.twig' with {'isRateable': ticket.isRateable, 'formUrl': path('ticket_rate', {'id': ticket.ID}), 'rating': ticket.rating} %} +