+ <%= form_tag(url_for, method: :get, style: "width: 100%;") do %>
+
+ <% end %>
+
diff --git a/backend/app/views/spree/admin/users/_form.html.erb b/backend/app/views/spree/admin/users/_form.html.erb
index 760d62a516c..d95d5b7041d 100644
--- a/backend/app/views/spree/admin/users/_form.html.erb
+++ b/backend/app/views/spree/admin/users/_form.html.erb
@@ -76,5 +76,14 @@
<% end %>
<% end %>
<% end %>
+
+ <% if can?(:update, @user) && @user.respond_to?(:timezone) %>
+ <%= f.field_container :timezone do %>
+ <%= f.label :timezone %>
+ <%= f.collection_select :timezone, ActiveSupport::TimeZone.all, :name, :to_s,
+ {include_blank: t("spree.none")},
+ class: "select2 fullwidth" %>
+ <% end %>
+ <% end %>
diff --git a/backend/spec/controllers/spree/admin/base_controller_spec.rb b/backend/spec/controllers/spree/admin/base_controller_spec.rb
index ee106119cab..8824ccfc3c7 100644
--- a/backend/spec/controllers/spree/admin/base_controller_spec.rb
+++ b/backend/spec/controllers/spree/admin/base_controller_spec.rb
@@ -38,4 +38,19 @@ def index
end
end
end
+
+ context "authorized request" do
+ stub_authorization!
+
+ it "allows access" do
+ get :index
+ expect(response.body).to eq("test")
+ end
+
+ it "sets timezone by param" do
+ get :index, params: {solidus_timezone: "Hawaii"}
+ expect(session).to have_key(:solidus_timezone)
+ expect(session[:solidus_timezone]).to eq("Hawaii")
+ end
+ end
end
diff --git a/backend/spec/controllers/spree/admin/stock_items_controller_spec.rb b/backend/spec/controllers/spree/admin/stock_items_controller_spec.rb
index f0336bf573f..774f90a8c56 100644
--- a/backend/spec/controllers/spree/admin/stock_items_controller_spec.rb
+++ b/backend/spec/controllers/spree/admin/stock_items_controller_spec.rb
@@ -13,7 +13,7 @@ module Admin
let(:stock_item) { variant.stock_items.first }
let!(:user) { create :user }
- before { expect(controller).to receive(:spree_current_user).and_return(user) }
+ before { expect(controller).to receive(:spree_current_user).twice.and_return(user) }
before { request.env["HTTP_REFERER"] = "product_admin_page" }
subject do
diff --git a/core/app/helpers/spree/core/controller_helpers/timezone.rb b/core/app/helpers/spree/core/controller_helpers/timezone.rb
new file mode 100644
index 00000000000..6ed6cb00b94
--- /dev/null
+++ b/core/app/helpers/spree/core/controller_helpers/timezone.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Spree
+ module Core
+ module ControllerHelpers
+ module Timezone
+ extend ActiveSupport::Concern
+
+ included do
+ around_action :set_timezone
+ end
+
+ private
+
+ # Sets the timezone for the current request.
+ #
+ # Uses the most preferred timezone or falls back to the server default.
+ #
+ # It respects the server's configured timezone from +config/application.rb+.
+ #
+ def set_timezone(&action)
+ timezone = if timezone_change_needed?
+ resolved_timezone || Time.zone.name
+ else
+ session[:solidus_timezone]
+ end
+ session[:solidus_timezone] = timezone
+ Time.use_zone(timezone, &action)
+ end
+
+ # Checks if we need to change the timezone or not.
+ def timezone_change_needed?
+ params[:solidus_timezone].present? || session[:solidus_timezone].blank?
+ end
+
+ # Returns the first valid timezone from the priority chain, or nil.
+ #
+ # The priority order is:
+ #
+ # * the passed parameter: +params[:solidus_timezone]+
+ # * the user's timezone preference
+ #
+ def resolved_timezone
+ candidates = [params[:solidus_timezone], timezone_from_user].compact
+ candidates.detect { |tz| ActiveSupport::TimeZone[tz].present? }
+ end
+
+ # Try to get the timezone from user settings.
+ def timezone_from_user
+ spree_current_user.try(:timezone).presence
+ end
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/permitted_attributes.rb b/core/lib/spree/permitted_attributes.rb
index 7a3f9c54aeb..0e8097064c5 100644
--- a/core/lib/spree/permitted_attributes.rb
+++ b/core/lib/spree/permitted_attributes.rb
@@ -138,7 +138,7 @@ module PermittedAttributes
# by changing a user with higher priveleges' email to one a lower-priveleged
# admin owns. Creating a user with an email is handled separate at the
# controller level.
- @@user_attributes = [:password, :password_confirmation, customer_metadata: {}]
+ @@user_attributes = [:password, :password_confirmation, :timezone, customer_metadata: {}]
@@variant_attributes = [
:name, :presentation, :cost_price, :lock_version,
diff --git a/core/spec/helpers/controller_helpers/timezone_spec.rb b/core/spec/helpers/controller_helpers/timezone_spec.rb
new file mode 100644
index 00000000000..70fa22dbc04
--- /dev/null
+++ b/core/spec/helpers/controller_helpers/timezone_spec.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+RSpec.describe Spree::Core::ControllerHelpers::Timezone, type: :controller do
+ controller(ActionController::Base) do
+ include Spree::Core::ControllerHelpers::Timezone
+
+ def index
+ render plain: Time.zone.name
+ end
+
+ private
+
+ attr_reader :spree_current_user
+ end
+
+ let(:original_timezone) { Time.zone.name }
+
+ describe "#set_timezone" do
+ context "with params[:solidus_timezone]" do
+ it "sets the timezone from the param" do
+ get :index, params: {solidus_timezone: "Hawaii"}
+ expect(response.body).to eq("Hawaii")
+ end
+
+ it "stores the timezone in the session" do
+ get :index, params: {solidus_timezone: "Hawaii"}
+ expect(session[:solidus_timezone]).to eq("Hawaii")
+ end
+
+ it "takes priority over session" do
+ get :index, params: {solidus_timezone: "Hawaii"}, session: {solidus_timezone: "Tokyo"}
+ expect(response.body).to eq("Hawaii")
+ end
+ end
+
+ context "with session[:solidus_timezone]" do
+ it "uses the timezone from the session" do
+ get :index, session: {solidus_timezone: "Tokyo"}
+ expect(response.body).to eq("Tokyo")
+ end
+ end
+
+ context "with spree_current_user timezone" do
+ let(:user) { double("User", timezone: "Berlin") }
+
+ before do
+ controller.instance_variable_set(:@spree_current_user, user)
+ end
+
+ it "uses the user's timezone" do
+ get :index
+ expect(response.body).to eq("Berlin")
+ end
+
+ context "when user does not respond to timezone" do
+ let(:user) { double("User") }
+
+ it "falls back to the server default" do
+ get :index
+ expect(response.body).to eq(original_timezone)
+ end
+ end
+
+ context "when user's timezone is blank" do
+ let(:user) { double("User", timezone: "") }
+
+ it "falls back to the server default" do
+ get :index
+ expect(response.body).to eq(original_timezone)
+ end
+ end
+ end
+
+ context "with an invalid timezone" do
+ it "falls back to the server default" do
+ get :index, params: {solidus_timezone: "Nonexistent/Zone"}
+ expect(response.body).to eq(original_timezone)
+ end
+ end
+
+ context "with no timezone set anywhere" do
+ it "uses the server default timezone" do
+ get :index
+ expect(response.body).to eq(original_timezone)
+ end
+
+ it "stores the server default in session" do
+ get :index
+ expect(session[:solidus_timezone]).to eq(original_timezone)
+ end
+ end
+
+ it "restores the original timezone after the request" do
+ original = Time.zone.name
+ get :index, params: {solidus_timezone: "Hawaii"}
+ expect(Time.zone.name).to eq(original)
+ end
+ end
+end