diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 4a39ffc..1133ab4 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -1138,6 +1138,22 @@ hr { .table tr:first-child td { border-top: 0; } +/* Horizontal scroll wrapper for wide tables on narrow screens. + The table inside keeps width: 100% but expands beyond the wrapper + when its content min-width exceeds the available space — at which + point overflow-x lets the user swipe through it. */ +.table-scroll { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + border-radius: 0.5rem; + box-shadow: 0 1px 3px 0 rgba(0,0,0,0.08); +} + +.table-scroll .table { + box-shadow: none; + border-radius: 0; +} + /* ---------- Role/status badges ---------------------------- */ @@ -1193,6 +1209,50 @@ hr { .admin-nav-btn:hover { background: rgba(255, 255, 255, 0.2); } +/* Burger button for the admin nav — only renders below the lg breakpoint + (≤1023.98px). On desktop it's display:none and stays out of the way so + the original navbar layout is byte-identical to pre-mobile-work. */ +.admin-nav__burger { + display: none; + background: none; + border: 0; + color: #ffffff; + cursor: pointer; + padding: 0.25rem; + margin-left: auto; +} + +.admin-nav__drawer { + background: #0C2866; + color: #ffffff; + padding: 0.5rem 2rem 1rem; +} + +.admin-nav__drawer a, +.admin-nav__drawer .nav-disabled { + display: block; + padding: 0.5rem 0; + color: #ffffff; +} + +.admin-nav__drawer a:hover { text-decoration: underline; } + +.admin-nav__drawer > div { padding-top: 0.5rem; } + +@media (max-width: 1023.98px) { + /* Hide every admin-nav child except the brand (first) and the burger so + the navy bar collapses to brand + burger. The drawer below the nav + reveals the full link list when toggled. */ + .admin-nav { padding: 0.875rem 1.25rem; gap: 0.75rem; } + .admin-nav > *:not(:first-child):not(.admin-nav__burger) { display: none; } + .admin-nav__burger { display: inline-flex; } +} + +@media (min-width: 1024px) { + /* Drawer is mobile-only; never reveal it on desktop even if toggled. */ + .admin-nav__drawer { display: none !important; } +} + /* ---------- Section heading helper ------------------------ */ @@ -1508,6 +1568,239 @@ hr { margin: 0; } +/* Admin: embassy application — interview view ------------------------- */ +/* Dense single-page layout. The attaché scans down a tight 3-column grid + (label / question / answer), so each Q&A occupies one or two lines. */ + +.qa-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + flex-wrap: wrap; + gap: 0.75rem; + margin-bottom: 0.875rem; +} + +.qa-header__title { + font-size: 1.25rem; + font-weight: 700; + color: #0C2866; + margin: 0; +} + +.qa-header__sub { + font-size: 0.8125rem; + color: #6b7280; + margin: 0.125rem 0 0; +} + +.qa-header__sub strong { color: #0C2866; } + +.qa-header__actions { + display: flex; + gap: 0.375rem; +} + +.qa-header__actions .btn { + padding: 0.375rem 0.75rem; + font-size: 0.8125rem; +} + +/* Compact info strip — notary + pool draw on one row */ +.qa-strip { + display: flex; + flex-wrap: wrap; + gap: 0; + background: #fffaf0; + border: 1px solid #e6d3a3; + border-left: 4px solid #c89b3c; + border-radius: 0.25rem; + margin-bottom: 1rem; + font-size: 0.8125rem; + line-height: 1.4; +} + +.qa-strip__cell { + flex: 1 1 18rem; + padding: 0.5rem 0.75rem; + display: flex; + align-items: baseline; + gap: 0.5rem; + min-width: 0; +} + +.qa-strip__label { + font-size: 0.6875rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + color: #8a6d2a; + white-space: nowrap; +} + +.qa-strip__value { + color: #0C2866; + min-width: 0; + word-wrap: break-word; +} + +/* Section block */ +.qa-section { + margin-bottom: 1.25rem; +} + +.qa-section__head { + display: flex; + align-items: baseline; + gap: 0.5rem; + flex-wrap: wrap; + margin: 0 0 0.25rem; + padding-bottom: 0.25rem; + border-bottom: 2px solid #0C2866; + font-family: "Playfair Display", Georgia, serif; +} + +.qa-section__num { + font-size: 0.75rem; + font-weight: 700; + color: #6b7280; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; +} + +.qa-section__title { + font-size: 1rem; + font-weight: 700; + color: #0C2866; + text-transform: uppercase; + letter-spacing: 0.06em; +} + +.qa-section__badge { + margin-left: auto; + display: inline-block; + padding: 0.1875rem 0.625rem; + background: #fbbf24; + color: #78350f; + border-radius: 0.25rem; + font-size: 0.6875rem; + font-weight: 800; + letter-spacing: 0.06em; + text-transform: uppercase; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +/* Section 3 — randomized pool gets a distinct background tint + and a thicker yellow accent so the eye lands on it instantly. */ +.qa-section--pool { + background: #fffbeb; + border: 1px solid #fde68a; + border-left: 4px solid #f59e0b; + border-radius: 0.375rem; + padding: 0.625rem 0.875rem 0.5rem; + margin: 1.25rem 0; +} + +.qa-section--pool .qa-section__head { + border-bottom-color: #f59e0b; +} + +/* One Q&A row: label · question · answer */ +.qa-row { + display: grid; + grid-template-columns: 2.25rem minmax(10rem, 17rem) 1fr; + gap: 0.5rem 0.75rem; + align-items: baseline; + padding: 0.4rem 0.25rem; + border-bottom: 1px solid #f1f3f5; + font-size: 0.875rem; + line-height: 1.4; +} + +.qa-row:last-child { border-bottom: none; } + +.qa-row__num { + font-size: 0.75rem; + font-weight: 700; + color: #ffffff; + background: #0C2866; + padding: 0.125rem 0.375rem; + border-radius: 0.25rem; + text-align: center; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + align-self: center; + justify-self: start; +} + +.qa-section--pool .qa-row__num { + background: #f59e0b; + color: #78350f; +} + +.qa-row__q { + font-size: 0.8125rem; + color: #4b5563; + font-weight: 500; +} + +.qa-row__a { + color: #111827; + font-weight: 500; + min-width: 0; + word-wrap: break-word; + overflow-wrap: anywhere; +} + +.qa-row__a p { + margin: 0; + font-size: 0.875rem; + line-height: 1.4; + color: #111827; +} + +.qa-row__a p + p { margin-top: 0.25rem; } + +.qa-blank { + font-style: italic; + color: #9ca3af; + font-size: 0.8125rem; +} + +.qa-pills { + display: inline-flex; + flex-wrap: wrap; + gap: 0.25rem; +} + +.qa-pill { + display: inline-block; + padding: 0.0625rem 0.5rem; + background: #e0e7ff; + color: #1e3a8a; + border-radius: 999px; + font-size: 0.8125rem; + font-weight: 500; + line-height: 1.4; +} + +.qa-affirm { + color: #047857; + font-weight: 700; +} + +.qa-decline { + color: #b91c1c; + font-weight: 700; +} + +/* On narrow screens the question column gets too cramped; stack instead. */ +@media (max-width: 640px) { + .qa-row { + grid-template-columns: 2.25rem 1fr; + } + .qa-row__q { grid-column: 2; } + .qa-row__a { grid-column: 2; } +} + /* Booking confirm actions --------------------------------------------- */ /* button_to wraps its button in a
, which is block-level by default. @@ -2134,7 +2427,8 @@ hr { display: flex; align-items: flex-start; justify-content: space-between; - gap: 1rem; + flex-wrap: wrap; + gap: 0.75rem 1rem; margin-bottom: 0.625rem; padding-bottom: 0.5rem; border-bottom: 1px solid #eef1f4; diff --git a/app/helpers/schedule_helper.rb b/app/helpers/schedule_helper.rb index 3317b59..7f40864 100644 --- a/app/helpers/schedule_helper.rb +++ b/app/helpers/schedule_helper.rb @@ -1,5 +1,5 @@ module ScheduleHelper - KIND_DISPLAY_OVERRIDES = { "lightning" => "Lightning Talks" }.freeze + KIND_DISPLAY_OVERRIDES = { "lightning" => "⚡ Lightning Talks" }.freeze def kind_display_label(kind) return nil if kind.blank? diff --git a/app/views/admin/embassy_applications/_question_row.html.erb b/app/views/admin/embassy_applications/_question_row.html.erb new file mode 100644 index 0000000..2b94468 --- /dev/null +++ b/app/views/admin/embassy_applications/_question_row.html.erb @@ -0,0 +1,32 @@ +<%# One row per question. Three columns: section label · question · answer. + Locals: application, question, section_label %> +<% answer = application.answer_for(question) %> +
+ <%= section_label %> +
<%= question.label %>
+
+ <% case question.field_type %> + <% when "checkbox_group" %> + <% values = Array(answer&.value_array) %> + <% if values.any? %> + + <% values.each do |v| %><%= v %><% end %> + + <% else %> + — none — + <% end %> + <% when "checkbox" %> + <% if answer&.value_text == "true" %> + ✓ Affirmed + <% else %> + ✗ Not affirmed + <% end %> + <% else %> + <% if answer&.value_text.present? %> + <%= simple_format(answer.value_text) %> + <% else %> + — blank — + <% end %> + <% end %> +
+
diff --git a/app/views/admin/embassy_applications/delivered.html.erb b/app/views/admin/embassy_applications/delivered.html.erb index eac8df8..116f1ad 100644 --- a/app/views/admin/embassy_applications/delivered.html.erb +++ b/app/views/admin/embassy_applications/delivered.html.erb @@ -1,6 +1,6 @@ <% content_for :title, "Delivered Passports — Embassy Admin" %> -
+

Delivered Passports

@@ -10,7 +10,7 @@

-<%= form_with url: delivered_admin_embassy_applications_path, method: :get, local: true, class: "mb-6 flex gap-2" do %> +<%= form_with url: delivered_admin_embassy_applications_path, method: :get, local: true, class: "mb-6 flex gap-2 flex-wrap" do %> <%= text_field_tag :q, @query, placeholder: "Search by name, email, or serial…", class: "input flex-1" %> @@ -30,6 +30,7 @@ <% end %>
<% else %> +
@@ -69,4 +70,5 @@ <% end %>
+
<% end %> diff --git a/app/views/admin/embassy_applications/index.html.erb b/app/views/admin/embassy_applications/index.html.erb index 67c4dd2..2ae68c2 100644 --- a/app/views/admin/embassy_applications/index.html.erb +++ b/app/views/admin/embassy_applications/index.html.erb @@ -1,6 +1,6 @@ <% content_for :title, "Submitted Applications — Embassy Admin" %> -
+

Submitted Applications

@@ -13,7 +13,7 @@ class: "btn btn-red" %>

-<%= form_with url: admin_embassy_applications_path, method: :get, local: true, class: "mb-6 flex gap-2" do %> +<%= form_with url: admin_embassy_applications_path, method: :get, local: true, class: "mb-6 flex gap-2 flex-wrap" do %> <%= text_field_tag :q, @query, placeholder: "Search by name, email, or serial…", class: "input flex-1" %> @@ -33,6 +33,7 @@ <% end %>
<% else %> +
@@ -72,4 +73,5 @@ <% end %>
+
<% end %> diff --git a/app/views/admin/embassy_applications/show.html.erb b/app/views/admin/embassy_applications/show.html.erb index 29e86f2..168c52f 100644 --- a/app/views/admin/embassy_applications/show.html.erb +++ b/app/views/admin/embassy_applications/show.html.erb @@ -1,70 +1,53 @@ <% content_for :title, "Application #{@application.serial} — Embassy Admin" %> -
+
-

+

Application <%= @application.serial %>

-

- <%= @booking.user.full_name %> · +

+ <%= @booking.user.full_name %> · <%= ScheduleItem::DAY_META.dig(@schedule_item.day, :date) %> · <%= @schedule_item.time_label %> · submitted <%= @application.submitted_at&.strftime("%b %-d · %-l:%M %p") %>

-
- <%= link_to "Download PDF", - admin_embassy_application_path(@application, format: :pdf), - class: "btn btn-red btn-lg" %> +
+ <%= link_to "Download PDF", admin_embassy_application_path(@application, format: :pdf), class: "btn btn-red" %> <%= link_to "← Back", admin_embassy_applications_path, class: "btn btn-muted" %>
-
- Random pool draw: - <%= @application.drawn_question_ids.join(", ") %> - (4 questions from the active Section 3 pool). - <% if @notary %> -
- Notary assignment: - <%= @notary.external_id %> · - “Someone who <%= @notary.description.sub(/^./, &:downcase) %>” - <% end %> -
- -
-

Submitted Answers

- <% [1, 2, 3, 4, 5].each do |n| %> -
-

- Section <%= n %> · <%= embassy_section_title(n) %> -

-
- <% @sections[n].each do |question| %> - <% answer = @application.answer_for(question) %> -
<%= question.external_id %> · <%= question.label %>
-
- <% case question.field_type %> - <% when "checkbox_group" %> - <% values = Array(answer&.value_array) %> - <% if values.any? %> -
    - <% values.each { |v| %>
  • <%= v %>
  • <% } %> -
- <% else %> - — None selected — - <% end %> - <% when "checkbox" %> - <%= answer&.value_text == "true" ? "✓ Affirmed" : "— Not affirmed —" %> - <% else %> - <% if answer&.value_text.present? %> - <%= simple_format(answer.value_text) %> - <% else %> - — Blank — - <% end %> - <% end %> -
+<% if @notary %> + <%# What notary the applicant must locate, plus the follow-up question they'll be asked. %> +
+
+ Notary + + <%= @notary.external_id %> · + Someone who <%= @notary.description.sub(/^./, &:downcase) %> + <% if @notary.followup_prompt.present? %> + · follow-up: “<%= @notary.followup_prompt %>” <% end %> -
+
- <% end %> -
+
+<% end %> + +<% [ 1, 2, 3, 4, 5 ].each do |n| %> +
"> +

+ § <%= n %> + <%= embassy_section_title(n) %> + <% if n == 3 %> + ★ Randomized question pool + <% end %> +

+ + <% @sections[n].each_with_index do |question, idx| %> + <%= render "question_row", + application: @application, + question: question, + section_label: "#{n}.#{idx + 1}" %> + <% end %> +
+<% end %> diff --git a/app/views/admin/embassy_questions/index.html.erb b/app/views/admin/embassy_questions/index.html.erb index fbb7c1c..a27d6a7 100644 --- a/app/views/admin/embassy_questions/index.html.erb +++ b/app/views/admin/embassy_questions/index.html.erb @@ -1,6 +1,6 @@ <% content_for :title, "Question Bank — Embassy Admin" %> -
+

Question Bank

@@ -40,6 +40,7 @@

+
@@ -57,6 +58,7 @@ <% end %>
+
<% end %> @@ -77,6 +79,7 @@
+
@@ -100,6 +103,7 @@ <% end %>
+
<% archived = @questions.select(&:archived?) %> @@ -113,6 +117,7 @@

+
@@ -130,5 +135,6 @@ <% end %>
+
<% end %> diff --git a/app/views/admin/schedule_items/index.html.erb b/app/views/admin/schedule_items/index.html.erb index 957b1bf..b9208f2 100644 --- a/app/views/admin/schedule_items/index.html.erb +++ b/app/views/admin/schedule_items/index.html.erb @@ -1,6 +1,6 @@ <% content_for :container_width, "max-w-6xl" %> -
+

Schedule Items

<%= link_to "Add item", new_admin_schedule_item_path, class: "btn btn-red" %>
@@ -13,6 +13,7 @@ <%= link_to "Clear filter", admin_schedule_items_path, class: "text-blue underline" %>

<% else %> +
@@ -61,4 +62,5 @@ <% end %>
+
<% end %> diff --git a/app/views/admin/users/index.html.erb b/app/views/admin/users/index.html.erb index 12342b8..1ede300 100644 --- a/app/views/admin/users/index.html.erb +++ b/app/views/admin/users/index.html.erb @@ -1,6 +1,6 @@ <% content_for :container_width, "max-w-6xl" %> -
+

Users

<%= button_to "Sync from Tito", sync_admin_users_path, method: :post, class: "btn btn-navy" %> @@ -8,6 +8,7 @@
+
@@ -49,3 +50,4 @@ <% end %>
+
diff --git a/app/views/admin/users/show.html.erb b/app/views/admin/users/show.html.erb index 8c02f4c..b4b223f 100644 --- a/app/views/admin/users/show.html.erb +++ b/app/views/admin/users/show.html.erb @@ -4,7 +4,7 @@ <%= link_to "\u2190 All users", admin_users_path, class: "text-blue underline" %>
-
+

<%= @user.full_name %>

<%= @user.email %>

diff --git a/app/views/embassy_applications/show.html.erb b/app/views/embassy_applications/show.html.erb index f62ab71..5f2ee45 100644 --- a/app/views/embassy_applications/show.html.erb +++ b/app/views/embassy_applications/show.html.erb @@ -27,7 +27,7 @@ embassy_application_path(@application, format: :pdf), class: "btn btn-red btn-lg" %>

- 2 pages · standard letter size · includes Notary Certification + 3 pages · standard letter size · includes Notary Certification

diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb index 974528d..566614f 100644 --- a/app/views/layouts/admin.html.erb +++ b/app/views/layouts/admin.html.erb @@ -19,21 +19,45 @@ - +
+ + + +
diff --git a/app/views/schedule_items/_form.html.erb b/app/views/schedule_items/_form.html.erb index 4d83ab8..a36419b 100644 --- a/app/views/schedule_items/_form.html.erb +++ b/app/views/schedule_items/_form.html.erb @@ -10,11 +10,23 @@
<%= form_with model: schedule_item, class: "space-y-4" do |f| %> - <%# Day is implied by the plan section this form was opened from — - ScheduleItemsController#new seeds @schedule_item.day from params[:day]. - We render it as a hidden field so admins-browsing-as-attendees still - submit the intended day without an extra dropdown. %> - <%= f.hidden_field :day %> + <%# Day is set one of two ways: + - When the form is opened from a /plan day section ("+ Add custom block"), + ScheduleItemsController#new seeds @schedule_item.day from params[:day] + and we keep the field hidden — the user already chose the day implicitly. + - When opened from /dashboard ("Host an Activity"), no day is implied, + so the caller passes show_day_picker: true and we render a select. %> + <% if local_assigns.fetch(:show_day_picker, false) %> +
+ <%= f.label :day, "Day", class: "label" %> + <%= f.select :day, + ScheduleItem::DAY_META.map { |key, meta| [ "#{meta[:label]} · #{meta[:date]}", key ] }, + {}, + class: "input" %> +
+ <% else %> + <%= f.hidden_field :day %> + <% end %>
diff --git a/app/views/schedule_items/edit.html.erb b/app/views/schedule_items/edit.html.erb index 74c5bd8..b296d98 100644 --- a/app/views/schedule_items/edit.html.erb +++ b/app/views/schedule_items/edit.html.erb @@ -6,6 +6,6 @@ Edit your activity - <%= render "form", schedule_item: @schedule_item %> + <%= render "form", schedule_item: @schedule_item, show_day_picker: false %>
diff --git a/app/views/schedule_items/new.html.erb b/app/views/schedule_items/new.html.erb index 5aa4920..c91a77d 100644 --- a/app/views/schedule_items/new.html.erb +++ b/app/views/schedule_items/new.html.erb @@ -14,7 +14,7 @@ on /plan this fragment is extracted into that frame (form expands inline). On direct visit, the surrounding page renders. %> <%= turbo_frame_tag "new_schedule_item_#{@schedule_item.day}" do %> - <%= render "form", schedule_item: @schedule_item %> + <%= render "form", schedule_item: @schedule_item, show_day_picker: params[:day].blank? %> <% end %>
diff --git a/app/views/shared/_navbar.html.erb b/app/views/shared/_navbar.html.erb index a2944d8..577da8f 100644 --- a/app/views/shared/_navbar.html.erb +++ b/app/views/shared/_navbar.html.erb @@ -10,8 +10,10 @@