collavre_plan 0.1.0
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 +7 -0
- data/Rakefile +2 -0
- data/app/components/collavre/plans_timeline_component.html.erb +14 -0
- data/app/components/collavre/plans_timeline_component.rb +53 -0
- data/app/controllers/collavre_plan/application_controller.rb +4 -0
- data/app/controllers/collavre_plan/creative_plans_controller.rb +69 -0
- data/app/controllers/collavre_plan/plans_controller.rb +170 -0
- data/app/javascript/collavre_plan.js +8 -0
- data/app/javascript/controllers/creatives/set_plan_modal_controller.js +124 -0
- data/app/javascript/controllers/index.js +6 -0
- data/app/javascript/modules/plans_menu.js +50 -0
- data/app/javascript/modules/plans_timeline.js +411 -0
- data/app/models/collavre/plan.rb +43 -0
- data/app/services/collavre/creatives/plan_tagger.rb +64 -0
- data/app/views/collavre/creatives/_set_plan_modal.html.erb +32 -0
- data/app/views/collavre/labels/_plan_extra.html.erb +14 -0
- data/app/views/collavre/labels/_plan_suffix.html.erb +3 -0
- data/app/views/collavre_plan/creatives/_set_plan_button.html.erb +1 -0
- data/app/views/collavre_plan/shared/navigation/_mobile_plans_button.html.erb +1 -0
- data/app/views/collavre_plan/shared/navigation/_panels.html.erb +3 -0
- data/app/views/collavre_plan/shared/navigation/_plans_button.html.erb +3 -0
- data/config/locales/plans.en.yml +16 -0
- data/config/locales/plans.ko.yml +23 -0
- data/config/routes.rb +4 -0
- data/lib/collavre_plan/engine.rb +87 -0
- data/lib/collavre_plan/version.rb +3 -0
- data/lib/collavre_plan.rb +5 -0
- metadata +96 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f1f4d20acde286797f3e083741480a5c654a0c93f9a674c09cedfc5bc7865f55
|
|
4
|
+
data.tar.gz: 81034f4bd8a3e0c6b36bd8d59786fbae8f5f0e141fcf34c29d88fce8feadf199
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6a366c4ef91d21b2caba0215f42ac7a1489b9b5ed251039da6baf12858b7f0de7e0cca533a3783d7d21f3839aa642811dc86e62a75522333f882a3bd1d30652f
|
|
7
|
+
data.tar.gz: e75dd6df51d244be43629f6ac461493810c71d61e6b4db42d8261d1dd574c71deb1b50b328cfc8ce5015c4ac2a10c0d7e55dde085f880d6a29cfb37e9ab1c9af
|
data/Rakefile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<div class="plans-timeline-wrapper">
|
|
2
|
+
<button id="timeline-today-btn" class="btn btn-xs" type="button"><%= t('collavre.plans.today', default: '오늘') %></button>
|
|
3
|
+
<div id="plans-timeline" class="horizontal-timeline" data-plans='<%= raw plan_data.to_json %>' data-start-date="<%= @start_date %>" data-end-date="<%= @end_date %>" data-delete-confirm="<%= t('collavre.plans.delete_confirm', default: 'Are you sure?') %>"></div>
|
|
4
|
+
</div>
|
|
5
|
+
<hr>
|
|
6
|
+
<div class="plan-form-container">
|
|
7
|
+
<%= form_with(model: Collavre::Plan.new, url: CollavrePlan::Engine.routes.url_helpers.plans_path, local: true, id: 'new-plan-form') do |form| %>
|
|
8
|
+
<%= form.hidden_field :creative_id, id: 'plan-creative-id' %>
|
|
9
|
+
<input type="text" id="plan-select-creative-input" placeholder="<%= t('collavre.plans.select_creative', default: 'Select Creative') %>" autocomplete="off">
|
|
10
|
+
<%= form.date_field :start_date, placeholder: t('collavre.plans.start_date'), id: 'plan-start-date' %>
|
|
11
|
+
<%= form.date_field :target_date, placeholder: t('collavre.plans.target_date'), id: 'plan-target-date' %>
|
|
12
|
+
<%= form.submit t('collavre.plans.add_plan'), id: 'add-plan-btn', class: 'btn btn-sm btn-primary', disabled: true %>
|
|
13
|
+
<% end %>
|
|
14
|
+
</div>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Collavre
|
|
2
|
+
class PlansTimelineComponent < ViewComponent::Base
|
|
3
|
+
# Accepts pre-filtered plans and calendar_events from the controller
|
|
4
|
+
def initialize(plans:, calendar_events: Collavre::CalendarEvent.none)
|
|
5
|
+
@start_date = Date.current - 30
|
|
6
|
+
@end_date = Date.current + 30
|
|
7
|
+
@plans = plans
|
|
8
|
+
@calendar_events = calendar_events
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :plans, :calendar_events, :start_date, :end_date
|
|
12
|
+
|
|
13
|
+
# Called after component enters render context - safe to use helpers here
|
|
14
|
+
def plan_data
|
|
15
|
+
@plan_data ||= @plans.map { |plan| plan_item(plan) } + @calendar_events.map { |event| calendar_item(event) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def plan_item(plan)
|
|
21
|
+
{
|
|
22
|
+
id: plan.id,
|
|
23
|
+
name: (plan.creative&.effective_description(nil, false) || plan.name.presence || I18n.l(plan.target_date)),
|
|
24
|
+
created_at: plan.created_at.to_date,
|
|
25
|
+
start_date: plan.start_date,
|
|
26
|
+
target_date: plan.target_date,
|
|
27
|
+
progress: plan.progress,
|
|
28
|
+
path: plan_creatives_path(plan),
|
|
29
|
+
deletable: plan.owner_id == Current.user&.id
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def calendar_item(event)
|
|
34
|
+
{
|
|
35
|
+
id: "calendar_event_#{event.id}",
|
|
36
|
+
name: event.summary.presence || I18n.l(event.start_time.to_date),
|
|
37
|
+
created_at: event.start_time.to_date,
|
|
38
|
+
target_date: event.end_time.to_date,
|
|
39
|
+
progress: event.creative&.progress || 0,
|
|
40
|
+
path: event.creative ? helpers.collavre.creative_path(event.creative) : event.html_link,
|
|
41
|
+
deletable: event.user_id == Current.user&.id
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def plan_creatives_path(plan)
|
|
46
|
+
if helpers.params[:id].present?
|
|
47
|
+
helpers.collavre.creative_path(helpers.params[:id], tags: [ plan.id ])
|
|
48
|
+
else
|
|
49
|
+
helpers.collavre.creatives_path(tags: [ plan.id ])
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
module CollavrePlan
|
|
2
|
+
class CreativePlansController < ApplicationController
|
|
3
|
+
before_action :require_authentication
|
|
4
|
+
|
|
5
|
+
def create
|
|
6
|
+
result = tagger.apply
|
|
7
|
+
respond_to do |format|
|
|
8
|
+
format.html do
|
|
9
|
+
flash[result.success? ? :notice : :alert] = translate_message(
|
|
10
|
+
result,
|
|
11
|
+
success_key: "collavre.creatives.index.plan_tags_applied",
|
|
12
|
+
success_default: "Plan tags applied to selected creatives.",
|
|
13
|
+
failure_key: "collavre.creatives.index.plan_tag_failed",
|
|
14
|
+
failure_default: "Please select a plan and at least one creative."
|
|
15
|
+
)
|
|
16
|
+
redirect_back fallback_location: Collavre::Engine.routes.url_helpers.creatives_path(select_mode: 1)
|
|
17
|
+
end
|
|
18
|
+
format.json do
|
|
19
|
+
if result.success?
|
|
20
|
+
render json: { message: translate_message(result, success_key: "collavre.creatives.index.plan_tags_applied", success_default: "Plan tags applied.", failure_key: "", failure_default: "") }, status: :ok
|
|
21
|
+
else
|
|
22
|
+
render json: { error: translate_message(result, success_key: "", success_default: "", failure_key: "collavre.creatives.index.plan_tag_failed", failure_default: "Failed to apply plan.") }, status: :unprocessable_entity
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def destroy
|
|
29
|
+
result = tagger.remove
|
|
30
|
+
respond_to do |format|
|
|
31
|
+
format.html do
|
|
32
|
+
flash[result.success? ? :notice : :alert] = translate_message(
|
|
33
|
+
result,
|
|
34
|
+
success_key: "collavre.creatives.index.plan_tags_removed",
|
|
35
|
+
success_default: "Plan tag removed from selected creatives.",
|
|
36
|
+
failure_key: "collavre.creatives.index.plan_tag_remove_failed",
|
|
37
|
+
failure_default: "Please select a plan and at least one creative."
|
|
38
|
+
)
|
|
39
|
+
redirect_back fallback_location: Collavre::Engine.routes.url_helpers.creatives_path(select_mode: 1)
|
|
40
|
+
end
|
|
41
|
+
format.json do
|
|
42
|
+
if result.success?
|
|
43
|
+
render json: { message: translate_message(result, success_key: "collavre.creatives.index.plan_tags_removed", success_default: "Plan tag removed.", failure_key: "", failure_default: "") }, status: :ok
|
|
44
|
+
else
|
|
45
|
+
render json: { error: translate_message(result, success_key: "", success_default: "", failure_key: "collavre.creatives.index.plan_tag_remove_failed", failure_default: "Failed to remove plan.") }, status: :unprocessable_entity
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def tagger
|
|
54
|
+
Collavre::Creatives::PlanTagger.new(plan_id: params[:plan_id], creative_ids: parsed_creative_ids, user: Current.user)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def parsed_creative_ids
|
|
58
|
+
params[:creative_ids].to_s.split(",").map(&:strip).reject(&:blank?)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def translate_message(result, success_key:, success_default:, failure_key:, failure_default:)
|
|
62
|
+
if result.success?
|
|
63
|
+
I18n.t(success_key, default: success_default)
|
|
64
|
+
else
|
|
65
|
+
I18n.t(failure_key, default: failure_default)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
module CollavrePlan
|
|
2
|
+
class PlansController < ApplicationController
|
|
3
|
+
def index
|
|
4
|
+
center = if params[:date].present?
|
|
5
|
+
Date.parse(params[:date]) rescue Date.current
|
|
6
|
+
else
|
|
7
|
+
Date.current
|
|
8
|
+
end
|
|
9
|
+
start_date = center - 30
|
|
10
|
+
end_date = center + 30
|
|
11
|
+
@plans = Collavre::Plan.joins(:creative)
|
|
12
|
+
.where("target_date >= ? AND DATE(creatives.created_at) <= ?", start_date, end_date)
|
|
13
|
+
.order(Arel.sql("DATE(creatives.created_at) ASC"))
|
|
14
|
+
.select { |plan| plan.readable_by?(Current.user) }
|
|
15
|
+
calendar_scope = Collavre::CalendarEvent.includes(:creative)
|
|
16
|
+
.where("DATE(start_time) <= ? AND DATE(end_time) >= ?", end_date, start_date)
|
|
17
|
+
.order(:start_time)
|
|
18
|
+
events_in_scope = calendar_scope.to_a
|
|
19
|
+
own_events = events_in_scope.select { |event| event.user_id == Current.user.id }
|
|
20
|
+
shared_events = events_in_scope.reject { |event| event.user_id == Current.user.id }
|
|
21
|
+
.select { |event| event.creative&.has_permission?(Current.user, :write) }
|
|
22
|
+
@calendar_events = (own_events + shared_events).uniq.sort_by(&:start_time)
|
|
23
|
+
respond_to do |format|
|
|
24
|
+
format.html do
|
|
25
|
+
render html: render_to_string(Collavre::PlansTimelineComponent.new(plans: @plans, calendar_events: @calendar_events), layout: false)
|
|
26
|
+
end
|
|
27
|
+
format.json do
|
|
28
|
+
plan_jsons = @plans.map { |p| plan_json(p) }
|
|
29
|
+
event_jsons = @calendar_events.map { |e| calendar_json(e) }
|
|
30
|
+
render json: plan_jsons + event_jsons
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def create
|
|
36
|
+
@plan = Collavre::Plan.new(plan_params)
|
|
37
|
+
@plan.owner = Current.user
|
|
38
|
+
if @plan.save
|
|
39
|
+
respond_to do |format|
|
|
40
|
+
format.html do
|
|
41
|
+
redirect_back fallback_location: main_app.root_path, notice: t("collavre.plans.created")
|
|
42
|
+
end
|
|
43
|
+
format.json do
|
|
44
|
+
render json: plan_json(@plan), status: :created
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
else
|
|
48
|
+
respond_to do |format|
|
|
49
|
+
format.html do
|
|
50
|
+
flash[:alert] = @plan.errors.full_messages.join(", ")
|
|
51
|
+
redirect_back fallback_location: main_app.root_path
|
|
52
|
+
end
|
|
53
|
+
format.json do
|
|
54
|
+
render json: { errors: @plan.errors.full_messages }, status: :unprocessable_entity
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def destroy
|
|
61
|
+
@plan = Collavre::Plan.find(params[:id])
|
|
62
|
+
return render_forbidden unless plan_editable_by_current_user?
|
|
63
|
+
|
|
64
|
+
@plan.destroy
|
|
65
|
+
respond_to do |format|
|
|
66
|
+
format.html do
|
|
67
|
+
redirect_back fallback_location: main_app.root_path,
|
|
68
|
+
notice: t("collavre.plans.deleted", default: "Plan deleted.")
|
|
69
|
+
end
|
|
70
|
+
format.json { head :no_content }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def update
|
|
75
|
+
@plan = Collavre::Plan.find(params[:id])
|
|
76
|
+
return render_forbidden unless plan_editable_by_current_user?
|
|
77
|
+
|
|
78
|
+
if @plan.update(plan_update_params)
|
|
79
|
+
respond_to do |format|
|
|
80
|
+
format.html do
|
|
81
|
+
redirect_back fallback_location: main_app.root_path,
|
|
82
|
+
notice: t("collavre.plans.updated", default: "Plan updated.")
|
|
83
|
+
end
|
|
84
|
+
format.json do
|
|
85
|
+
render json: plan_json(@plan, creative_id: params[:creative_id] || @plan.creative_id), status: :ok
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
else
|
|
89
|
+
respond_to do |format|
|
|
90
|
+
format.html do
|
|
91
|
+
flash[:alert] = @plan.errors.full_messages.join(", ")
|
|
92
|
+
redirect_back fallback_location: main_app.root_path
|
|
93
|
+
end
|
|
94
|
+
format.json do
|
|
95
|
+
render json: { errors: @plan.errors.full_messages }, status: :unprocessable_entity
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def plan_params
|
|
104
|
+
params.require(:plan).permit(:target_date, :start_date, :creative_id)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def plan_update_params
|
|
108
|
+
params.require(:plan).permit(:target_date, :start_date)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def plan_editable_by_current_user?
|
|
112
|
+
return true if @plan.owner_id == Current.user&.id
|
|
113
|
+
return true if @plan.creative&.has_permission?(Current.user, :write)
|
|
114
|
+
|
|
115
|
+
tagged_creative = Collavre::Creative.find_by(id: params[:creative_id])
|
|
116
|
+
return false unless tagged_creative
|
|
117
|
+
return false unless @plan.tags.exists?(creative_id: tagged_creative.id)
|
|
118
|
+
|
|
119
|
+
tagged_creative.has_permission?(Current.user, :write)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def render_forbidden
|
|
123
|
+
respond_to do |format|
|
|
124
|
+
format.html do
|
|
125
|
+
redirect_back fallback_location: main_app.root_path,
|
|
126
|
+
alert: t("collavre.plans.update_forbidden", default: "You do not have permission to update this plan.")
|
|
127
|
+
end
|
|
128
|
+
format.json do
|
|
129
|
+
render json: { error: "forbidden" }, status: :forbidden
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def plan_json(plan, creative_id: nil)
|
|
135
|
+
{
|
|
136
|
+
id: plan.id,
|
|
137
|
+
name: (plan.creative&.effective_description(nil, false) || plan.name.presence || I18n.l(plan.target_date)),
|
|
138
|
+
created_at: plan.created_at.to_date,
|
|
139
|
+
start_date: plan.start_date,
|
|
140
|
+
target_date: plan.target_date,
|
|
141
|
+
progress: plan.progress,
|
|
142
|
+
path: plan_creatives_path(plan, creative_id: creative_id),
|
|
143
|
+
deletable: plan.owner_id == Current.user&.id
|
|
144
|
+
}
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def calendar_json(event)
|
|
148
|
+
{
|
|
149
|
+
id: "calendar_event_#{event.id}",
|
|
150
|
+
name: event.summary.presence || I18n.l(event.start_time.to_date),
|
|
151
|
+
created_at: event.start_time.to_date,
|
|
152
|
+
target_date: event.end_time.to_date,
|
|
153
|
+
progress: event.creative&.progress || 0,
|
|
154
|
+
path: event.creative ? Collavre::Engine.routes.url_helpers.creative_path(event.creative) : event.html_link,
|
|
155
|
+
deletable: event.user_id == Current.user&.id
|
|
156
|
+
}
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def plan_creatives_path(plan, creative_id: nil)
|
|
160
|
+
collavre_routes = Collavre::Engine.routes.url_helpers
|
|
161
|
+
if creative_id.present?
|
|
162
|
+
collavre_routes.creative_path(creative_id, tags: [ plan.id ])
|
|
163
|
+
elsif params[:id].present?
|
|
164
|
+
collavre_routes.creative_path(params[:id], tags: [ plan.id ])
|
|
165
|
+
else
|
|
166
|
+
collavre_routes.creatives_path(tags: [ plan.id ])
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus'
|
|
2
|
+
|
|
3
|
+
// Self-contained controller: attaches to the modal wrapper element
|
|
4
|
+
// and listens for 'plan:open-modal' on document for cross-element communication.
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
static targets = ['modal', 'form', 'idsInput', 'planSelect']
|
|
7
|
+
static values = {
|
|
8
|
+
selectOne: String,
|
|
9
|
+
selectPlan: String,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
connect() {
|
|
13
|
+
this._openHandler = () => this._open()
|
|
14
|
+
document.addEventListener('plan:open-modal', this._openHandler)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
disconnect() {
|
|
18
|
+
document.removeEventListener('plan:open-modal', this._openHandler)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Called via document event from the set-plan button
|
|
22
|
+
_open() {
|
|
23
|
+
if (!this.hasModalTarget) return
|
|
24
|
+
this.modalTarget.style.display = 'flex'
|
|
25
|
+
document.body.classList.add('no-scroll')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Can still be called via data-action for elements inside the controller scope
|
|
29
|
+
open(event) {
|
|
30
|
+
event.preventDefault()
|
|
31
|
+
this._open()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
close(event) {
|
|
35
|
+
if (event) event.preventDefault()
|
|
36
|
+
if (!this.hasModalTarget) return
|
|
37
|
+
this.modalTarget.style.display = 'none'
|
|
38
|
+
document.body.classList.remove('no-scroll')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
backdrop(event) {
|
|
42
|
+
if (event.target === this.modalTarget) {
|
|
43
|
+
this.close(event)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async submit(event) {
|
|
48
|
+
event.preventDefault()
|
|
49
|
+
if (!this.hasFormTarget || !this.hasIdsInputTarget) return
|
|
50
|
+
const ids = this.selectedIds()
|
|
51
|
+
|
|
52
|
+
if (ids.length === 0) {
|
|
53
|
+
this.alert(this.selectOneValue)
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const planId = this.planSelectTarget.value
|
|
58
|
+
if (!planId) {
|
|
59
|
+
this.alert(this.selectPlanValue)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await this.performRequest(this.formTarget.action, 'POST', {
|
|
64
|
+
plan_id: planId,
|
|
65
|
+
creative_ids: ids.join(',')
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async remove(event) {
|
|
70
|
+
event.preventDefault()
|
|
71
|
+
const ids = this.selectedIds()
|
|
72
|
+
if (ids.length === 0) {
|
|
73
|
+
this.alert(this.selectOneValue)
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const planId = this.planSelectTarget.value
|
|
78
|
+
if (!planId) {
|
|
79
|
+
this.alert(this.selectPlanValue)
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
await this.performRequest(event.currentTarget.dataset.removePath, 'DELETE', {
|
|
84
|
+
plan_id: planId,
|
|
85
|
+
creative_ids: ids.join(',')
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async performRequest(url, method, body) {
|
|
90
|
+
try {
|
|
91
|
+
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content
|
|
92
|
+
const response = await fetch(url, {
|
|
93
|
+
method: method,
|
|
94
|
+
headers: {
|
|
95
|
+
'Content-Type': 'application/json',
|
|
96
|
+
'Accept': 'application/json',
|
|
97
|
+
'X-CSRF-Token': csrfToken
|
|
98
|
+
},
|
|
99
|
+
body: JSON.stringify(body)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
if (response.ok) {
|
|
103
|
+
const data = await response.json()
|
|
104
|
+
this.alert(data.message)
|
|
105
|
+
this.close()
|
|
106
|
+
} else {
|
|
107
|
+
const data = await response.json()
|
|
108
|
+
this.alert(data.error || 'Operation failed')
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error(error)
|
|
112
|
+
this.alert('An unexpected error occurred.')
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
selectedIds() {
|
|
117
|
+
return Array.from(document.querySelectorAll('.select-creative-checkbox:checked')).map((checkbox) => checkbox.value)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
alert(message) {
|
|
121
|
+
if (!message) return
|
|
122
|
+
window.alert(message)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Plans menu functionality
|
|
2
|
+
// Handles the plans menu button click and lazy-loads plans data
|
|
3
|
+
|
|
4
|
+
import { notifyPopupOpen, onOtherPopupOpen } from 'collavre/lib/gnb_popup_manager'
|
|
5
|
+
|
|
6
|
+
const POPUP_ID = 'plans-menu'
|
|
7
|
+
let initialized = false
|
|
8
|
+
|
|
9
|
+
function initPlansMenu() {
|
|
10
|
+
if (initialized) return
|
|
11
|
+
initialized = true
|
|
12
|
+
|
|
13
|
+
document.addEventListener('turbo:load', function() {
|
|
14
|
+
const btns = document.querySelectorAll('.plans-menu-btn')
|
|
15
|
+
const area = document.getElementById('plans-list-area')
|
|
16
|
+
let loaded = false
|
|
17
|
+
const timeline = document.getElementById('plans-timeline')
|
|
18
|
+
|
|
19
|
+
function closePlans() {
|
|
20
|
+
if (area) area.style.display = 'none'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
onOtherPopupOpen(POPUP_ID, closePlans)
|
|
24
|
+
|
|
25
|
+
btns.forEach(function(btn) {
|
|
26
|
+
btn.addEventListener('click', function() {
|
|
27
|
+
if (area.style.display === 'none') {
|
|
28
|
+
notifyPopupOpen(POPUP_ID)
|
|
29
|
+
area.style.display = 'block'
|
|
30
|
+
if (!loaded) {
|
|
31
|
+
const plansUrl = area.dataset.plansUrl || '/plans.json'
|
|
32
|
+
fetch(plansUrl)
|
|
33
|
+
.then(function(r) { return r.json() })
|
|
34
|
+
.then(function(plans) {
|
|
35
|
+
if (timeline) { timeline.dataset.plans = JSON.stringify(plans) }
|
|
36
|
+
if (window.initPlansTimeline && timeline) {
|
|
37
|
+
window.initPlansTimeline(timeline)
|
|
38
|
+
}
|
|
39
|
+
loaded = true
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
area.style.display = 'none'
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
initPlansMenu()
|