diff --git a/backend/app/controllers/concerns/recordings/club_archive_actions.rb b/backend/app/controllers/concerns/recordings/club_archive_actions.rb index fddd316..2f52ca2 100644 --- a/backend/app/controllers/concerns/recordings/club_archive_actions.rb +++ b/backend/app/controllers/concerns/recordings/club_archive_actions.rb @@ -13,7 +13,12 @@ module Recordings end def update - permitted = params.require(:recording).permit(:privacy_status, :title) + if admin_expiry_update_requested? + apply_admin_expiry_update! + return + end + + permitted = recording_update_params attrs = {} privacy = permitted[:privacy_status] attrs[:privacy_status] = privacy if privacy.in?(Recording::PRIVACY_STATUSES) @@ -25,6 +30,8 @@ module Recordings redirect_to archive_index_path, notice: "Replay aggiornato" rescue ActiveRecord::RecordInvalid => e redirect_to archive_index_path, alert: e.record.errors.full_messages.join(", ") + rescue Recordings::ExtendExpiry::Error => e + redirect_to archive_index_path, alert: e.message end def destroy @@ -87,5 +94,30 @@ module Recordings def archive_namespace raise NotImplementedError end + + def recording_update_params + params.require(:recording).permit(:privacy_status, :title, :expires_at, :extend_days) + end + + def admin_expiry_update_requested? + return false unless archive_namespace == :admin + + permitted = recording_update_params + permitted[:extend_days].present? || permitted[:expires_at].present? + end + + def apply_admin_expiry_update! + permitted = recording_update_params + extender = Recordings::ExtendExpiry.new(@recording) + + if permitted[:extend_days].present? + extender.by_days(permitted[:extend_days]) + redirect_to archive_index_path, notice: "Scadenza replay prolungata di #{permitted[:extend_days].to_i} giorni" + else + expires_at = Time.zone.parse(permitted[:expires_at].to_s)&.end_of_day + extender.to_time(expires_at) + redirect_to archive_index_path, notice: "Scadenza replay aggiornata" + end + end end end diff --git a/backend/app/services/recordings/extend_expiry.rb b/backend/app/services/recordings/extend_expiry.rb new file mode 100644 index 0000000..30d61d4 --- /dev/null +++ b/backend/app/services/recordings/extend_expiry.rb @@ -0,0 +1,39 @@ +module Recordings + class ExtendExpiry + class Error < StandardError; end + + def initialize(recording) + @recording = recording + end + + def to_time(expires_at) + raise Error, "Data scadenza obbligatoria" if expires_at.blank? + + parsed = expires_at.is_a?(Time) || expires_at.is_a?(ActiveSupport::TimeWithZone) ? expires_at : Time.zone.parse(expires_at.to_s) + raise Error, "Data non valida" unless parsed + + apply(parsed) + end + + def by_days(days) + count = days.to_i + raise Error, "Giorni non validi" if count <= 0 + + base = [@recording.expires_at, Time.current].compact.max + apply(base + count.days) + end + + private + + def apply(new_expires_at) + raise Error, "Replay già eliminato" if @recording.deleted? + raise Error, "La scadenza deve essere nel futuro" unless new_expires_at > Time.current + + @recording.update!( + expires_at: new_expires_at, + expiry_warning_sent_at: nil + ) + @recording + end + end +end diff --git a/backend/app/views/recordings/_club_archive.html.erb b/backend/app/views/recordings/_club_archive.html.erb index 9e613fc..cab1b4f 100644 --- a/backend/app/views/recordings/_club_archive.html.erb +++ b/backend/app/views/recordings/_club_archive.html.erb @@ -112,6 +112,26 @@ Nessuna scadenza <% end %> + <% if local_assigns[:admin_mode] && !rec.deleted? && rec.expires_at.present? %> + <%= form_with url: paths.update.call(rec), method: :patch, local: true, class: "replay-archive__expiry-form" do %> + <% filter_params.each { |key, value| concat hidden_field_tag(key, value) } %> + <%= date_field_tag "recording[expires_at]", + rec.expires_at.to_date, + min: Date.tomorrow, + class: "replay-archive__expiry-input", + title: "Nuova data di scadenza" %> + <%= submit_tag "Salva", class: "replay-archive__expiry-save" %> + <% end %> +
+ <% [[30, "+30 gg"], [90, "+90 gg"], [180, "+180 gg"]].each do |days, label| %> + <%= button_to label, paths.update.call(rec), + method: :patch, + params: filter_params.merge(recording: { extend_days: days }), + class: "replay-archive__expiry-extend", + title: "Prolunga la scadenza di #{days} giorni" %> + <% end %> +
+ <% end %> <%= rec.status_label %> diff --git a/backend/public/marketing.css b/backend/public/marketing.css index 9612bee..a38edc9 100644 --- a/backend/public/marketing.css +++ b/backend/public/marketing.css @@ -1593,6 +1593,55 @@ a.replay-archive__thumb:hover { margin: 0; } +.replay-archive__expiry-form { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; + margin-top: 8px; +} + +.replay-archive__expiry-input { + font-size: 0.8rem; + padding: 4px 6px; + border-radius: 4px; + border: 1px solid #444; + background: #1a1a1a; + color: #eee; +} + +.replay-archive__expiry-save { + font-size: 0.75rem; + padding: 4px 8px; + border-radius: 4px; + border: 1px solid #555; + background: #2a2a2a; + color: #ddd; + cursor: pointer; +} + +.replay-archive__expiry-quick { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-top: 6px; +} + +.replay-archive__expiry-extend { + font-size: 0.7rem; + padding: 2px 6px; + border-radius: 4px; + border: 1px solid #444; + background: #222; + color: #aaa; + cursor: pointer; +} + +.replay-archive__expiry-extend:hover { + color: #fff; + border-color: #666; +} + .visually-hidden { position: absolute; width: 1px; diff --git a/backend/spec/requests/admin/club_recordings_spec.rb b/backend/spec/requests/admin/club_recordings_spec.rb index 4865121..cd9b18e 100644 --- a/backend/spec/requests/admin/club_recordings_spec.rb +++ b/backend/spec/requests/admin/club_recordings_spec.rb @@ -47,4 +47,29 @@ RSpec.describe "Admin club replays", type: :request do expect(recording.reload.status).to eq("expired") expect(recording.deleted_at).to be_present end + + it "prolunga la scadenza con giorni aggiuntivi" do + original = recording.expires_at + recording.update!(expires_at: 1.hour.ago, expiry_warning_sent_at: Time.current) + + patch admin_club_recording_path(club, recording), params: { recording: { extend_days: 30 } } + + expect(response).to redirect_to(admin_club_recordings_path(club)) + recording.reload + expect(recording.expires_at).to be > Time.current + expect(recording.expires_at).to be_within(2.seconds).of(30.days.from_now) + expect(recording.expiry_warning_sent_at).to be_nil + expect(Recording.expired_pending_purge).not_to include(recording) + end + + it "imposta una nuova data di scadenza" do + new_date = 60.days.from_now.to_date + + patch admin_club_recording_path(club, recording), params: { recording: { expires_at: new_date } } + + expect(response).to redirect_to(admin_club_recordings_path(club)) + recording.reload + expect(recording.expires_at.to_date).to eq(new_date) + expect(recording.expiry_warning_sent_at).to be_nil + end end diff --git a/backend/spec/services/recordings/extend_expiry_spec.rb b/backend/spec/services/recordings/extend_expiry_spec.rb new file mode 100644 index 0000000..7c5e6e6 --- /dev/null +++ b/backend/spec/services/recordings/extend_expiry_spec.rb @@ -0,0 +1,64 @@ +require "rails_helper" + +RSpec.describe Recordings::ExtendExpiry do + let!(:club) { Club.create!(name: "Club", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff") } + let!(:team) { club.teams.create!(name: "Tigers", sport: "volleyball") } + let!(:user) { User.create!(email: "coach@test.com", name: "Coach", password: "password123", role: "coach") } + let!(:match) { team.matches.create!(opponent_name: "Rival", scheduled_at: 1.day.ago) } + let!(:session) do + StreamSession.create!(match: match, user: user, platform: "matchlivetv", status: "ended", ended_at: 1.hour.ago) + end + let!(:recording) do + Recording.create!( + stream_session: session, team: team, status: "ready", privacy_status: "public", + storage_key: "teams/#{team.id}/sessions/#{session.id}/replay.mp4", + expires_at: 2.days.from_now, + expiry_warning_sent_at: 1.day.ago + ) + end + + it "imposta una nuova data di scadenza e resetta l'avviso" do + new_expiry = 45.days.from_now + + described_class.new(recording).to_time(new_expiry) + + recording.reload + expect(recording.expires_at).to be_within(1.second).of(new_expiry) + expect(recording.expiry_warning_sent_at).to be_nil + end + + it "prolunga dalla scadenza corrente" do + original = recording.expires_at + + described_class.new(recording).by_days(30) + + recording.reload + expect(recording.expires_at).to be_within(1.second).of(original + 30.days) + expect(recording.expiry_warning_sent_at).to be_nil + end + + it "prolunga da oggi se la scadenza è già passata" do + recording.update!(expires_at: 2.days.ago) + + described_class.new(recording).by_days(10) + + recording.reload + expect(recording.expires_at).to be_within(1.second).of(10.days.from_now) + end + + it "esclude il replay dalla coda di purge dopo il prolungamento" do + recording.update!(expires_at: 1.hour.ago) + + expect(Recording.expired_pending_purge).to include(recording) + + described_class.new(recording).by_days(30) + + expect(Recording.expired_pending_purge).not_to include(recording.reload) + end + + it "rifiuta date nel passato" do + expect do + described_class.new(recording).to_time(1.day.ago) + end.to raise_error(Recordings::ExtendExpiry::Error, /futuro/) + end +end