From b3b9781b7a60402d607272877e5970f77523dcd7 Mon Sep 17 00:00:00 2001 From: Paul Maillardet Date: Fri, 9 Jun 2023 15:40:24 +0200 Subject: [PATCH] Notification des techniciens dans les inventaires de retour (#323) --- CHANGELOG.md | 3 + client/src/stores/api/events.ts | 9 ++- client/src/stores/api/reservations.ts | 4 +- .../Inventory/Item/Material/index.scss | 32 +++++++--- .../EventReturn/components/Footer/index.js | 31 +++++++-- .../EventReturn/components/Footer/index.scss | 4 -- .../themes/default/pages/EventReturn/index.js | 63 ++++++++++++++++++- .../pages/EventReturn/translations/en.yml | 12 ++++ .../pages/EventReturn/translations/fr.yml | 12 ++++ .../default/style/vendors/_tooltip.scss | 2 + server/src/App/Config/Acl.php | 1 + server/src/App/Config/routes.php | 9 +-- .../src/App/Controllers/EventController.php | 53 +++++++++++++++- .../not-returned/event-technician.twig | 34 ++++++++++ .../not-returned/event-technician.twig | 34 ++++++++++ server/tests/endpoints/EventsTest.php | 60 ++++++++++++++++-- server/tests/endpoints/ReservationsTest.php | 12 ++-- 17 files changed, 338 insertions(+), 37 deletions(-) create mode 100644 server/src/views/emails/en/notifications/not-returned/event-technician.twig create mode 100644 server/src/views/emails/fr/notifications/not-returned/event-technician.twig diff --git a/CHANGELOG.md b/CHANGELOG.md index 574df01cf..b8d6a005f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ Ce projet adhère au principe du [Semantic Versioning](https://semver.org/spec/v - Ajoute la possibilité de choisir un emplacement de rangement pour chaque matériel au sein d'un parc, et affiche cette information dans les fiches de sorties et les inventaires de retour (Premium #294). +- Dans les inventaires de retour des événements, un bouton permet d'envoyer une notification + par e-mail aux techniciens assignés à l'événement, tant que le matériel n'a pas été + complètement retourné, ou que l'inventaire n'est pas terminé (Premium #293). - Dans le calendrier, un nouveau filtre permet de filtrer les événements par catégorie du matériel qu'il contient (Premium #297). - Affiche la durée des événements et réservations dans l'onglet "périodes de réservation" diff --git a/client/src/stores/api/events.ts b/client/src/stores/api/events.ts index 6daa2aa69..e318d7e28 100644 --- a/client/src/stores/api/events.ts +++ b/client/src/stores/api/events.ts @@ -212,11 +212,11 @@ const unarchive = async (id: Event['id']): Promise => ( ); const updateReturnInventory = async (id: Event['id'], inventory: EventReturnInventory): Promise => ( - normalize((await requester.put(`/events/${id}/inventory`, inventory)).data) + normalize((await requester.put(`/events/${id}/return`, inventory)).data) ); const finishReturnInventory = async (id: Event['id'], inventory: EventReturnInventory): Promise => ( - normalize((await requester.put(`/events/${id}/inventory/finish`, inventory)).data) + normalize((await requester.put(`/events/${id}/return/finish`, inventory)).data) ); const createInvoice = async (id: Event['id'], discountRate: number = 0): Promise => ( @@ -252,6 +252,10 @@ const attachDocument = async (id: Event['id'], file: File, options: RequestConfi return (await requester.post(`/events/${id}/documents`, formData, options)).data; }; +const notifyReturn = async (id: Event['id']): Promise<{ sent: number }> => ( + (await requester.post(`/events/${id}/return/notify`)).data +); + export default { all, one, @@ -269,4 +273,5 @@ export default { remove, documents, attachDocument, + notifyReturn, }; diff --git a/client/src/stores/api/reservations.ts b/client/src/stores/api/reservations.ts index 757c39928..8d35d1062 100644 --- a/client/src/stores/api/reservations.ts +++ b/client/src/stores/api/reservations.ts @@ -184,11 +184,11 @@ const missingMaterials = async (id: Reservation['id']): Promise => ( - (await requester.put(`/reservations/${id}/inventory`, inventory)).data + (await requester.put(`/reservations/${id}/return`, inventory)).data ); const finishReturnInventory = async (id: Reservation['id'], inventory: ReservationReturnInventory): Promise => ( - (await requester.put(`/reservations/${id}/inventory/finish`, inventory)).data + (await requester.put(`/reservations/${id}/return/finish`, inventory)).data ); const archive = async (id: Reservation['id']): Promise => ( diff --git a/client/src/themes/default/components/Inventory/Item/Material/index.scss b/client/src/themes/default/components/Inventory/Item/Material/index.scss index 6fabc40fd..ceb51285f 100644 --- a/client/src/themes/default/components/Inventory/Item/Material/index.scss +++ b/client/src/themes/default/components/Inventory/Item/Material/index.scss @@ -20,16 +20,18 @@ } &__reference { - flex: 0 0 200px; + flex: 1; } &__name { flex: 1; + display: none; color: globals.$text-soft-color; } &__park-location { flex: 1; + display: none; color: globals.$text-soft-color; font-size: 0.85rem; } @@ -68,12 +70,6 @@ } } - &--with-park-location { - #{$block}__name { - flex: 0 0 330px; - } - } - &--complete { background: rgba(globals.$text-success-color, 0.25); @@ -105,4 +101,26 @@ font-weight: 600; } } + + @media (min-width: globals.$screen-tablet) { + &__reference { + flex: 0 0 200px; + } + + &__name { + display: block; + } + } + + @media (min-width: globals.$screen-desktop) { + &__park-location { + display: block; + } + + &--with-park-location { + #{$block}__name { + flex: 0 0 330px; + } + } + } } diff --git a/client/src/themes/default/pages/EventReturn/components/Footer/index.js b/client/src/themes/default/pages/EventReturn/components/Footer/index.js index 6d8f6dfce..25318d8e2 100644 --- a/client/src/themes/default/pages/EventReturn/components/Footer/index.js +++ b/client/src/themes/default/pages/EventReturn/components/Footer/index.js @@ -1,17 +1,20 @@ import './index.scss'; +import { defineComponent } from '@vue/composition-api'; import Fragment from '@/components/Fragment'; import IconMessage from '@/themes/default/components/IconMessage'; import Button from '@/themes/default/components/Button'; // @vue/component -export default { - name: 'EventReturnFooter', +const EventReturnFooter = defineComponent({ + name: '', props: { isDone: Boolean, isSaving: Boolean, hasEnded: Boolean, + canNotify: Boolean, + isNotifying: Boolean, }, - emits: ['save', 'terminate'], + emits: ['save', 'terminate', 'notify'], methods: { // ------------------------------------------------------ // - @@ -26,6 +29,10 @@ export default { handleClickTerminate() { this.$emit('terminate'); }, + + handleClickNotify() { + this.$emit('notify'); + }, }, render() { const { @@ -33,8 +40,11 @@ export default { isDone, isSaving, hasEnded, + canNotify, + isNotifying, handleClickSave, handleClickTerminate, + handleClickNotify, } = this; return ( @@ -68,6 +78,17 @@ export default { {isSaving ? __('saving') : __('terminate-inventory')} )} + {canNotify && ( + + )} {!hasEnded && (
); }, -}; +}); + +export default EventReturnFooter; diff --git a/client/src/themes/default/pages/EventReturn/components/Footer/index.scss b/client/src/themes/default/pages/EventReturn/components/Footer/index.scss index e3a76a32a..7da13e458 100644 --- a/client/src/themes/default/pages/EventReturn/components/Footer/index.scss +++ b/client/src/themes/default/pages/EventReturn/components/Footer/index.scss @@ -11,10 +11,6 @@ color: globals.$text-success-color; } - &__action { - margin-left: globals.$content-padding-small-vertical; - } - &__warning { margin-top: globals.$content-padding-large-horizontal; color: globals.$text-warning-color; diff --git a/client/src/themes/default/pages/EventReturn/index.js b/client/src/themes/default/pages/EventReturn/index.js index 52734790a..08cdde1a5 100644 --- a/client/src/themes/default/pages/EventReturn/index.js +++ b/client/src/themes/default/pages/EventReturn/index.js @@ -27,6 +27,8 @@ const EventReturn = defineComponent({ isLoading: false, isFetched: false, isSaving: false, + isSaved: false, + isNotifying: false, displayGroup: DisplayGroup.CATEGORIES, criticalError: null, validationErrors: null, @@ -68,6 +70,25 @@ const EventReturn = defineComponent({ } return !!this.event.is_return_inventory_done; }, + + canNotify() { + const { event, inventory, isSaved } = this; + if (!event) { + return false; + } + + const isAllReturned = event.materials.every(({ id: materialId, pivot }) => { + const quantities = inventory.find(({ id }) => id === materialId); + return quantities ? quantities.actual === pivot.quantity : false; + }); + + return ( + event?.is_return_inventory_started && + event.technicians.length > 0 && + !isAllReturned && + isSaved + ); + }, }, mounted() { this.fetchData(); @@ -93,6 +114,7 @@ const EventReturn = defineComponent({ return; } this.$set(this.inventory, index, { id, ...quantities }); + this.isSaved = false; }, handleChangeDisplayGroup(group) { @@ -122,6 +144,35 @@ const EventReturn = defineComponent({ await this.save(true); }, + async handleNotify() { + const { $t: __, event, isNotifying } = this; + if (isNotifying) { + return; + } + + const count = event.technicians.length; + const isConfirmed = await confirm({ + type: 'warning', + text: __('page.event-return.please-confirm-send-notification', { count }, count), + confirmButtonText: __('page.event-return.yes-send'), + }); + + if (!isConfirmed) { + return; + } + + this.isNotifying = true; + + try { + const { sent } = await apiEvents.notifyReturn(this.id); + this.$toasted.success(__('page.event-return.technicians-notified', { sent }, sent)); + } catch { + this.$toasted.error(__('errors.unknown')); + } finally { + this.isNotifying = false; + } + }, + // ------------------------------------------------------ // - // - Méthodes internes @@ -133,6 +184,7 @@ const EventReturn = defineComponent({ const event = await apiEvents.one(this.id); this.setEvent(event); this.isFetched = true; + this.isSaved = event.is_return_inventory_started; } catch (error) { if (!axios.isAxiosError(error)) { this.criticalError = ERROR.UNKNOWN; @@ -159,8 +211,9 @@ const EventReturn = defineComponent({ ); try { - const _event = await doRequest(); - this.setEvent(_event); + const event = await doRequest(); + this.setEvent(event); + this.isSaved = true; this.validationErrors = null; this.$toasted.success(__('page.event-return.saved')); @@ -222,11 +275,14 @@ const EventReturn = defineComponent({ isSaving, hasEnded, isDone, + canNotify, + isNotifying, displayGroup, handleChangeDisplayGroup, handleChangeInventory, handleSave, handleTerminate, + handleNotify, } = this; if (criticalError || !isFetched) { @@ -267,6 +323,9 @@ const EventReturn = defineComponent({ hasEnded={hasEnded} onSave={handleSave} onTerminate={handleTerminate} + canNotify={canNotify} + onNotify={handleNotify} + isNotifying={isNotifying} /> )} diff --git a/client/src/themes/default/pages/EventReturn/translations/en.yml b/client/src/themes/default/pages/EventReturn/translations/en.yml index 34fd80307..73f0ca1ba 100644 --- a/client/src/themes/default/pages/EventReturn/translations/en.yml +++ b/client/src/themes/default/pages/EventReturn/translations/en.yml @@ -16,6 +16,18 @@ all-material-returned: some-material-is-missing: Some materials did not return from this event! some-material-came-back-broken: Some materials came back broken. +notify-technicians: Notify technicians +notify-technicians-tooltip: |- + Send an e-mail to technicians assigned to + the event with a list of missing materials. +please-confirm-send-notification: + - Do you really want to send an e-mail notification to the technician? + - Do you really want to send an e-mail notification to {count} technician(s)? +yes-send: Yes, send +technicians-notified: + - "The technician has been notified by e-mail." + - "{sent} technicians have been notified by e-mail." + confirm-terminate-title: Do you really want to terminate this return inventory? confirm-terminate-text: Please note that it will no longer be possible to modify it. confirm-terminate-text-with-broken: >- diff --git a/client/src/themes/default/pages/EventReturn/translations/fr.yml b/client/src/themes/default/pages/EventReturn/translations/fr.yml index eff224698..d0617d201 100644 --- a/client/src/themes/default/pages/EventReturn/translations/fr.yml +++ b/client/src/themes/default/pages/EventReturn/translations/fr.yml @@ -16,6 +16,18 @@ all-material-returned: some-material-is-missing: "Du matériel n'est pas revenu de cet événement\_!" some-material-came-back-broken: Du matériel est revenu en panne. +notify-technicians: Notifier les techniciens +notify-technicians-tooltip: |- + Envoyer un e-mail aux techniciens assignés à + l'événement avec la liste du matériel manquant. +please-confirm-send-notification: + - "Voulez-vous vraiment envoyer une notification par e-mail au technicien\_?" + - "Voulez-vous vraiment envoyer une notification par e-mail aux {count} techniciens\_?" +yes-send: Oui, envoyer +technicians-notified: + - "Le technicien a bien été notifié par e-mail." + - "{sent} techniciens ont bien été notifiés par e-mail." + confirm-terminate-title: "Voulez-vous vraiment terminer cet inventaire de retour\_?" confirm-terminate-text: Veuillez noter qu'il ne sera plus possible de le modifier. confirm-terminate-text-with-broken: diff --git a/client/src/themes/default/style/vendors/_tooltip.scss b/client/src/themes/default/style/vendors/_tooltip.scss index 8d44acee7..d6eaedb3a 100644 --- a/client/src/themes/default/style/vendors/_tooltip.scss +++ b/client/src/themes/default/style/vendors/_tooltip.scss @@ -6,5 +6,7 @@ border-radius: 3px; background: $bg-color-tooltip; color: $text-soft-color; + text-align: center; + white-space: pre-line; } } diff --git a/server/src/App/Config/Acl.php b/server/src/App/Config/Acl.php index 7d820fbad..6d0da661f 100644 --- a/server/src/App/Config/Acl.php +++ b/server/src/App/Config/Acl.php @@ -134,6 +134,7 @@ class Acl 'restore', 'updateReturnInventory', 'finishReturnInventory', + 'notifyAboutReturnInventory', 'delete', 'createInvoice', 'createEstimate', diff --git a/server/src/App/Config/routes.php b/server/src/App/Config/routes.php index 2f7fc4bcc..351917a93 100644 --- a/server/src/App/Config/routes.php +++ b/server/src/App/Config/routes.php @@ -117,6 +117,7 @@ '/events/{id:[0-9]+}/invoices[/]' => 'EventController:createInvoice', '/events/{id:[0-9]+}/estimates[/]' => 'EventController:createEstimate', '/events/{id:[0-9]+}/documents[/]' => 'EventController:attachDocument', + '/events/{id:[0-9]+}/return/notify[/]' => 'EventController:notifyAboutReturnInventory', '/reservations/{id:[0-9]+}/invoices[/]' => 'ReservationController:createInvoice', '/reservations/{id:[0-9]+}/documents[/]' => 'ReservationController:attachDocument', @@ -167,8 +168,8 @@ '/events/{id:[0-9]+}[/]' => 'EventController:update', '/events/restore/{id:[0-9]+}[/]' => 'EventController:restore', - '/events/{id:[0-9]+}/inventory[/]' => 'EventController:updateReturnInventory', - '/events/{id:[0-9]+}/inventory/finish[/]' => 'EventController:finishReturnInventory', + '/events/{id:[0-9]+}/return[/]' => 'EventController:updateReturnInventory', + '/events/{id:[0-9]+}/return/finish[/]' => 'EventController:finishReturnInventory', '/events/{id:[0-9]+}/archive[/]' => 'EventController:archive', '/events/{id:[0-9]+}/unarchive[/]' => 'EventController:unarchive', @@ -192,8 +193,8 @@ '/reservations/{id:[0-9]+}/archive[/]' => 'ReservationController:archive', '/reservations/{id:[0-9]+}/unarchive[/]' => 'ReservationController:unarchive', '/reservations/{id:[0-9]+}/note[/]' => 'ReservationController:updateNote', - '/reservations/{id:[0-9]+}/inventory[/]' => 'ReservationController:updateReturnInventory', - '/reservations/{id:[0-9]+}/inventory/finish[/]' => 'ReservationController:finishReturnInventory', + '/reservations/{id:[0-9]+}/return[/]' => 'ReservationController:updateReturnInventory', + '/reservations/{id:[0-9]+}/return/finish[/]' => 'ReservationController:finishReturnInventory', '/bookings/{id:[0-9]+}/materials[/]' => 'BookingController:updateMaterials', ], diff --git a/server/src/App/Controllers/EventController.php b/server/src/App/Controllers/EventController.php index fceec5b72..b1bf0a436 100644 --- a/server/src/App/Controllers/EventController.php +++ b/server/src/App/Controllers/EventController.php @@ -6,6 +6,7 @@ use Brick\Math\BigDecimal as Decimal; use DI\Container; use Fig\Http\Message\StatusCodeInterface as StatusCode; +use Robert2\API\Config\Config; use Robert2\API\Controllers\Traits\WithCrud; use Robert2\API\Controllers\Traits\WithPdf; use Robert2\API\Errors\Exception\ValidationException; @@ -18,6 +19,8 @@ use Robert2\API\Models\MaterialUnit; use Robert2\API\Services\Auth; use Robert2\API\Services\I18n; +use Robert2\API\Services\Mailer; +use Robert2\API\Services\View; use Slim\Exception\HttpBadRequestException; use Slim\Exception\HttpNotFoundException; use Slim\Http\Response; @@ -28,12 +31,14 @@ class EventController extends BaseController use WithPdf; private I18n $i18n; + protected Mailer $mailer; - public function __construct(Container $container, I18n $i18n) + public function __construct(Container $container, I18n $i18n, Mailer $mailer) { parent::__construct($container); $this->i18n = $i18n; + $this->mailer = $mailer; } // ------------------------------------------------------ @@ -157,6 +162,52 @@ public function finishReturnInventory(Request $request, Response $response): Res return $response->withJson(static::_formatOne($event), StatusCode::STATUS_OK); } + public function notifyAboutReturnInventory(Request $request, Response $response): Response + { + $id = (int) $request->getAttribute('id'); + $event = Event::findOrFail($id); + + if ($event->is_return_inventory_done) { + throw new HttpBadRequestException($request, "This event's return inventory is already done."); + } + + if ($event->technicians->isEmpty()) { + throw new HttpBadRequestException($request, "This event has no technician assigned."); + } + + $eventFiltered = tap(clone $event, fn(Event $_event) => $_event->setRelation( + 'materials', + $_event->materials->filter(fn (Material $material) => ( + $material->pivot->quantity - $material->pivot->quantity_returned > 0 + )) + )); + + if ($eventFiltered->materials->isEmpty()) { + throw new HttpBadRequestException($request, "All materials of this event are returned."); + } + + $data = [ + 'company' => Config::getSettings('companyData'), + 'event' => $eventFiltered, + ]; + + $sentCount = 0; + foreach ($event->technicians as $technician) { + $email = $technician->technician->email; + if (empty($email)) { + continue; + } + + $subject = $this->i18n->translate('not-returned-materials-notification'); + $message = (new View($this->i18n, 'emails')) + ->fetch('notifications/not-returned/event-technician', $data); + $this->mailer->send($email, $subject, $message); + $sentCount += 1; + } + + return $response->withJson(['sent' => $sentCount], StatusCode::STATUS_CREATED); + } + public function createEstimate(Request $request, Response $response): Response { $id = (int) $request->getAttribute('id'); diff --git a/server/src/views/emails/en/notifications/not-returned/event-technician.twig b/server/src/views/emails/en/notifications/not-returned/event-technician.twig new file mode 100644 index 000000000..e88517e69 --- /dev/null +++ b/server/src/views/emails/en/notifications/not-returned/event-technician.twig @@ -0,0 +1,34 @@ +{% extends "emails/base.twig" %} + +{% set date = event.start_date|format_date('short', locale=locale) %} +{% set dueDate = event.end_date|format_date('short', locale=locale) %} + +{% block body %} +

+ Hello,
+
+ The equipment of the event "{{ event.title }}" which ended + on {{ dueDate }} has not yet been fully returned. +

+

+ Here is the list of missing equipment: +

+
    + {% for material in event.materials %} + {% set quantityNotReturned = material.pivot.quantity - material.pivot.quantity_returned %} +
  • + {{ quantityNotReturned }} × {{ material.name }} + | ref.: "{{ material.reference }}" + | returned: {{ (material.pivot.quantity_returned ?: 0)|format_number(locale=locale) }} / {{ material.pivot.quantity }}. +
  • + {% endfor %} +
+

+ Sincerely yours, +

+

+ {{ company['name'] }}
+ {{ company['email'] }}
+ {{ company['phone'] }} +

+{% endblock %} diff --git a/server/src/views/emails/fr/notifications/not-returned/event-technician.twig b/server/src/views/emails/fr/notifications/not-returned/event-technician.twig new file mode 100644 index 000000000..380c06efb --- /dev/null +++ b/server/src/views/emails/fr/notifications/not-returned/event-technician.twig @@ -0,0 +1,34 @@ +{% extends "emails/base.twig" %} + +{% set date = event.start_date|format_date('short', locale=locale) %} +{% set dueDate = event.end_date|format_date('short', locale=locale) %} + +{% block body %} +

+ Bonjour,
+
+ Le matériel de l'événement « {{ event.title }} » qui s'est + terminé le {{ dueDate }} n'a pas encore été entièrement restitué. +

+

+ Voici la liste du matériel manquant : +

+
    + {% for material in event.materials %} + {% set quantityNotReturned = material.pivot.quantity - material.pivot.quantity_returned %} +
  • + {{ quantityNotReturned }} × {{ material.name }} + | réf. : « {{ material.reference }} » + | retourné : {{ (material.pivot.quantity_returned ?: 0)|format_number(locale=locale) }} / {{ material.pivot.quantity }}. +
  • + {% endfor %} +
+

+ Bien cordialement, +

+

+ {{ company['name'] }}
+ {{ company['email'] }}
+ {{ company['phone'] }} +

+{% endblock %} diff --git a/server/tests/endpoints/EventsTest.php b/server/tests/endpoints/EventsTest.php index f8e39ed37..e733fc955 100644 --- a/server/tests/endpoints/EventsTest.php +++ b/server/tests/endpoints/EventsTest.php @@ -934,7 +934,7 @@ public function testDuplicateEvent() public function testUpdateReturnInventoryNotFound() { - $this->client->put('/api/events/999/inventory'); + $this->client->put('/api/events/999/return'); $this->assertStatusCode(StatusCode::STATUS_NOT_FOUND); } @@ -944,7 +944,7 @@ public function testUpdateReturnInventory() ['id' => 1, 'is_unitary' => false, 'actual' => 2, 'broken' => 0], ['id' => 2, 'is_unitary' => false, 'actual' => 2, 'broken' => 1], ]; - $this->client->put('/api/events/2/inventory', $data); + $this->client->put('/api/events/2/return', $data); $this->assertStatusCode(StatusCode::STATUS_OK); $response = $this->_getResponseAsArray(); @@ -984,7 +984,7 @@ public function testUpdateReturnInventoryBadData() ['id' => 1, 'is_unitary' => false, 'actual' => 2, 'broken' => 3], ['id' => 2, 'is_unitary' => false, 'actual' => 3, 'broken' => 0], ]; - $this->client->put('/api/events/2/inventory', $data); + $this->client->put('/api/events/2/return', $data); $this->assertApiValidationError([ ['id' => 1, 'message' => "Broken quantity cannot be greater than returned quantity."], ['id' => 2, 'message' => "Returned quantity cannot be greater than output quantity."], @@ -996,7 +996,7 @@ public function testFinishReturnInventory() Carbon::setTestNow(Carbon::create(2023, 2, 1, 10, 00, 00)); // - Test avec du matériel non-unitaire - $this->client->put('/api/events/2/inventory/finish', [ + $this->client->put('/api/events/2/return/finish', [ ['id' => 1, 'is_unitary' => false, 'actual' => 3, 'broken' => 0], ['id' => 2, 'is_unitary' => false, 'actual' => 2, 'broken' => 1], ]); @@ -1025,7 +1025,7 @@ public function testFinishReturnInventory() )); // - Test avec du matériel unitaire - $this->client->put('/api/events/4/inventory/finish', [ + $this->client->put('/api/events/4/return/finish', [ [ 'id' => 1, 'is_unitary' => false, @@ -1462,4 +1462,54 @@ public function testGetDocuments() $this->assertStatusCode(StatusCode::STATUS_OK); $this->assertResponseData([]); } + + public function testNotifyReturnInventory() + { + // - Test avec un événement dont l'inventaire de retour a déjà été fait. + $this->client->post('/api/events/1/return/notify'); + $this->assertStatusCode(StatusCode::STATUS_BAD_REQUEST); + $this->assertApiErrorMessage("This event's return inventory is already done."); + + // - Test avec un événement qui n'a pas de technicien assigné. + $event = Event::findOrFail(2); + $event->is_return_inventory_done = false; + $event->save(); + $this->client->post('/api/events/2/return/notify'); + $this->assertStatusCode(StatusCode::STATUS_BAD_REQUEST); + $this->assertApiErrorMessage("This event has no technician assigned."); + + // - Test en assignant un technicien à un événement qui n'a pas de matériel. + $event = Event::findOrFail(6); + $event->syncTechnicians([ + [ + 'id' => 1, + 'start_time' => '2019-03-15 08:00:00', + 'end_time' => '2019-03-15 18:00:00', + 'position' => null, + ], + ]); + $this->client->post('/api/events/6/return/notify'); + $this->assertStatusCode(StatusCode::STATUS_BAD_REQUEST); + $this->assertApiErrorMessage("All materials of this event are returned."); + + // - Test en assignant des techniciens à un événement + // qui n'a pas encore d'inventaire de retour. + $event = Event::findOrFail(4); + $event->syncTechnicians([ + [ + 'id' => 1, + 'start_time' => '2019-03-01 05:00:00', + 'end_time' => '2019-03-01 06:00:00', + 'position' => 'Décollage', + ], + [ + 'id' => 2, + 'start_time' => '2019-03-01 05:00:00', + 'end_time' => '2019-03-01 06:00:00', + 'position' => 'Décollage', + ], + ]); + $this->client->post('/api/events/4/return/notify'); + $this->assertResponseData(['sent' => 2]); + } } diff --git a/server/tests/endpoints/ReservationsTest.php b/server/tests/endpoints/ReservationsTest.php index 96425ae35..362744edc 100644 --- a/server/tests/endpoints/ReservationsTest.php +++ b/server/tests/endpoints/ReservationsTest.php @@ -384,13 +384,13 @@ public function testDelete(): void public function testUpdateReturnInventoryNotFound() { - $this->client->put('/api/reservations/999/inventory'); + $this->client->put('/api/reservations/999/return'); $this->assertStatusCode(StatusCode::STATUS_NOT_FOUND); } public function testUpdateReturnInventoryNotApproved() { - $this->client->put('/api/reservations/2/inventory'); + $this->client->put('/api/reservations/2/return'); $this->assertStatusCode(StatusCode::STATUS_NOT_FOUND); } @@ -424,7 +424,7 @@ public function testUpdateReturnInventory() 'units' => [], ], ]; - $this->client->put('/api/reservations/4/inventory', $data); + $this->client->put('/api/reservations/4/return', $data); $this->assertStatusCode(StatusCode::STATUS_OK); $expected = array_merge(self::data(4), [ @@ -461,7 +461,7 @@ public function testUpdateReturnInventoryBadData() ['id' => 4, 'actual' => 2, 'broken' => 0, 'units' => []], ['id' => 7, 'actual' => 1, 'broken' => 0, 'units' => []], ]; - $this->client->put('/api/reservations/4/inventory', $data); + $this->client->put('/api/reservations/4/return', $data); $this->assertApiValidationError([ ['id' => 4, 'message' => "Returned quantity cannot be greater than output quantity."], ['id' => 7, 'message' => "Please specify the return status of each unit."], @@ -502,7 +502,7 @@ public function testFinishReturnInventory() ], ]; - $this->client->put('/api/reservations/4/inventory/finish', $data); + $this->client->put('/api/reservations/4/return/finish', $data); $this->assertStatusCode(StatusCode::STATUS_OK); $expected = array_merge(self::data(4), [ @@ -709,7 +709,7 @@ public function testGetDocuments() $this->assertStatusCode(StatusCode::STATUS_NOT_FOUND); // - Réservation non validée. - $this->client->put('/api/reservations/2/inventory'); + $this->client->put('/api/reservations/2/return'); $this->assertStatusCode(StatusCode::STATUS_NOT_FOUND); // - Documents de la réservation #1.