Проект: CRM/ERP-система управления автомойками
Компонент: мониторинг статуса заказов для клиентов
Технологический стек проекта: Postgres, ElasticSearch; Rails, Grape; AngularJS (v1)
| angular.module('CarWash').controller('ClientMonitorController', function ( | |
| $scope, $http, $interval, $timeout, $stateParams, $sce, Reservation, | |
| ) { | |
| $scope.TIME_INTERVALS = { | |
| RESERVATIONS_UPDATE: 15 * 1000, | |
| } | |
| const notification = new Audio('monitor_notifications/default_notification.mp3'); | |
| const announcementPanel = $('#announcementPanel'); | |
| const clientMonitorUUID = $stateParams.monitorUUID; | |
| // Получаем данные настроек нужного монитора заказов | |
| $http({ | |
| url: '/client_monitor/settings', | |
| method: 'GET', | |
| params: { | |
| client_monitor_uuid: clientMonitorUUID, | |
| }, | |
| }).then((response) => { | |
| let playlistID = response.data['youtube_playlist'].split(/list=/)[1]; | |
| let bgImage = response.data['bg_image']; | |
| $scope.TIME_INTERVALS.RESERVATION_ANNOUNCEMENT = response.data['announcement_period'] * 1000; | |
| if (bgImage) { | |
| $('body.client_monitor').css({ | |
| 'background-image': `url('${bgImage}')` | |
| }); | |
| } | |
| if (playlistID) { | |
| $scope.youtubeUrl = $sce.trustAsResourceUrl(`https://www.youtube.com/embed/videoseries?list=${playlistID}&enablejsapi=1&autoplay=1&loop=1`); | |
| }; | |
| }); | |
| $scope.announcementInProgress = false; | |
| $scope.reservations = { | |
| toAnnounce: [], | |
| queue: [], | |
| } | |
| // Fullscreen announcement of finished reservations in | |
| // a popup | |
| const announceFullscreenReservation = (reservations) => { | |
| $scope.announcementInProgress = true; | |
| const r = reservations.pop(); | |
| notification.play().catch((error) => { | |
| console.log('Caught DOMExc., trying to reload'); | |
| }).then(() => { | |
| notification.play(); | |
| }); | |
| $timeout(() => { | |
| notification.play() | |
| }, $scope.TIME_INTERVALS.RESERVATION_ANNOUNCEMENT / 2); | |
| announcementPanel.removeClass('hidden'); | |
| $scope.announcedReservation = r; | |
| return $timeout($scope.TIME_INTERVALS.RESERVATION_ANNOUNCEMENT).then(() => { | |
| $scope.announcementInProgress = false; | |
| announcementPanel.addClass('hidden'); | |
| }); | |
| }; | |
| $scope.announceFullscreenReservations = () => { | |
| announceFullscreenReservation($scope.reservations.toAnnounce).then(() => { | |
| if ($scope.reservations.toAnnounce.length != 0) { | |
| $scope.announceFullscreenReservations(); | |
| } | |
| }) | |
| }; | |
| // Получаем список готовых заказов с заданной периодичностью | |
| // и строим очередь для выведения большого поп-апа на весь экран по одному | |
| $scope.updateReservations = () => { | |
| if ($scope.announcementInProgress) { | |
| return | |
| }; | |
| return $http({ | |
| url: '/client_monitor', | |
| method: 'GET', | |
| params: { | |
| client_monitor_uuid: clientMonitorUUID, | |
| }, | |
| }).then((response) => { | |
| let newQueue = response.data['reservations_to_announce']; | |
| $scope.reservations.toAnnounce = reservationQueuesDiff(newQueue, | |
| $scope.reservations.queue); | |
| $scope.reservations.queue = newQueue; | |
| console.log('Finished reservations were fetched'); | |
| if ($scope.reservations.toAnnounce.length > 0) { | |
| $scope.announceFullscreenReservations(); | |
| } | |
| }); | |
| }; | |
| $scope.updateReservations(); | |
| $interval($scope.updateReservations, $scope.TIME_INTERVALS.RESERVATIONS_UPDATE); | |
| const reservationQueuesDiff = (newQueue, oldQueue) => { | |
| return newQueue.filter((newRes) => { | |
| return !oldQueue.some((oldRes) => { | |
| return (newRes.id == oldRes.id) && (newRes.type == oldRes.type); | |
| }); | |
| }); | |
| }; | |
| $scope.manualInfo = (r) => { | |
| let parts = r.announcement_text.split('|'); | |
| return { | |
| tag: parts[0], | |
| make: parts[1], | |
| model: parts[2] | |
| } | |
| } | |
| }, ); |
| class ClientMonitorController < ApplicationController | |
| # Устанавливаем, какая точка сети автомоек должна быть контекстом | |
| before_action :set_service_location | |
| MANUAL_RESERVATION_ANNOUNCEMENT_PERIOD = 3.minutes | |
| def index | |
| car_attributes = { | |
| car: { | |
| include: { | |
| car_make: { only: :name }, | |
| car_model: { only: :name } | |
| }, | |
| only: :tag | |
| } | |
| } | |
| # Собираем все заказы, которые уже готовы (waiting_for_client) | |
| reservations_to_announce = Reservation | |
| .where(service_location_id: @service_location.id) | |
| .order(:time_end) | |
| .select { |r| r.business_process_status =~ /waiting_for_client/ } | |
| .as_json(include: car_attributes) | |
| .map { |item| item.merge(type: 'reservation') } | |
| # Отдельно собираем объевления заказов, добавляемые вручную сотрудниками | |
| manual_reservations_to_announce = ManualReservationAnnouncement.where( | |
| service_location_id: @service_location.id, | |
| organization_id: @service_location.organization_id | |
| ).where('created_at > ?', Time.now - MANUAL_RESERVATION_ANNOUNCEMENT_PERIOD) | |
| .order(:created_at) | |
| .as_json(methods: [:time_end]) | |
| .map { |item| item.merge(type: 'manual_reservation') } | |
| all_reservations = [*reservations_to_announce, *manual_reservations_to_announce].sort_by do |item| | |
| item['time_end'] | |
| end | |
| render json: { reservations_to_announce: all_reservations } | |
| end | |
| # Настройки внешнего экрана (в данном случае используется плейлист с YouTube для, иначе фолбэк на картинку; | |
| # так же время, которое заказ показывается на экране) | |
| def settings | |
| ytpl = @service_location.setting(:client_monitor_youtube_playlist_url).value | |
| announcement_period = @service_location.setting(:client_monitor_announcement_period).value | |
| render json: { | |
| youtube_playlist: ytpl, | |
| bg_image: @service_location.client_monitor_image.url, | |
| announcement_period: announcement_period | |
| } | |
| end | |
| private | |
| def set_service_location | |
| @service_location = ServiceLocation.find_by(client_monitor_uuid: params[:client_monitor_uuid]) | |
| end | |
| end |