spree_cm_commissioner 2.12.9 → 2.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/app/controllers/spree/admin/orders/customer_details_controller_decorator.rb +6 -4
- data/app/controllers/spree/admin/system/waiting_room_sessions_controller.rb +111 -0
- data/app/controllers/spree/api/v2/platform/resource_controller_decorator.rb +48 -0
- data/app/helpers/spree/admin/system/waiting_room_sessions_helper.rb +50 -0
- data/app/models/spree_cm_commissioner/line_item_decorator.rb +1 -1
- data/app/models/spree_cm_commissioner/pricing_actions/create_route_adjustments.rb +2 -2
- data/app/models/spree_cm_commissioner/voting_contestant.rb +1 -1
- data/app/overrides/spree/admin/orders/_order_actions/actions.html.erb.deface +4 -4
- data/app/overrides/spree/admin/orders/_search/taxon_search_field.html.erb.deface +8 -0
- data/app/overrides/spree/admin/orders/customer_details/edit/reassign_customer_search.html.erb.deface +4 -5
- data/app/overrides/spree/admin/taxons/_form/preview_checkbox.html.erb.deface +4 -0
- data/app/overrides/spree/admin/taxons/_form/vendor.html.erb.deface +10 -2
- data/app/overrides/spree/admin/users/_tabs/tabs.html.erb.deface +1 -1
- data/app/services/spree_cm_commissioner/waiting_room/admission/call_guests.rb +6 -3
- data/app/services/spree_cm_commissioner/waiting_room/guests/find.rb +37 -0
- data/app/services/spree_cm_commissioner/waiting_room/guests/list.rb +59 -0
- data/app/services/spree_cm_commissioner/waiting_room_lobby_metadata_fetcher.rb +13 -11
- data/app/views/spree/admin/shared/_taxon_tabs.html.erb +1 -1
- data/app/views/spree/admin/system/waiting_room_sessions/_guest_raw.html.erb +70 -0
- data/app/views/spree/admin/system/waiting_room_sessions/_guests_pagination.html.erb +36 -0
- data/app/views/spree/admin/system/waiting_room_sessions/_guests_table.html.erb +86 -0
- data/app/views/spree/admin/system/waiting_room_sessions/_tabs.html.erb +10 -0
- data/app/views/spree/admin/system/waiting_room_sessions/guests.html.erb +141 -0
- data/app/views/spree/admin/system/waiting_room_sessions/index.html.erb +1 -2
- data/app/views/spree/admin/user_events/_new_user.html.erb +2 -1
- data/app/views/spree/admin/user_events/_users.html.erb +1 -1
- data/config/locales/en.yml +5 -2
- data/config/locales/km.yml +5 -2
- data/config/routes.rb +3 -0
- data/lib/spree_cm_commissioner/version.rb +1 -1
- metadata +11 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 709925b9130644b3e39ef1bb01415692019bc72643176593877c42f1a710ff0f
|
|
4
|
+
data.tar.gz: a336bae1ac6555051a4d6c718d8cce7bb1fcbed67b8bea0a2accb5b96cb8ae1b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '0769de948ff5436227d88a348a051a7992956deca52f0a165c4cfa885750b9d672c9192482070d922b52921ded1b080a08c004eb6d33206ae67638ee4eec3a61'
|
|
7
|
+
data.tar.gz: '09affaee452bf8a04579f43999a2beb71282b0d369109085fa59b63a8851d0c6fb80ad67b9ffc3930542ab6fe9e6b9c500c17b243380b5d33440225e30d8cc02'
|
data/Gemfile.lock
CHANGED
|
@@ -15,11 +15,13 @@ module Spree
|
|
|
15
15
|
def assign_customer
|
|
16
16
|
user = tenant_user_scope.find_by(id: params[:user_id])
|
|
17
17
|
|
|
18
|
-
if user
|
|
19
|
-
@order.associate_user!(user)
|
|
20
|
-
flash[:success] = Spree.t('admin.orders.customer_details.customer_assigned', email: user.email)
|
|
21
|
-
else
|
|
18
|
+
if user.nil?
|
|
22
19
|
flash[:error] = Spree.t(:user_not_found)
|
|
20
|
+
elsif user.id == @order.user_id
|
|
21
|
+
flash[:notice] = Spree.t('admin.orders.customer_details.user_already_assigned', email: user.email)
|
|
22
|
+
else
|
|
23
|
+
@order.associate_user!(user)
|
|
24
|
+
flash[:success] = Spree.t('admin.orders.customer_details.user_assigned', email: user.email)
|
|
23
25
|
end
|
|
24
26
|
|
|
25
27
|
redirect_to spree.edit_admin_order_customer_path(@order)
|
|
@@ -2,6 +2,9 @@ module Spree
|
|
|
2
2
|
module Admin
|
|
3
3
|
module System
|
|
4
4
|
class WaitingRoomSessionsController < Spree::Admin::BaseController
|
|
5
|
+
GUESTS_PER_PAGE_OPTIONS = [25, 50, 100, 200, 500, 1000].freeze
|
|
6
|
+
GUESTS_DEFAULT_PER_PAGE = 50
|
|
7
|
+
|
|
5
8
|
def index
|
|
6
9
|
@filter = params[:filter] if params[:filter] == 'load_test'
|
|
7
10
|
|
|
@@ -21,6 +24,114 @@ module Spree
|
|
|
21
24
|
|
|
22
25
|
redirect_back fallback_location: admin_system_waiting_room_sessions_path
|
|
23
26
|
end
|
|
27
|
+
|
|
28
|
+
# Only resolves date/doc_id for the form + the frame's src URL. The actual Firestore
|
|
29
|
+
# read happens in #guests_status, once the browser loads that src -- never here, or every
|
|
30
|
+
# full-page load would pay for a read whose result is thrown away unrendered.
|
|
31
|
+
def guests
|
|
32
|
+
@date = params[:date].presence || Time.zone.today.strftime('%Y-%m-%d')
|
|
33
|
+
@doc_id = params[:doc_id].presence
|
|
34
|
+
@per_page = guests_per_page
|
|
35
|
+
@in_queue_only = guests_in_queue_only?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# GET .../waiting_room_sessions/guests_status -- the poll target, mirroring
|
|
39
|
+
# SeatLockable#seat_lock_status: a standalone endpoint the async-operation-status
|
|
40
|
+
# Stimulus controller reloads every 10s, kept separate from `guests` so the initial
|
|
41
|
+
# full-page load never has to guess whether it's being polled.
|
|
42
|
+
def guests_status
|
|
43
|
+
date = params[:date].presence || Time.zone.today.strftime('%Y-%m-%d')
|
|
44
|
+
doc_id = params[:doc_id].presence
|
|
45
|
+
in_queue_only = guests_in_queue_only?
|
|
46
|
+
error = nil
|
|
47
|
+
|
|
48
|
+
guests =
|
|
49
|
+
if doc_id
|
|
50
|
+
# A direct doc-id lookup always finds the doc regardless of status -- an admin
|
|
51
|
+
# searching a specific id almost certainly wants to know its state (including
|
|
52
|
+
# Admitted/Left), not have it hidden by the same filter that shapes the plain list.
|
|
53
|
+
result = SpreeCmCommissioner::WaitingRoom::Guests::Find.call(date: date, doc_id: doc_id)
|
|
54
|
+
error = result.error.to_s unless result.success?
|
|
55
|
+
[result.value&.[](:guest)].compact
|
|
56
|
+
else
|
|
57
|
+
# Cached for the same span as the client's poll interval so N concurrently open admin
|
|
58
|
+
# tabs (each polling every 10s) collapse into one Firestore read per date per 10s,
|
|
59
|
+
# instead of one per open tab. Keyed by filter too -- "in queue" and "all" are
|
|
60
|
+
# different Firestore queries, not a client-side slice of the same result.
|
|
61
|
+
#
|
|
62
|
+
# A failed fetch (e.g. a missing Firestore composite index for in_queue_only's
|
|
63
|
+
# where+order combination) is never cached: caching `[]` would make the next 10s
|
|
64
|
+
# poll -- and the one after that -- believe the queue is empty instead of retrying.
|
|
65
|
+
cache_key = "admin/waiting_room_guests/#{date}/#{in_queue_only}"
|
|
66
|
+
docs = Rails.cache.read(cache_key)
|
|
67
|
+
|
|
68
|
+
if docs.nil?
|
|
69
|
+
result = SpreeCmCommissioner::WaitingRoom::Guests::List.call(date: date, in_queue_only: in_queue_only)
|
|
70
|
+
if result.success?
|
|
71
|
+
docs = result.value[:guests]
|
|
72
|
+
Rails.cache.write(cache_key, docs, expires_in: 10.seconds)
|
|
73
|
+
else
|
|
74
|
+
error = result.error.to_s
|
|
75
|
+
docs = []
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
Kaminari.paginate_array(docs).page(params[:page]).per(guests_per_page)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
render partial: 'guests_table',
|
|
83
|
+
locals: { guests: guests, doc_id_search: doc_id.present?, date: date, frontier_position: frontier_position, error: error },
|
|
84
|
+
layout: false
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# GET .../waiting_room_sessions/guest -- the click target for a row's "Raw" button. Not
|
|
88
|
+
# part of the cached list/poll cycle: a lone extra Firestore read only when an admin
|
|
89
|
+
# actually wants the full document, instead of shipping every field of every row (up to
|
|
90
|
+
# MAX_DOCS of them) in the guests_status response whether anyone looks at it or not.
|
|
91
|
+
def guest
|
|
92
|
+
# This is the one place in the tab meant to show the truth of this instant, not a
|
|
93
|
+
# 10s-old cached snapshot -- an admin opens it specifically to check a guest's current
|
|
94
|
+
# state. expires_now stops the browser from ever serving a previous click's response
|
|
95
|
+
# back for the same doc_id (e.g. reopening the same row after its state changed).
|
|
96
|
+
expires_now
|
|
97
|
+
|
|
98
|
+
date = params[:date].presence || Time.zone.today.strftime('%Y-%m-%d')
|
|
99
|
+
doc_id = params[:doc_id]
|
|
100
|
+
result = SpreeCmCommissioner::WaitingRoom::Guests::Find.call(date: date, doc_id: doc_id)
|
|
101
|
+
error = result.error.to_s unless result.success?
|
|
102
|
+
|
|
103
|
+
render partial: 'guest_raw', locals: { guest: result.value&.[](:guest), error: error }, layout: false
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
# Same shape as Spree's own admin index actions (e.g. Admin::OrdersController#index:
|
|
109
|
+
# `.per(params[:per_page] || Spree::Backend::Config[:admin_orders_per_page])`) -- an
|
|
110
|
+
# unlisted value still works (typed straight into the URL, same as any stock Spree index),
|
|
111
|
+
# GUESTS_PER_PAGE_OPTIONS only bounds what the dropdown itself offers.
|
|
112
|
+
def guests_per_page
|
|
113
|
+
requested = params[:per_page].to_i
|
|
114
|
+
requested.positive? ? requested : GUESTS_DEFAULT_PER_PAGE
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Defaults on: hiding Admitted/Left is the useful everyday view, and it's also what keeps
|
|
118
|
+
# the MAX_DOCS cap from being spent on already-resolved history on a day with heavy
|
|
119
|
+
# traffic (see WaitingRoom::Guests::List). ?status=all is the explicit escape hatch for
|
|
120
|
+
# historical/debugging lookups that need to see resolved guests too.
|
|
121
|
+
def guests_in_queue_only?
|
|
122
|
+
params[:status] != 'all'
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Same 10s window as the guest list itself: this is one extra doc read per render, and the
|
|
126
|
+
# "people ahead" figure it feeds only changes as often as Positions::Stamp ticks (10s),
|
|
127
|
+
# so caching it any shorter would just be re-fetching a value that hasn't moved.
|
|
128
|
+
def frontier_position
|
|
129
|
+
Rails.cache.fetch('admin/waiting_room_frontier_position', expires_in: 10.seconds) do
|
|
130
|
+
fetcher = SpreeCmCommissioner::WaitingRoomLobbyMetadataFetcher.new
|
|
131
|
+
fetcher.load_document_data
|
|
132
|
+
fetcher.frontier_position
|
|
133
|
+
end
|
|
134
|
+
end
|
|
24
135
|
end
|
|
25
136
|
end
|
|
26
137
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V2
|
|
4
|
+
module Platform
|
|
5
|
+
# Single reusable entry point for tenant-scoping any Platform API resource: pass
|
|
6
|
+
# `?tenant_id=X` on the request and the collection is narrowed to that tenant, without each
|
|
7
|
+
# controller having to wire up its own filter/param handling.
|
|
8
|
+
#
|
|
9
|
+
# Tri-state on the param itself:
|
|
10
|
+
# - absent: no scoping applied, collection flows through unrestricted (existing callers,
|
|
11
|
+
# eg. the products/users pickers in checkouts and private sales, are unaffected).
|
|
12
|
+
# - "0": scope to records with no tenant (tenant_id IS NULL) - most vendors today aren't
|
|
13
|
+
# attached to a tenant, so this is the common case for tenant-scoped search.
|
|
14
|
+
# - any other value: scope to that exact tenant_id.
|
|
15
|
+
#
|
|
16
|
+
# Default assumes the model owns a `tenant_id` column directly (true for Spree::User,
|
|
17
|
+
# Spree::Vendor, SpreeCmCommissioner::HomepageSection - see their `multi_tenant :tenant`
|
|
18
|
+
# declarations). A model related to tenant only indirectly should override
|
|
19
|
+
# `scope_by_tenant` in its own controller decorator.
|
|
20
|
+
module ResourceControllerDecorator
|
|
21
|
+
def scope
|
|
22
|
+
base_scope = super
|
|
23
|
+
|
|
24
|
+
# Keep the existing behavior when no tenant_id param is passed,
|
|
25
|
+
# so that existing controllers are unaffected.
|
|
26
|
+
return base_scope if params[:tenant_id].nil?
|
|
27
|
+
|
|
28
|
+
scope_by_tenant(base_scope, params[:tenant_id])
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def scope_by_tenant(scope, tenant_id)
|
|
34
|
+
return scope unless scope.klass.column_names.include?('tenant_id')
|
|
35
|
+
|
|
36
|
+
if tenant_id.to_s == '0'
|
|
37
|
+
scope.where(tenant_id: nil)
|
|
38
|
+
else
|
|
39
|
+
scope.where(tenant_id: tenant_id)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
Spree::Api::V2::Platform::ResourceController.prepend(Spree::Api::V2::Platform::ResourceControllerDecorator)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Admin
|
|
3
|
+
module System
|
|
4
|
+
# Shared badge/derived-value logic for the Guests tab (_guests_table.html.erb's Queue and
|
|
5
|
+
# Heartbeat columns, _guest_raw.html.erb's modal) -- kept in one place so both never drift
|
|
6
|
+
# into disagreeing about what "Admitted" or "Stale ping" means for the same document.
|
|
7
|
+
module WaitingRoomSessionsHelper
|
|
8
|
+
# Mutually exclusive lifecycle stage, in the order a guest actually moves through them.
|
|
9
|
+
# Left is checked first because Heartbeat::Sweep can mark left_at on a doc that was still
|
|
10
|
+
# mid-call (call_dispatched_at set but never granted) -- left always wins once set.
|
|
11
|
+
def guest_queue_status(guest)
|
|
12
|
+
if guest[:left_at]
|
|
13
|
+
['Left queue', 'badge-secondary']
|
|
14
|
+
elsif guest[:allow_to_enter_room_at]
|
|
15
|
+
%w[Admitted badge-success]
|
|
16
|
+
elsif guest[:call_dispatched_at]
|
|
17
|
+
['Being called', 'badge-info']
|
|
18
|
+
else
|
|
19
|
+
%w[Waiting badge-primary]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Only meaningful before a guest is resolved: once left_at or allow_to_enter_room_at is
|
|
24
|
+
# set, Heartbeat::Sweep's query (allow_to_enter_room_at == nil, left_at == nil) no longer
|
|
25
|
+
# looks at this doc's last_ping_at at all, so showing a ping state past that point would
|
|
26
|
+
# describe a check the server itself has stopped making.
|
|
27
|
+
def guest_heartbeat_status(guest)
|
|
28
|
+
return nil if guest[:left_at] || guest[:allow_to_enter_room_at]
|
|
29
|
+
|
|
30
|
+
last_ping_at = guest[:last_ping_at]
|
|
31
|
+
return ['Never pinged', 'badge-secondary'] if last_ping_at.nil?
|
|
32
|
+
|
|
33
|
+
stale = last_ping_at < SpreeCmCommissioner::WaitingRoom::Heartbeat::Sweep::HEARTBEAT_TTL_SECONDS.seconds.ago
|
|
34
|
+
stale ? ['Stale ping', 'badge-warning'] : ['Active ping', 'badge-success']
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Same formula the app itself shows the guest (see waiting_room_provider.dart's
|
|
38
|
+
# _peopleAheadOfYou): position minus the lobby's live frontier_position, floored at 1 so a
|
|
39
|
+
# guest at the very front never reads "0 ahead" (session presence, not this count, is what
|
|
40
|
+
# actually admits them). Nil until Positions::Stamp assigns this guest a position at all.
|
|
41
|
+
def guest_people_ahead(guest, frontier_position)
|
|
42
|
+
position = guest[:position]
|
|
43
|
+
return nil if position.nil?
|
|
44
|
+
|
|
45
|
+
[position - frontier_position.to_i, 1].max
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -37,7 +37,7 @@ module SpreeCmCommissioner
|
|
|
37
37
|
base.validate :ensure_all_guests_have_blocks, if: :required_seat_selection?
|
|
38
38
|
|
|
39
39
|
base.whitelisted_ransackable_associations |= %w[guests order]
|
|
40
|
-
base.whitelisted_ransackable_attributes |= %w[number to_date from_date vendor_id]
|
|
40
|
+
base.whitelisted_ransackable_attributes |= %w[number to_date from_date vendor_id event_id]
|
|
41
41
|
|
|
42
42
|
base.delegate :delivery_required?, :high_demand,
|
|
43
43
|
to: :variant
|
|
@@ -34,8 +34,8 @@ module SpreeCmCommissioner
|
|
|
34
34
|
seen = {}
|
|
35
35
|
|
|
36
36
|
line_item_contexts.each_with_object([]) do |line_item_context, result|
|
|
37
|
-
line_item_context.guest_contexts.select { |gc| guest_context_eligible?(gc, line_item_context) }.
|
|
38
|
-
key = guest_context.saved_guest_id.
|
|
37
|
+
line_item_context.guest_contexts.select { |gc| guest_context_eligible?(gc, line_item_context) }.each do |guest_context|
|
|
38
|
+
key = guest_context.saved_guest_id.presence || guest_context.id
|
|
39
39
|
next if seen[key]
|
|
40
40
|
|
|
41
41
|
seen[key] = true
|
|
@@ -34,7 +34,7 @@ module SpreeCmCommissioner
|
|
|
34
34
|
|
|
35
35
|
# Republish the parent session's Firestore document whenever anything the show screen renders
|
|
36
36
|
# changes, so a reveal reaches every device without a pull-to-refresh.
|
|
37
|
-
FIRESTORE_PUBLISHED_COLUMNS = %w[visible vote_number position eliminated].freeze
|
|
37
|
+
FIRESTORE_PUBLISHED_COLUMNS = %w[visible vote_number position eliminated confirmed_rank].freeze
|
|
38
38
|
|
|
39
39
|
after_commit :publish_session_to_firestore, on: %i[create update destroy]
|
|
40
40
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<% end %>
|
|
14
14
|
|
|
15
15
|
<% if can?(:fire, order) && order.can_cancel? %>
|
|
16
|
-
<% cancel_label = Spree.t(:cancel
|
|
16
|
+
<% cancel_label = Spree.t(:cancel) %>
|
|
17
17
|
<%= button_link_to cancel_label,
|
|
18
18
|
'#',
|
|
19
19
|
icon: 'cancel.svg',
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
<div class="modal-dialog" role="document">
|
|
25
25
|
<div class="modal-content">
|
|
26
26
|
<div class="modal-header">
|
|
27
|
-
<h5 class="modal-title
|
|
27
|
+
<h5 class="mr-auto modal-title" id="cancelModalLabel-<%= order.number %>"><%= Spree.t(:order_sure_want_to, event: cancel_label) %></h5>
|
|
28
28
|
<button type="button" class="close" data-dismiss="modal" aria-label="<%= Spree.t(:close) %>">
|
|
29
29
|
<span aria-hidden="true">×</span>
|
|
30
30
|
</button>
|
|
@@ -32,12 +32,12 @@
|
|
|
32
32
|
|
|
33
33
|
<%= form_with model: order, url: spree.cancel_admin_order_url(order), method: :put do |f| %>
|
|
34
34
|
<div class="modal-body">
|
|
35
|
-
<div class="form-group text-
|
|
35
|
+
<div class="text-left form-group text-start">
|
|
36
36
|
<%= f.label :internal_note, raw(Spree.t(:reason) + required_span_tag), class: 'd-block text-left text-start' %>
|
|
37
37
|
<%= f.text_area :internal_note, rows: 3, class: 'form-control', placeholder: Spree.t(:enter_internal_note, default: 'Enter reason here...'), required: true %>
|
|
38
38
|
</div>
|
|
39
39
|
</div>
|
|
40
|
-
<div class="modal-footer d-flex
|
|
40
|
+
<div class="justify-end modal-footer d-flex">
|
|
41
41
|
<button type="button" class="btn btn-primary" data-dismiss="modal"><%= Spree.t(:close) %></button>
|
|
42
42
|
<%= f.submit Spree.t(:confirm), class: 'btn btn-danger' %>
|
|
43
43
|
</div>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<!-- insert_before "[data-hook='admin_orders_index_search'] .row:nth-child(5) .col-12.col-lg-4:nth-child(3)" -->
|
|
2
|
+
|
|
3
|
+
<div class="col-12 col-lg-4">
|
|
4
|
+
<div class="form-group">
|
|
5
|
+
<%= label_tag :q_line_items_event_id_eq, "Event" %>
|
|
6
|
+
<%= f.select :line_items_event_id_eq, Spree::Taxon.events.pluck(:name, :id), { include_blank: true }, class: 'select2-clear js-filterable' %>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
data/app/overrides/spree/admin/orders/customer_details/edit/reassign_customer_search.html.erb.deface
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<!-- replace "#select-customer" -->
|
|
2
2
|
|
|
3
|
-
<% unless @order.completed? && @order.user.present? %>
|
|
4
3
|
<%= turbo_frame_tag 'customer_reassign' do %>
|
|
5
4
|
<div class="mb-3 card" data-hook>
|
|
6
5
|
<div class="card-header">
|
|
@@ -28,14 +27,15 @@
|
|
|
28
27
|
|
|
29
28
|
<% if @customer_search_query.present? %>
|
|
30
29
|
<% if @customer_search_result %>
|
|
30
|
+
<% replacing_user = @order.user.present? %>
|
|
31
31
|
<div class="flex-wrap gap-2 mt-2 mb-0 alert alert-success d-flex justify-content-between align-items-center">
|
|
32
32
|
<span><%= Spree.t('admin.orders.customer_details.customer_found', email: @customer_search_result.email) %></span>
|
|
33
|
-
<%= button_to Spree.t('admin.orders.customer_details.
|
|
33
|
+
<%= button_to replacing_user ? Spree.t('admin.orders.customer_details.replace_user') : Spree.t('admin.orders.customer_details.assign_user'),
|
|
34
34
|
spree.assign_customer_admin_order_customer_path(@order),
|
|
35
35
|
method: :patch,
|
|
36
36
|
params: { user_id: @customer_search_result.id },
|
|
37
|
-
class:
|
|
38
|
-
form: { data: { turbo_frame: '_top' } } %>
|
|
37
|
+
class: "btn btn-sm #{replacing_user ? 'btn-danger' : 'btn-primary'}",
|
|
38
|
+
form: { data: { turbo_frame: '_top', turbo_confirm: (Spree.t('admin.orders.customer_details.replace_user_confirm', email: @order.user.email) if replacing_user) } } %>
|
|
39
39
|
</div>
|
|
40
40
|
<% else %>
|
|
41
41
|
<div class="mt-2 mb-0 alert alert-warning">
|
|
@@ -46,4 +46,3 @@
|
|
|
46
46
|
</div>
|
|
47
47
|
</div>
|
|
48
48
|
<% end %>
|
|
49
|
-
<% end %>
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
<!-- insert_before "erb[loud]:contains('field_container :hide_from_nav')" -->
|
|
2
2
|
|
|
3
|
+
<% if @taxon.depth == 1 %>
|
|
4
|
+
|
|
3
5
|
<%= f.field_container :preview, class: ['custom-control', 'custom-checkbox', 'my-4'] do %>
|
|
4
6
|
<%= f.check_box :preview, class: 'custom-control-input' %>
|
|
5
7
|
<%= f.label :preview, 'Preview (internal team only)', class: 'custom-control-label' %>
|
|
6
8
|
<%= f.error_message_on :preview %>
|
|
7
9
|
<% end %>
|
|
10
|
+
|
|
11
|
+
<% end %>
|
|
@@ -3,11 +3,19 @@
|
|
|
3
3
|
<% if @taxon.depth == 1 %>
|
|
4
4
|
<div>
|
|
5
5
|
<%= f.field_container :vendor_id do %>
|
|
6
|
-
|
|
6
|
+
<label class="d-flex">
|
|
7
|
+
<span class="mr-2"><%= Spree.t(:vendor) %></span>
|
|
8
|
+
<% if f.object.vendor_id.present? %>
|
|
9
|
+
<%= link_to_with_icon 'arrow-right-circle.svg', Spree.t(' '), spree.edit_admin_vendor_path(f.object.vendor_id), class: "d-flex align-items-center" do %>
|
|
10
|
+
<i class="bi bi-arrow-right-circle me-1"></i>
|
|
11
|
+
<% end %>
|
|
12
|
+
<% else %>
|
|
13
|
+
<span class="text-muted"><%= Spree.t(:no_vendor_assigned) %></span>
|
|
14
|
+
<% end %>
|
|
15
|
+
</label>
|
|
7
16
|
<%= f.select :vendor_id, Spree::Vendor.order(:name).pluck(:name, :id),
|
|
8
17
|
{ include_blank: true }, { class: 'select2-clear js-filterable' } %>
|
|
9
18
|
<%= f.error_message_on :vendor_id, class: 'error-message' %>
|
|
10
19
|
<% end %>
|
|
11
20
|
</div>
|
|
12
21
|
<% end %>
|
|
13
|
-
|
|
@@ -128,9 +128,12 @@ module SpreeCmCommissioner
|
|
|
128
128
|
previous_guests + eligible_guests_in(records_path, remaining_slots)
|
|
129
129
|
end
|
|
130
130
|
|
|
131
|
-
# This query requires a composite index (allow_to_enter_room_at ==,
|
|
132
|
-
#
|
|
133
|
-
#
|
|
131
|
+
# This query requires a composite index (allow_to_enter_room_at ==, call_dispatched_at ==,
|
|
132
|
+
# left_at ==, queued_at order, position !=); create it in Firebase beforehand, same as the
|
|
133
|
+
# plain index #fetch_long_waiting_guests already needed. The `!=` field goes LAST, after the
|
|
134
|
+
# orderBy field, not right after the equality fields the way a `<`/`>` range filter would --
|
|
135
|
+
# Firestore's own generated index link is the source of truth here, not the field order
|
|
136
|
+
# `!=` filters read like in the query itself.
|
|
134
137
|
#
|
|
135
138
|
# left_at == nil excludes a guest who already left the queue (leaveQueue()/markLeft(), same
|
|
136
139
|
# write Positions::Stamp#lowest_unresolved_position_in and #sweep_abandoned both already
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module WaitingRoom
|
|
3
|
+
module Guests
|
|
4
|
+
# Single-doc lookup by id, for the admin "Guests" tab's search box. Deliberately a direct
|
|
5
|
+
# `get` rather than filtering List's result: a doc can exist outside List's queued_at-ordered
|
|
6
|
+
# MAX_DOCS window (or the request predates that cache), and a single-doc read costs the same
|
|
7
|
+
# whichever way it's found.
|
|
8
|
+
class Find
|
|
9
|
+
prepend ::Spree::ServiceModule::Base
|
|
10
|
+
include SpreeCmCommissioner::FirestoreConnection
|
|
11
|
+
|
|
12
|
+
def call(date:, doc_id:, firestore: nil)
|
|
13
|
+
@firestore = firestore if firestore.present?
|
|
14
|
+
|
|
15
|
+
guest = firestore_available? ? fetch(date, doc_id) : nil
|
|
16
|
+
|
|
17
|
+
success(guest: guest)
|
|
18
|
+
rescue StandardError => e
|
|
19
|
+
failure(nil, e.message)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def fetch(date, doc_id)
|
|
25
|
+
snapshot = firestore.col(records_path(date)).doc(doc_id).get
|
|
26
|
+
return nil unless snapshot.exists?
|
|
27
|
+
|
|
28
|
+
snapshot.data.merge(doc_id: snapshot.document_id)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def records_path(date)
|
|
32
|
+
"waiting_guests/#{date}/records"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module WaitingRoom
|
|
3
|
+
module Guests
|
|
4
|
+
# Raw read of a waiting-guest partition for the admin "Guests" tab — the only place in the
|
|
5
|
+
# codebase that lists the actual `waiting_guests/{date}/records` documents rather than
|
|
6
|
+
# deriving lobby-level aggregates from them (contrast Heartbeat::Sweep/Positions::Stamp/
|
|
7
|
+
# Admission::CallGuests, which only ever query for docs matching specific eligibility
|
|
8
|
+
# criteria, never dump the partition itself).
|
|
9
|
+
class List
|
|
10
|
+
prepend ::Spree::ServiceModule::Base
|
|
11
|
+
include SpreeCmCommissioner::FirestoreConnection
|
|
12
|
+
|
|
13
|
+
# Same ceiling the admin UI paginates over; keeps one read bounded regardless of how many
|
|
14
|
+
# guests are actually queued.
|
|
15
|
+
MAX_DOCS = 5000
|
|
16
|
+
|
|
17
|
+
# in_queue_only: true filters at the Firestore query level (not after fetching) to the
|
|
18
|
+
# same "not yet resolved" criteria Admission::CallGuests uses, and defaults on — without
|
|
19
|
+
# it, a partition with more than MAX_DOCS total docs for the day fills the cap with the
|
|
20
|
+
# OLDEST docs by queued_at, which past a certain volume are entirely already-resolved
|
|
21
|
+
# (Admitted/Left) guests, silently hiding everyone still actually waiting behind them.
|
|
22
|
+
def call(date:, in_queue_only: true, firestore: nil)
|
|
23
|
+
@firestore = firestore if firestore.present?
|
|
24
|
+
|
|
25
|
+
guests = firestore_available? ? fetch(date, in_queue_only) : []
|
|
26
|
+
|
|
27
|
+
success(guests: guests)
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
failure(nil, e.message)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# Ordered by queued_at (arrival order), not `position` — a not-yet-stamped doc has
|
|
35
|
+
# `position: nil` and would sort inconsistently, hiding guests who just joined the queue.
|
|
36
|
+
#
|
|
37
|
+
# in_queue_only's composite index (allow_to_enter_room_at ==, left_at ==, queued_at order)
|
|
38
|
+
# is tracked in firebase/firestore.indexes.json — a different, narrower shape than the one
|
|
39
|
+
# CallGuests needs (which also filters call_dispatched_at), so it isn't covered by that one
|
|
40
|
+
# already existing. Still needs `firebase deploy --only firestore:indexes` run against each
|
|
41
|
+
# project (see firebase/README.md) before this filter works live: adding it to the tracked
|
|
42
|
+
# file doesn't deploy it by itself.
|
|
43
|
+
def fetch(date, in_queue_only)
|
|
44
|
+
query = firestore.col(records_path(date))
|
|
45
|
+
query = query.where('allow_to_enter_room_at', '==', nil).where('left_at', '==', nil) if in_queue_only
|
|
46
|
+
|
|
47
|
+
query.order('queued_at', 'asc')
|
|
48
|
+
.limit(MAX_DOCS)
|
|
49
|
+
.get
|
|
50
|
+
.map { |snapshot| snapshot.data.merge(doc_id: snapshot.document_id) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def records_path(date)
|
|
54
|
+
"waiting_guests/#{date}/records"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -1,18 +1,22 @@
|
|
|
1
|
-
require 'google/cloud/firestore'
|
|
2
|
-
|
|
3
1
|
module SpreeCmCommissioner
|
|
4
2
|
# Reads the lobby document (waiting_rooms/lobby) — the read-side counterpart to
|
|
5
3
|
# WaitingRoom::PublishLobbyPath. Kept separate from WaitingRoomSystemMetadataFetcher,
|
|
6
4
|
# which owns the unrelated metadata/system document.
|
|
7
5
|
class WaitingRoomLobbyMetadataFetcher
|
|
6
|
+
include SpreeCmCommissioner::FirestoreConnection
|
|
7
|
+
|
|
8
8
|
attr_reader :document_data
|
|
9
9
|
|
|
10
10
|
def initialize(firestore: nil)
|
|
11
11
|
@firestore = firestore if firestore.present?
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
# Environments without the credential (dev, test, CI) get an empty document instead of a
|
|
15
|
+
# NoMethodError on a nil service_account -- every accessor below already treats a missing key
|
|
16
|
+
# in document_data as "unknown" (nil/false), so an empty hash here is a real, valid state, not
|
|
17
|
+
# a special case each of them has to know about.
|
|
14
18
|
def load_document_data
|
|
15
|
-
@document_data = document.get.data || {}
|
|
19
|
+
@document_data = firestore_available? ? (document.get.data || {}) : {}
|
|
16
20
|
end
|
|
17
21
|
|
|
18
22
|
# firebase metadata
|
|
@@ -32,16 +36,14 @@ module SpreeCmCommissioner
|
|
|
32
36
|
document_data[:available_slots]
|
|
33
37
|
end
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def firestore
|
|
40
|
-
@firestore ||= Google::Cloud::Firestore.new(project_id: service_account[:project_id], credentials: service_account)
|
|
39
|
+
# written by Positions::Stamp -- every position up to and including this one is resolved
|
|
40
|
+
# (granted or left). "N people ahead of you" for a stamped guest = their position minus this.
|
|
41
|
+
def frontier_position
|
|
42
|
+
document_data[:frontier_position]
|
|
41
43
|
end
|
|
42
44
|
|
|
43
|
-
def
|
|
44
|
-
@
|
|
45
|
+
def document
|
|
46
|
+
@document ||= firestore.col('waiting_rooms').doc('lobby')
|
|
45
47
|
end
|
|
46
48
|
end
|
|
47
49
|
end
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
<%# user event only show for event %>
|
|
34
34
|
<%= content_tag :li, class: 'nav-item' do %>
|
|
35
35
|
<%= link_to_with_icon 'user.svg',
|
|
36
|
-
Spree.t(:
|
|
36
|
+
Spree.t(:crews),
|
|
37
37
|
admin_user_events_path(params: {taxon_id: @taxon.id, taxonomy_id: @taxonomy}),
|
|
38
38
|
class: "nav-link #{'active' if current == :user_events}" %>
|
|
39
39
|
<% end if can?(:manage, SpreeCmCommissioner::UserTaxon) && @taxon.event? && @taxon.depth == 1 %>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<%# Rendered only when a row's "Raw" button is clicked (see _guests_table.html.erb) -- the full
|
|
2
|
+
document, every field, is never present in the guests_status response until an admin actually
|
|
3
|
+
asks for one row's worth of it. Targets the ONE shared modal frame (guests.html.erb's
|
|
4
|
+
#guest_raw_modal_frame), not a per-doc id: only one modal is ever open at a time, so each click
|
|
5
|
+
just replaces whatever the frame last held. %>
|
|
6
|
+
<%= turbo_frame_tag 'guest_raw_modal_frame' do %>
|
|
7
|
+
<% if guest %>
|
|
8
|
+
<%# Same helper _guests_table.html.erb's Queue/Heartbeat columns use -- one definition of what
|
|
9
|
+
"Admitted" or "Stale ping" means, so the table and this modal never disagree about it. %>
|
|
10
|
+
<% queue_label, queue_badge = guest_queue_status(guest) %>
|
|
11
|
+
<% heartbeat = guest_heartbeat_status(guest) %>
|
|
12
|
+
|
|
13
|
+
<div class="mb-3 d-flex align-items-center flex-wrap">
|
|
14
|
+
<span class="mr-2 text-muted small text-uppercase" style="letter-spacing: .04em;">Queue</span>
|
|
15
|
+
<span class="mr-3 badge <%= queue_badge %>"><%= queue_label %></span>
|
|
16
|
+
|
|
17
|
+
<% if heartbeat %>
|
|
18
|
+
<% heartbeat_label, heartbeat_badge = heartbeat %>
|
|
19
|
+
<span class="mr-2 text-muted small text-uppercase" style="letter-spacing: .04em;">Heartbeat</span>
|
|
20
|
+
<span class="badge <%= heartbeat_badge %>"><%= heartbeat_label %></span>
|
|
21
|
+
<% end %>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<% session = guest[:session] %>
|
|
25
|
+
<% if session %>
|
|
26
|
+
<% session_expired_at = session[:expired_at] %>
|
|
27
|
+
<% session_state, session_badge =
|
|
28
|
+
if session_expired_at.nil?
|
|
29
|
+
['Unknown', 'badge-secondary']
|
|
30
|
+
elsif session_expired_at > Time.zone.now
|
|
31
|
+
['Active', 'badge-success']
|
|
32
|
+
else
|
|
33
|
+
['Expired', 'badge-danger']
|
|
34
|
+
end %>
|
|
35
|
+
|
|
36
|
+
<div class="mb-3 p-3 border rounded">
|
|
37
|
+
<div class="mb-2 d-flex align-items-center flex-wrap">
|
|
38
|
+
<span class="mr-2 text-muted small text-uppercase" style="letter-spacing: .04em;">Session</span>
|
|
39
|
+
<span class="badge <%= session_badge %>"><%= session_state %></span>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<table class="mb-0 table table-sm table-borderless">
|
|
43
|
+
<tbody>
|
|
44
|
+
<tr>
|
|
45
|
+
<td class="text-muted" style="width: 120px;">ID</td>
|
|
46
|
+
<td><code><%= session[:id] || '—' %></code></td>
|
|
47
|
+
</tr>
|
|
48
|
+
<tr>
|
|
49
|
+
<td class="text-muted">Created</td>
|
|
50
|
+
<td><%= session[:created_at]&.strftime('%Y-%m-%d %H:%M:%S') || '—' %></td>
|
|
51
|
+
</tr>
|
|
52
|
+
<tr>
|
|
53
|
+
<td class="text-muted">Expires</td>
|
|
54
|
+
<td><%= session_expired_at&.strftime('%Y-%m-%d %H:%M:%S') || '—' %></td>
|
|
55
|
+
</tr>
|
|
56
|
+
</tbody>
|
|
57
|
+
</table>
|
|
58
|
+
</div>
|
|
59
|
+
<% else %>
|
|
60
|
+
<div class="mb-3 text-muted small">No session issued yet.</div>
|
|
61
|
+
<% end %>
|
|
62
|
+
|
|
63
|
+
<div class="mb-2 text-muted small text-uppercase" style="letter-spacing: .04em;">Raw document</div>
|
|
64
|
+
<pre class="p-2 mb-0 text-left rounded bg-light" style="max-height: 400px; overflow: auto;"><%= JSON.pretty_generate(guest) %></pre>
|
|
65
|
+
<% elsif error.present? %>
|
|
66
|
+
<div class="alert alert-danger mb-0">Could not load this document: <%= error %></div>
|
|
67
|
+
<% else %>
|
|
68
|
+
<span class="text-muted small">Document not found (may have been resolved or swept since the list loaded).</span>
|
|
69
|
+
<% end %>
|
|
70
|
+
<% end %>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<%# Mirrors Spree's own spree/admin/shared/_index_table_options partial (pagination left,
|
|
2
|
+
per-page select right) so this page behaves the same as every other admin index.
|
|
3
|
+
Doesn't reuse that partial's global `.js-per-page-select` JS (ransack.es6 in
|
|
4
|
+
spree_backend), though: that handler is bound once on the page-wide `spree:load` event,
|
|
5
|
+
which has already fired by the time Turbo lazy-loads this frame's content -- the select
|
|
6
|
+
doesn't exist yet when the binding runs, so it silently never receives the listener and
|
|
7
|
+
picking a value does nothing. `onchange` on the element itself has no such timing
|
|
8
|
+
dependency. Submits to the full #guests page -- the same place the date chips and doc-ID
|
|
9
|
+
search above already navigate to -- rather than straight to #guests_status, so every
|
|
10
|
+
control on this page reloads the same way instead of some doing a full page load and one
|
|
11
|
+
doing a frame-scoped partial swap.
|
|
12
|
+
|
|
13
|
+
Placed above the table, not below: with up to MAX_DOCS rows on a page, scrolling to the
|
|
14
|
+
bottom just to change page or page size is the exact friction this is meant to avoid. %>
|
|
15
|
+
<div class="row index-pagination-row mb-2">
|
|
16
|
+
<div class="col-12 col-sm-9 text-center text-sm-left">
|
|
17
|
+
<%= paginate guests, theme: 'admin-twitter-bootstrap-4' %>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="col-12 col-sm-3 text-center text-sm-left">
|
|
20
|
+
<%# data-turbo-frame="_top": this form sits inside the polling turbo-frame, and #guests'
|
|
21
|
+
response contains that same frame id -- without this, Turbo would treat the submit
|
|
22
|
+
as a frame-scoped swap (URL bar and the rest of the page left untouched) rather than
|
|
23
|
+
the real full-page reload wanted here. %>
|
|
24
|
+
<%= form_tag guests_admin_system_waiting_room_sessions_path(date: date, doc_id: params[:doc_id], status: params[:status]),
|
|
25
|
+
method: :get, data: { turbo_frame: '_top' },
|
|
26
|
+
class: 'form-inline justify-content-center justify-content-sm-end' do %>
|
|
27
|
+
<%= select_tag :per_page,
|
|
28
|
+
options_for_select(
|
|
29
|
+
Spree::Admin::System::WaitingRoomSessionsController::GUESTS_PER_PAGE_OPTIONS,
|
|
30
|
+
params[:per_page].presence&.to_i || Spree::Admin::System::WaitingRoomSessionsController::GUESTS_DEFAULT_PER_PAGE
|
|
31
|
+
),
|
|
32
|
+
class: 'w-auto form-control custom-select custom-select-sm',
|
|
33
|
+
onchange: 'this.form.requestSubmit()' %>
|
|
34
|
+
<% end %>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<%# Rendered by both #guests (as the frame's lazy-loaded src target) and #guests_status (every poll).
|
|
2
|
+
Wraps the SAME frame id `guests.html.erb` gave a `src` to, but WITHOUT a src attribute here --
|
|
3
|
+
Turbo refuses a response whose matching frame still carries the URL it was just fetched from
|
|
4
|
+
("has a source URL which references itself"), so the src stays only on the DOM element Turbo
|
|
5
|
+
already has, set once on initial load. See async_operation_status_controller.js and
|
|
6
|
+
shared_console/setup/inventory_items/_seat_lock_status.html.erb for the same split. %>
|
|
7
|
+
<%= turbo_frame_tag 'async_operation_status_frame' do %>
|
|
8
|
+
<%# The poller lives on this inner element, never on the frame itself: frame.reload() replaces
|
|
9
|
+
the frame's children and leaves its own attributes alone, so a flag on the frame would never
|
|
10
|
+
change and the poll would never stop. Each reload disconnects this controller (clearing its
|
|
11
|
+
timer) and connects a fresh one -- which is what keeps a doc-id search result from being
|
|
12
|
+
polled at all, since `in-progress` is always "true" here rather than driven by a real
|
|
13
|
+
terminal state (there's no "done" for a live guest list -- it always keeps refreshing while
|
|
14
|
+
this tab is open). %>
|
|
15
|
+
<div data-controller="async-operation-status"
|
|
16
|
+
data-async-operation-status-in-progress-value="true"
|
|
17
|
+
data-async-operation-status-interval-value="10000">
|
|
18
|
+
<% if error.present? %>
|
|
19
|
+
<div class="alert alert-danger mb-3">
|
|
20
|
+
Could not load guests: <%= error %>
|
|
21
|
+
</div>
|
|
22
|
+
<% end %>
|
|
23
|
+
|
|
24
|
+
<%= render 'guests_pagination', guests: guests, date: date unless doc_id_search %>
|
|
25
|
+
|
|
26
|
+
<%# A fixed, curated set of columns -- not one per field in the document. Queue/Heartbeat
|
|
27
|
+
already summarize left_at/call_dispatched_at/allow_to_enter_room_at/last_ping_at, and
|
|
28
|
+
anything else (including the session map) is one Raw click away, so there's no dynamic
|
|
29
|
+
per-doc column list to keep in sync here. %>
|
|
30
|
+
<div class="table-responsive">
|
|
31
|
+
<table class="table table-sm table-hover mb-0">
|
|
32
|
+
<thead class="text-muted">
|
|
33
|
+
<tr>
|
|
34
|
+
<th>doc_id</th>
|
|
35
|
+
<th>queued_at</th>
|
|
36
|
+
<th>people_ahead_of_you</th>
|
|
37
|
+
<th>position</th>
|
|
38
|
+
<th>Queue</th>
|
|
39
|
+
<th>Heartbeat</th>
|
|
40
|
+
<th>Raw</th>
|
|
41
|
+
</tr>
|
|
42
|
+
</thead>
|
|
43
|
+
<tbody>
|
|
44
|
+
<% guests.each do |guest| %>
|
|
45
|
+
<% queue_label, queue_badge = guest_queue_status(guest) %>
|
|
46
|
+
<% heartbeat = guest_heartbeat_status(guest) %>
|
|
47
|
+
<tr>
|
|
48
|
+
<td class="align-top"><code><%= guest[:doc_id] %></code></td>
|
|
49
|
+
<td class="align-top"><%= guest[:queued_at]&.strftime('%Y-%m-%d %H:%M:%S') || '—' %></td>
|
|
50
|
+
<td class="align-top"><%= guest_people_ahead(guest, frontier_position) || '—' %></td>
|
|
51
|
+
<td class="align-top"><%= guest[:position] || '—' %></td>
|
|
52
|
+
<td class="align-top"><span class="badge <%= queue_badge %>"><%= queue_label %></span></td>
|
|
53
|
+
<td class="align-top">
|
|
54
|
+
<% if heartbeat %>
|
|
55
|
+
<% heartbeat_label, heartbeat_badge = heartbeat %>
|
|
56
|
+
<span class="badge <%= heartbeat_badge %>"><%= heartbeat_label %></span>
|
|
57
|
+
<% else %>
|
|
58
|
+
—
|
|
59
|
+
<% end %>
|
|
60
|
+
</td>
|
|
61
|
+
|
|
62
|
+
<%# Nothing is fetched or rendered for this row until clicked: showGuestRawModal
|
|
63
|
+
(defined once in guests.html.erb) points the shared modal's frame at this URL
|
|
64
|
+
and opens the modal. A <button>, not a link -- this row sits inside the polling
|
|
65
|
+
turbo-frame, and an <a> here would get captured by Turbo's own frame-scoped
|
|
66
|
+
navigation before this row's markup is even a link click away from mattering. %>
|
|
67
|
+
<td class="align-top">
|
|
68
|
+
<%= button_tag 'View', type: 'button',
|
|
69
|
+
onclick: "showGuestRawModal('#{escape_javascript(guest_admin_system_waiting_room_sessions_path(date: date, doc_id: guest[:doc_id]))}')",
|
|
70
|
+
class: 'btn btn-outline-secondary btn-sm' %>
|
|
71
|
+
</td>
|
|
72
|
+
</tr>
|
|
73
|
+
<% end %>
|
|
74
|
+
|
|
75
|
+
<% if guests.empty? %>
|
|
76
|
+
<tr>
|
|
77
|
+
<td colspan="7" class="text-center text-muted py-4">
|
|
78
|
+
<%= doc_id_search ? 'No guest document found with that ID for this date.' : 'No guest documents for this date.' %>
|
|
79
|
+
</td>
|
|
80
|
+
</tr>
|
|
81
|
+
<% end %>
|
|
82
|
+
</tbody>
|
|
83
|
+
</table>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
<% end %>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<ul class="nav nav-tabs mb-3">
|
|
2
|
+
<li class="nav-item">
|
|
3
|
+
<%= link_to 'Sessions', admin_system_waiting_room_sessions_path,
|
|
4
|
+
class: "nav-link #{'active' if current == :sessions}" %>
|
|
5
|
+
</li>
|
|
6
|
+
<li class="nav-item">
|
|
7
|
+
<%= link_to 'Guests', guests_admin_system_waiting_room_sessions_path,
|
|
8
|
+
class: "nav-link #{'active' if current == :guests}" %>
|
|
9
|
+
</li>
|
|
10
|
+
</ul>
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<%= render 'spree/admin/shared/system_tabs', current: :waiting_room %>
|
|
2
|
+
<%= render 'tabs', current: :guests %>
|
|
3
|
+
|
|
4
|
+
<div class="mb-3 card">
|
|
5
|
+
<div class="card-header">
|
|
6
|
+
<h5 class="mb-0">Firestore waiting-room guests</h5>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div class="card-body">
|
|
10
|
+
<% today = Time.zone.today.strftime('%Y-%m-%d') %>
|
|
11
|
+
<% yesterday = Time.zone.yesterday.strftime('%Y-%m-%d') %>
|
|
12
|
+
<%# nil (omitted from the URL), not 'in_queue' -- keeps the common-case URL clean, and matches
|
|
13
|
+
the controller's own `params[:status] != 'all'` default-on check. %>
|
|
14
|
+
<% status_param = @in_queue_only ? nil : 'all' %>
|
|
15
|
+
|
|
16
|
+
<%# Only today/yesterday get chip buttons -- the common case by far, and a free-text date
|
|
17
|
+
input invites typos a date picker would need extra JS to avoid. `date` is still a plain
|
|
18
|
+
query param the controller happily accepts for any value, so any other date stays one
|
|
19
|
+
URL edit away without needing UI for it. %>
|
|
20
|
+
<div class="mr-3 form-group d-inline-block">
|
|
21
|
+
<label class="mb-0 mr-2">Date</label>
|
|
22
|
+
<div class="btn-group btn-group-sm" role="group">
|
|
23
|
+
<%= link_to 'Today', guests_admin_system_waiting_room_sessions_path(date: today, doc_id: @doc_id, per_page: @per_page, status: status_param),
|
|
24
|
+
class: "btn btn-outline-secondary #{'active' if @date == today}" %>
|
|
25
|
+
<%= link_to 'Yesterday', guests_admin_system_waiting_room_sessions_path(date: yesterday, doc_id: @doc_id, per_page: @per_page, status: status_param),
|
|
26
|
+
class: "btn btn-outline-secondary #{'active' if @date == yesterday}" %>
|
|
27
|
+
</div>
|
|
28
|
+
<% unless [today, yesterday].include?(@date) %>
|
|
29
|
+
<span class="ml-2 text-muted small">Viewing <code><%= @date %></code> (set via URL)</span>
|
|
30
|
+
<% end %>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<%# In queue only (default) hides Admitted/Left -- it's the everyday view, and it's also what
|
|
34
|
+
keeps WaitingRoom::Guests::List::MAX_DOCS from being spent on already-resolved history on
|
|
35
|
+
a busy day (see that class). All is the explicit opt-in for historical/debugging lookups. %>
|
|
36
|
+
<div class="mr-3 form-group d-inline-block">
|
|
37
|
+
<label class="mb-0 mr-2">Status</label>
|
|
38
|
+
<div class="btn-group btn-group-sm" role="group">
|
|
39
|
+
<%= link_to 'In queue', guests_admin_system_waiting_room_sessions_path(date: @date, doc_id: @doc_id, per_page: @per_page),
|
|
40
|
+
class: "btn btn-outline-secondary #{'active' if @in_queue_only}" %>
|
|
41
|
+
<%= link_to 'All', guests_admin_system_waiting_room_sessions_path(date: @date, doc_id: @doc_id, per_page: @per_page, status: 'all'),
|
|
42
|
+
class: "btn btn-outline-secondary #{'active' unless @in_queue_only}" %>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<%= form_tag guests_admin_system_waiting_room_sessions_path, method: :get, class: 'form-inline mb-3' do %>
|
|
47
|
+
<%= hidden_field_tag :date, @date, id: nil %>
|
|
48
|
+
<%= hidden_field_tag :status, status_param, id: nil %>
|
|
49
|
+
|
|
50
|
+
<div class="mr-3 form-group">
|
|
51
|
+
<%= label_tag :doc_id, 'Doc ID', class: 'mr-2 mb-0' %>
|
|
52
|
+
<%= text_field_tag :doc_id, @doc_id, class: 'form-control form-control-sm', placeholder: 'search by exact doc id' %>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<%= hidden_field_tag :per_page, @per_page, id: nil %>
|
|
56
|
+
|
|
57
|
+
<%= button_tag 'Search', type: :submit, class: 'btn btn-outline-secondary btn-sm mr-2' %>
|
|
58
|
+
|
|
59
|
+
<% if @doc_id.present? %>
|
|
60
|
+
<%= link_to 'Clear search', guests_admin_system_waiting_room_sessions_path(date: @date, per_page: @per_page, status: status_param), class: 'btn btn-link btn-sm' %>
|
|
61
|
+
<% end %>
|
|
62
|
+
<% end %>
|
|
63
|
+
|
|
64
|
+
<p class="mb-3 text-muted small">
|
|
65
|
+
Raw documents under <code>waiting_guests/<%= @date %>/records</code>, up to
|
|
66
|
+
<%= SpreeCmCommissioner::WaitingRoom::Guests::List::MAX_DOCS %> most-recently-queued<%= ' still in queue' if @in_queue_only %>.
|
|
67
|
+
This list refreshes automatically every 10 seconds.
|
|
68
|
+
</p>
|
|
69
|
+
|
|
70
|
+
<%# `src` is set HERE, once, on initial load -- the polled response (#guests_status, via
|
|
71
|
+
_guests_table.html.erb) re-renders this same frame id WITHOUT src, so this attribute
|
|
72
|
+
stays put across every reload() call the async-operation-status controller makes. %>
|
|
73
|
+
<%= turbo_frame_tag 'async_operation_status_frame',
|
|
74
|
+
src: guests_status_admin_system_waiting_room_sessions_path(date: @date, doc_id: @doc_id, page: params[:page], per_page: @per_page, status: status_param) do %>
|
|
75
|
+
<div class="py-4 d-flex justify-content-center">
|
|
76
|
+
<span class="spinner-border spinner-border-sm text-muted" role="status"></span>
|
|
77
|
+
</div>
|
|
78
|
+
<% end %>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<%# One shared modal for every row's "Raw" button (see _guests_table.html.erb), not one per row --
|
|
83
|
+
up to MAX_DOCS of those would mean pre-building MAX_DOCS empty modals whether anyone opens one
|
|
84
|
+
or not. Its frame has no `src`: nothing is fetched until a click points it at a URL below. Lives
|
|
85
|
+
outside the polling frame above so a 10s refresh never touches it while it's open.
|
|
86
|
+
|
|
87
|
+
`display: none` toggled directly in JS below, not via Bootstrap's .fade/.show classes and
|
|
88
|
+
`$(...).modal('show')` -- this app's bundle doesn't have Bootstrap's jQuery modal plugin
|
|
89
|
+
loaded (`$.fn.modal` is undefined here), so this styles and shows/hides itself with no
|
|
90
|
+
dependency on which JS bundle happens to be on the page. %>
|
|
91
|
+
<div id="guest_raw_modal" role="dialog" aria-labelledby="guest_raw_modal_label" aria-hidden="true"
|
|
92
|
+
style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 1050; background: rgba(0, 0, 0, 0.5);"
|
|
93
|
+
onclick="if (event.target === this) closeGuestRawModal();">
|
|
94
|
+
<div class="modal-dialog modal-lg" role="document" style="margin-top: 5vh;">
|
|
95
|
+
<div class="modal-content">
|
|
96
|
+
<div class="modal-header">
|
|
97
|
+
<h5 class="modal-title" id="guest_raw_modal_label">Guest document</h5>
|
|
98
|
+
<button type="button" class="close" aria-label="Close" onclick="closeGuestRawModal()">
|
|
99
|
+
<span aria-hidden="true">×</span>
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="modal-body">
|
|
103
|
+
<%= turbo_frame_tag 'guest_raw_modal_frame' do %>
|
|
104
|
+
<div class="text-muted small">Loading…</div>
|
|
105
|
+
<% end %>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<script>
|
|
112
|
+
// Called via a plain onclick on each row's "View" button (see _guests_table.html.erb) -- not an
|
|
113
|
+
// <a>. Turbo intercepts every <a> click inside a turbo-frame by default and tries to load the
|
|
114
|
+
// response into the nearest enclosing frame, which for a row is the polling
|
|
115
|
+
// async_operation_status_frame above; that frame's id has no match in the raw-document response,
|
|
116
|
+
// so Turbo would quietly clobber the table instead of opening anything. A <button> has no
|
|
117
|
+
// navigation for Turbo to intercept, so this function is just left to run.
|
|
118
|
+
function showGuestRawModal(url) {
|
|
119
|
+
// A turbo-frame keeps its old content visible while a new `src` loads (that's what lets
|
|
120
|
+
// polling elsewhere on this page swap without a blank flash) -- here that means the PREVIOUS
|
|
121
|
+
// row's document would stay on screen, unchanged, until the new fetch resolves. Clearing it
|
|
122
|
+
// first makes each click show its own row, never a stale one.
|
|
123
|
+
var frame = document.getElementById('guest_raw_modal_frame');
|
|
124
|
+
frame.innerHTML = '<div class="text-muted small">Loading…</div>';
|
|
125
|
+
|
|
126
|
+
// Cache-busted so reopening the SAME row's modal always issues a fresh request instead of
|
|
127
|
+
// Turbo/the browser reusing an identical prior GET for this exact URL -- this endpoint exists
|
|
128
|
+
// to show the guest's current Firestore state, not whatever it was on the last click.
|
|
129
|
+
frame.src = url + (url.indexOf('?') === -1 ? '?' : '&') + '_=' + Date.now();
|
|
130
|
+
|
|
131
|
+
document.getElementById('guest_raw_modal').style.display = 'block';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function closeGuestRawModal() {
|
|
135
|
+
document.getElementById('guest_raw_modal').style.display = 'none';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
document.addEventListener('keydown', function(event) {
|
|
139
|
+
if (event.key === 'Escape') closeGuestRawModal();
|
|
140
|
+
});
|
|
141
|
+
</script>
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<%= render 'spree/admin/shared/system_tabs', current: :waiting_room %>
|
|
2
|
-
|
|
3
|
-
<%= link_to_with_icon 'arrow-left.svg', "Back to Waiting Room", admin_system_waiting_room_path, class: 'btn btn-outline-secondary btn-sm mb-3' %>
|
|
2
|
+
<%= render 'tabs', current: :sessions %>
|
|
4
3
|
|
|
5
4
|
<div class="card mb-3">
|
|
6
5
|
<div class="card-header d-flex justify-content-between align-items-center flex-wrap">
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
{ include_hidden: true },
|
|
20
20
|
data: { autocomplete_url_value: 'users_api_v2',
|
|
21
21
|
autocomplete_return_attr_value: 'email',
|
|
22
|
-
autocomplete_search_query_value: '
|
|
22
|
+
autocomplete_search_query_value: 'email_or_normalized_login_or_phone_number_i_cont',
|
|
23
|
+
autocomplete_additional_url_params_value: "tenant_id=#{@taxon.vendor&.tenant_id || 0}",
|
|
23
24
|
minimum_input_length: 3 } %>
|
|
24
25
|
<%= f.error_message_on :user_id %>
|
|
25
26
|
<% end %>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<% end %>
|
|
12
12
|
|
|
13
13
|
<% content_for :page_actions do %>
|
|
14
|
-
<%= button_link_to Spree.t(:
|
|
14
|
+
<%= button_link_to Spree.t(:add_new_crews), new_admin_user_event_path(params: {taxon_id: @taxon.id}), class: "btn-success", icon: 'add.svg', id: 'admin_new_user_events_link' %>
|
|
15
15
|
<% end if can? :create, SpreeCmCommissioner::UserTaxon %>
|
|
16
16
|
|
|
17
17
|
<%= render partial: 'spree/admin/shared/taxon_tabs', locals: { current: :user_events } %>
|
data/config/locales/en.yml
CHANGED
|
@@ -660,8 +660,11 @@ en:
|
|
|
660
660
|
customer_found: "Found %{email}."
|
|
661
661
|
customer_not_found: 'No customer found for "%{query}" in this tenant.'
|
|
662
662
|
clear_search: "Clear search"
|
|
663
|
-
|
|
664
|
-
|
|
663
|
+
assign_user: "Assign User"
|
|
664
|
+
replace_user: "Replace User"
|
|
665
|
+
replace_user_confirm: "This order is currently assigned to %{email}. Replace them with the user found above?"
|
|
666
|
+
user_assigned: "Order reassigned to %{email}."
|
|
667
|
+
user_already_assigned: "This order is already assigned to %{email}."
|
|
665
668
|
event:
|
|
666
669
|
check_in:
|
|
667
670
|
success: "Guest check-in in successfully"
|
data/config/locales/km.yml
CHANGED
|
@@ -433,8 +433,11 @@ km:
|
|
|
433
433
|
customer_found: "រកឃើញ %{email}។"
|
|
434
434
|
customer_not_found: 'រកមិនឃើញអតិថិជនសម្រាប់ "%{query}" ក្នុង tenant នេះទេ។'
|
|
435
435
|
clear_search: "សម្អាតការស្វែងរក"
|
|
436
|
-
|
|
437
|
-
|
|
436
|
+
assign_user: "ផ្តល់ជូនអ្នកប្រើប្រាស់"
|
|
437
|
+
replace_user: "ជំនួសអ្នកប្រើប្រាស់"
|
|
438
|
+
replace_user_confirm: "លំដាប់នេះកំពុងភ្ជាប់ជាមួយ %{email} រួចហើយ។ តើជំនួសដោយអ្នកប្រើប្រាស់ដែលរកឃើញខាងលើមែនទេ?"
|
|
439
|
+
user_assigned: "លំដាប់នេះត្រូវបានផ្លាស់ប្តូរទៅ %{email}។"
|
|
440
|
+
user_already_assigned: "លំដាប់នេះបានភ្ជាប់ជាមួយ %{email} រួចហើយ។"
|
|
438
441
|
event:
|
|
439
442
|
check_in:
|
|
440
443
|
success: "Guest check-in in successfully"
|
data/config/routes.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spree_cm_commissioner
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.13.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- You
|
|
@@ -961,6 +961,7 @@ files:
|
|
|
961
961
|
- app/controllers/spree/api/v2/platform/homepage_section_relatable_options_controller.rb
|
|
962
962
|
- app/controllers/spree/api/v2/platform/option_types_controller_decorator.rb
|
|
963
963
|
- app/controllers/spree/api/v2/platform/places_controller.rb
|
|
964
|
+
- app/controllers/spree/api/v2/platform/resource_controller_decorator.rb
|
|
964
965
|
- app/controllers/spree/api/v2/platform/seat_number_layouts_controller.rb
|
|
965
966
|
- app/controllers/spree/api/v2/platform/taxons_controller_decorator.rb
|
|
966
967
|
- app/controllers/spree/api/v2/resource_controller_decorator.rb
|
|
@@ -1226,6 +1227,7 @@ files:
|
|
|
1226
1227
|
- app/finders/spree_cm_commissioner/variants/find_by_option_value_ids.rb
|
|
1227
1228
|
- app/helpers/spree/admin/base_helper_decorator.rb
|
|
1228
1229
|
- app/helpers/spree/admin/google_wallets_helper.rb
|
|
1230
|
+
- app/helpers/spree/admin/system/waiting_room_sessions_helper.rb
|
|
1229
1231
|
- app/helpers/spree/base_helper_decorator.rb
|
|
1230
1232
|
- app/helpers/spree/billing/navigation_helper_decorator.rb
|
|
1231
1233
|
- app/helpers/spree/billing/sortable_tree_helper.rb
|
|
@@ -1833,6 +1835,7 @@ files:
|
|
|
1833
1835
|
- app/overrides/spree/admin/orders/_search/line_item_number_search_field.html.erb.deface
|
|
1834
1836
|
- app/overrides/spree/admin/orders/_search/payment_method_search_field.html.erb.deface
|
|
1835
1837
|
- app/overrides/spree/admin/orders/_search/payment_number_search_field.html.erb.deface
|
|
1838
|
+
- app/overrides/spree/admin/orders/_search/taxon_search_field.html.erb.deface
|
|
1836
1839
|
- app/overrides/spree/admin/orders/_search/vendor_search_field.html.erb.deface
|
|
1837
1840
|
- app/overrides/spree/admin/orders/customer_details/_form/phone_number_field.html.erb.deface
|
|
1838
1841
|
- app/overrides/spree/admin/orders/customer_details/_form/view_profile_link.html.erb.deface
|
|
@@ -2481,6 +2484,8 @@ files:
|
|
|
2481
2484
|
- app/services/spree_cm_commissioner/waiting_room/admission/call_guests_chunk.rb
|
|
2482
2485
|
- app/services/spree_cm_commissioner/waiting_room/advance_queue.rb
|
|
2483
2486
|
- app/services/spree_cm_commissioner/waiting_room/bypass_eligibility.rb
|
|
2487
|
+
- app/services/spree_cm_commissioner/waiting_room/guests/find.rb
|
|
2488
|
+
- app/services/spree_cm_commissioner/waiting_room/guests/list.rb
|
|
2484
2489
|
- app/services/spree_cm_commissioner/waiting_room/heartbeat/sweep.rb
|
|
2485
2490
|
- app/services/spree_cm_commissioner/waiting_room/positions/stamp.rb
|
|
2486
2491
|
- app/services/spree_cm_commissioner/waiting_room/publish_lobby_path.rb
|
|
@@ -2706,6 +2711,11 @@ files:
|
|
|
2706
2711
|
- app/views/spree/admin/system/maintenance_tasks/_search.html.erb
|
|
2707
2712
|
- app/views/spree/admin/system/maintenance_tasks/index.html.erb
|
|
2708
2713
|
- app/views/spree/admin/system/waiting_room/show.html.erb
|
|
2714
|
+
- app/views/spree/admin/system/waiting_room_sessions/_guest_raw.html.erb
|
|
2715
|
+
- app/views/spree/admin/system/waiting_room_sessions/_guests_pagination.html.erb
|
|
2716
|
+
- app/views/spree/admin/system/waiting_room_sessions/_guests_table.html.erb
|
|
2717
|
+
- app/views/spree/admin/system/waiting_room_sessions/_tabs.html.erb
|
|
2718
|
+
- app/views/spree/admin/system/waiting_room_sessions/guests.html.erb
|
|
2709
2719
|
- app/views/spree/admin/system/waiting_room_sessions/index.html.erb
|
|
2710
2720
|
- app/views/spree/admin/taxon_childrens/index.html.erb
|
|
2711
2721
|
- app/views/spree/admin/taxon_vendors/_form.html.erb
|