collavre_plan 0.1.1 → 0.1.2
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/app/assets/stylesheets/collavre_plan/timeline.css +183 -0
- data/app/components/collavre/plans_timeline_component.html.erb +8 -2
- data/app/components/collavre/plans_timeline_component.rb +37 -4
- data/app/controllers/collavre_plan/plans_controller.rb +99 -2
- data/app/javascript/controllers/creatives/set_plan_modal_controller.js +2 -1
- data/app/javascript/modules/plans_timeline.js +63 -5
- data/app/views/collavre_plan/shared/navigation/_panels.html.erb +1 -0
- data/config/locales/plans.en.yml +4 -0
- data/config/locales/plans.ko.yml +4 -0
- data/lib/collavre_plan/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e99d6a30bd4978b465c1bb84e5733c85884f40c9e824178a0157bfa31779c81b
|
|
4
|
+
data.tar.gz: 0c118b9a2083ecbd3edb475c8a1bfa255c7df380579cf91848c16f15b338b4ac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2549c3dac73f60dcf4ede3c9dc605e9ed20153b72195e8f41721eae74850d5d2b3d1ece7d28baf8ffeca69f99f744c5ae5ec4561a9250c2fbc0e47fad34a188f
|
|
7
|
+
data.tar.gz: 81560db00fbd1ccfa4b31f2be018a970fbbab935f9c36b06e8b2308fef24792913284fc61030ab4b0b3516607af463811bbd1d5112b27879d6e8aa83245f4ced
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Plans timeline panel styles.
|
|
3
|
+
*
|
|
4
|
+
* Owned by the collavre_plan engine (not the host application.css) so the
|
|
5
|
+
* engine ships its own CSS. Loaded via stylesheet_link_tag in the engine's
|
|
6
|
+
* navigation panel partial. Design tokens (--color-*, --surface-*, --ease-*)
|
|
7
|
+
* are provided globally by collavre/design_tokens.css.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
#plans-list-area {
|
|
11
|
+
max-width: var(--max-width);
|
|
12
|
+
width: min(100%, var(--max-width));
|
|
13
|
+
margin: 0.5em auto;
|
|
14
|
+
box-sizing: border-box;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#plans-list-area hr {
|
|
18
|
+
border: 0;
|
|
19
|
+
border-top: 1px solid var(--color-border);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.horizontal-timeline {
|
|
23
|
+
overflow-x: auto;
|
|
24
|
+
position: relative;
|
|
25
|
+
padding-bottom: 1em;
|
|
26
|
+
width: 100%;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.plans-timeline-wrapper {
|
|
30
|
+
position: relative;
|
|
31
|
+
width: 100%;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.plans-timeline-wrapper #timeline-today-btn {
|
|
35
|
+
position: absolute;
|
|
36
|
+
top: 0;
|
|
37
|
+
right: 0;
|
|
38
|
+
z-index: 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.horizontal-timeline .timeline-scroll {
|
|
42
|
+
position: relative;
|
|
43
|
+
white-space: nowrap;
|
|
44
|
+
height: 120px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.horizontal-timeline .timeline-day {
|
|
48
|
+
display: inline-block;
|
|
49
|
+
width: 80px;
|
|
50
|
+
box-sizing: border-box;
|
|
51
|
+
border-right: 1px solid var(--color-border);
|
|
52
|
+
text-align: center;
|
|
53
|
+
font-size: 0.8em;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.plan-form-container {
|
|
57
|
+
padding: 0.5em;
|
|
58
|
+
display: flex;
|
|
59
|
+
justify-content: flex-end;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.plan-form-container form {
|
|
63
|
+
display: flex;
|
|
64
|
+
flex-direction: column;
|
|
65
|
+
gap: var(--paragraph-space-1);
|
|
66
|
+
max-width: min(28rem, 100%);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.plan-bar {
|
|
70
|
+
position: absolute;
|
|
71
|
+
height: 20px;
|
|
72
|
+
background: var(--color-border);
|
|
73
|
+
border-radius: var(--radius-2);
|
|
74
|
+
overflow: hidden;
|
|
75
|
+
cursor: pointer;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.plan-bar .plan-progress {
|
|
79
|
+
background: var(--color-complete);
|
|
80
|
+
height: 100%;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.plan-bar .plan-label {
|
|
84
|
+
position: absolute;
|
|
85
|
+
left: 2px;
|
|
86
|
+
top: 2px;
|
|
87
|
+
font-size: 0.75em;
|
|
88
|
+
color: var(--color-text);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.plan-bar .delete-plan-btn {
|
|
92
|
+
position: absolute;
|
|
93
|
+
top: 2px;
|
|
94
|
+
right: 2px;
|
|
95
|
+
border: none;
|
|
96
|
+
background: none;
|
|
97
|
+
font-weight: bold;
|
|
98
|
+
cursor: pointer;
|
|
99
|
+
color: var(--color-bg);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* Registration markers: creatives drawn at their created_at (single day). */
|
|
103
|
+
.plan-bar--registration {
|
|
104
|
+
background: var(--color-link);
|
|
105
|
+
opacity: 0.85;
|
|
106
|
+
min-width: 6px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.plan-bar--registration .plan-progress {
|
|
110
|
+
background: transparent;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Modification markers: creatives drawn at their updated_at (single day).
|
|
114
|
+
Amber to read as distinct from the blue registration markers. */
|
|
115
|
+
.plan-bar--modification {
|
|
116
|
+
background: var(--color-warning);
|
|
117
|
+
opacity: 0.85;
|
|
118
|
+
min-width: 6px;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.plan-bar--modification .plan-progress {
|
|
122
|
+
background: transparent;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* Timeline chip filters (Registered, ...) — default-off toggles. */
|
|
126
|
+
.timeline-controls {
|
|
127
|
+
display: flex;
|
|
128
|
+
align-items: center;
|
|
129
|
+
gap: 0.5em;
|
|
130
|
+
margin-bottom: 0.25em;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.timeline-chips {
|
|
134
|
+
display: flex;
|
|
135
|
+
gap: 0.25em;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.timeline-chip {
|
|
139
|
+
/* Inactive chips sit on a neutral surface (not transparent) so the off-state
|
|
140
|
+
still reads as a toggle against the timeline background, without looking
|
|
141
|
+
active. */
|
|
142
|
+
border: 1px solid var(--color-border);
|
|
143
|
+
background: var(--surface-secondary);
|
|
144
|
+
color: var(--color-text);
|
|
145
|
+
border-radius: var(--radius-round);
|
|
146
|
+
padding: 0.1em 0.7em;
|
|
147
|
+
font-size: 0.75em;
|
|
148
|
+
line-height: 1.6;
|
|
149
|
+
cursor: pointer;
|
|
150
|
+
transition: background-color 0.15s var(--ease-2), color 0.15s var(--ease-2), border-color 0.15s var(--ease-2);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.timeline-chip:hover {
|
|
154
|
+
background: var(--surface-hover);
|
|
155
|
+
color: var(--color-text);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.timeline-chip:focus-visible {
|
|
159
|
+
outline: 2px solid var(--color-link);
|
|
160
|
+
outline-offset: 1px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.timeline-chip--active {
|
|
164
|
+
background: var(--color-link);
|
|
165
|
+
border-color: var(--color-link);
|
|
166
|
+
color: var(--color-bg);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* Keep the active chip filled on hover — don't fall back to the inactive surface. */
|
|
170
|
+
.timeline-chip--active:hover {
|
|
171
|
+
background: var(--color-link);
|
|
172
|
+
color: var(--color-bg);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* The Modified chip's active fill matches its amber timeline markers
|
|
176
|
+
(.plan-bar--modification) so the filter color maps to the dots it controls.
|
|
177
|
+
--text-on-badge is the codebase convention for text over --color-warning. */
|
|
178
|
+
#chip-modifications.timeline-chip--active,
|
|
179
|
+
#chip-modifications.timeline-chip--active:hover {
|
|
180
|
+
background: var(--color-warning);
|
|
181
|
+
border-color: var(--color-warning);
|
|
182
|
+
color: var(--text-on-badge);
|
|
183
|
+
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
<div class="plans-timeline-wrapper">
|
|
2
|
-
<
|
|
3
|
-
|
|
2
|
+
<div class="timeline-controls">
|
|
3
|
+
<button id="timeline-today-btn" class="btn btn-xs" type="button"><%= t('collavre.plans.today', default: '오늘') %></button>
|
|
4
|
+
<div class="timeline-chips">
|
|
5
|
+
<button type="button" id="chip-registrations" class="timeline-chip<%= ' timeline-chip--active' if show_registrations %>" data-chip="registrations" aria-pressed="<%= show_registrations %>"><%= t('collavre.plans.chip_registrations', default: 'Registered') %></button>
|
|
6
|
+
<button type="button" id="chip-modifications" class="timeline-chip<%= ' timeline-chip--active' if show_modifications %>" data-chip="modifications" aria-pressed="<%= show_modifications %>"><%= t('collavre.plans.chip_modifications', default: 'Modified') %></button>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
<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-registrations="<%= show_registrations %>" data-modifications="<%= show_modifications %>" data-delete-confirm="<%= t('collavre.plans.delete_confirm', default: 'Are you sure?') %>"></div>
|
|
4
10
|
</div>
|
|
5
11
|
<hr>
|
|
6
12
|
<div class="plan-form-container">
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
module Collavre
|
|
2
2
|
class PlansTimelineComponent < ViewComponent::Base
|
|
3
|
-
# Accepts pre-filtered plans and
|
|
4
|
-
def initialize(plans:, calendar_events: Collavre::CalendarEvent.none)
|
|
3
|
+
# Accepts pre-filtered plans, calendar_events, registrations and modifications from the controller
|
|
4
|
+
def initialize(plans:, calendar_events: Collavre::CalendarEvent.none, registrations: [], show_registrations: false, modifications: [], show_modifications: false)
|
|
5
5
|
@start_date = Date.current - 30
|
|
6
6
|
@end_date = Date.current + 30
|
|
7
7
|
@plans = plans
|
|
8
8
|
@calendar_events = calendar_events
|
|
9
|
+
@registrations = registrations
|
|
10
|
+
@show_registrations = show_registrations
|
|
11
|
+
@modifications = modifications
|
|
12
|
+
@show_modifications = show_modifications
|
|
9
13
|
end
|
|
10
14
|
|
|
11
|
-
attr_reader :plans, :calendar_events, :start_date, :end_date
|
|
15
|
+
attr_reader :plans, :calendar_events, :registrations, :start_date, :end_date, :show_registrations, :modifications, :show_modifications
|
|
12
16
|
|
|
13
17
|
# Called after component enters render context - safe to use helpers here
|
|
14
18
|
def plan_data
|
|
15
|
-
@plan_data ||= @plans.map { |plan| plan_item(plan) } +
|
|
19
|
+
@plan_data ||= @plans.map { |plan| plan_item(plan) } +
|
|
20
|
+
@calendar_events.map { |event| calendar_item(event) } +
|
|
21
|
+
@registrations.map { |creative| registration_item(creative) } +
|
|
22
|
+
@modifications.map { |creative| modification_item(creative) }
|
|
16
23
|
end
|
|
17
24
|
|
|
18
25
|
private
|
|
@@ -42,6 +49,32 @@ class PlansTimelineComponent < ViewComponent::Base
|
|
|
42
49
|
}
|
|
43
50
|
end
|
|
44
51
|
|
|
52
|
+
def registration_item(creative)
|
|
53
|
+
{
|
|
54
|
+
id: "registration_#{creative.id}",
|
|
55
|
+
type: "registration",
|
|
56
|
+
name: (creative.effective_description(nil, false).presence || I18n.t("collavre.plans.registration_fallback", id: creative.id)),
|
|
57
|
+
created_at: creative.created_at.to_date,
|
|
58
|
+
target_date: creative.created_at.to_date,
|
|
59
|
+
progress: creative.progress,
|
|
60
|
+
path: helpers.collavre.creative_path(creative),
|
|
61
|
+
deletable: false
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def modification_item(creative)
|
|
66
|
+
{
|
|
67
|
+
id: "modification_#{creative.id}",
|
|
68
|
+
type: "modification",
|
|
69
|
+
name: (creative.effective_description(nil, false).presence || I18n.t("collavre.plans.modification_fallback", id: creative.id)),
|
|
70
|
+
created_at: creative.updated_at.to_date,
|
|
71
|
+
target_date: creative.updated_at.to_date,
|
|
72
|
+
progress: creative.progress,
|
|
73
|
+
path: helpers.collavre.creative_path(creative),
|
|
74
|
+
deletable: false
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
45
78
|
def plan_creatives_path(plan)
|
|
46
79
|
if helpers.params[:id].present?
|
|
47
80
|
helpers.collavre.creative_path(helpers.params[:id], tags: [ plan.id ])
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
module CollavrePlan
|
|
2
2
|
class PlansController < ApplicationController
|
|
3
|
+
# Cap how many registration/modification markers we draw so a busy window
|
|
4
|
+
# never floods the timeline (or the JSON payload). Most recent within the
|
|
5
|
+
# window win.
|
|
6
|
+
REGISTRATION_LIMIT = 300
|
|
7
|
+
MODIFICATION_LIMIT = 300
|
|
8
|
+
|
|
3
9
|
def index
|
|
4
10
|
center = if params[:date].present?
|
|
5
11
|
Date.parse(params[:date]) rescue Date.current
|
|
@@ -20,14 +26,24 @@ module CollavrePlan
|
|
|
20
26
|
shared_events = events_in_scope.reject { |event| event.user_id == Current.user.id }
|
|
21
27
|
.select { |event| event.creative&.has_permission?(Current.user, :write) }
|
|
22
28
|
@calendar_events = (own_events + shared_events).uniq.sort_by(&:start_time)
|
|
29
|
+
|
|
30
|
+
# "Registered"/"Modified" chips are default-off, so their creatives are
|
|
31
|
+
# only gathered when the client explicitly asks (lazy, zero cost normally).
|
|
32
|
+
@show_registrations = ActiveModel::Type::Boolean.new.cast(params[:registrations]) == true
|
|
33
|
+
@registrations = @show_registrations ? registration_creatives(start_date, end_date) : []
|
|
34
|
+
@show_modifications = ActiveModel::Type::Boolean.new.cast(params[:modifications]) == true
|
|
35
|
+
@modifications = @show_modifications ? modification_creatives(start_date, end_date) : []
|
|
36
|
+
|
|
23
37
|
respond_to do |format|
|
|
24
38
|
format.html do
|
|
25
|
-
render html: render_to_string(Collavre::PlansTimelineComponent.new(plans: @plans, calendar_events: @calendar_events), layout: false)
|
|
39
|
+
render html: render_to_string(Collavre::PlansTimelineComponent.new(plans: @plans, calendar_events: @calendar_events, registrations: @registrations, show_registrations: @show_registrations, modifications: @modifications, show_modifications: @show_modifications), layout: false)
|
|
26
40
|
end
|
|
27
41
|
format.json do
|
|
28
42
|
plan_jsons = @plans.map { |p| plan_json(p) }
|
|
29
43
|
event_jsons = @calendar_events.map { |e| calendar_json(e) }
|
|
30
|
-
|
|
44
|
+
registration_jsons = @registrations.map { |c| registration_json(c) }
|
|
45
|
+
modification_jsons = @modifications.map { |c| modification_json(c) }
|
|
46
|
+
render json: plan_jsons + event_jsons + registration_jsons + modification_jsons
|
|
31
47
|
end
|
|
32
48
|
end
|
|
33
49
|
end
|
|
@@ -119,6 +135,87 @@ module CollavrePlan
|
|
|
119
135
|
tagged_creative.has_permission?(Current.user, :write)
|
|
120
136
|
end
|
|
121
137
|
|
|
138
|
+
# Registered creatives owned by the current user, drawn at their created_at.
|
|
139
|
+
# Owner-scoped (cheap, indexed) and capped — readable-but-shared creatives are
|
|
140
|
+
# intentionally a follow-up to avoid a per-creative permission fan-out here.
|
|
141
|
+
def registration_creatives(start_date, end_date)
|
|
142
|
+
# Range-compare in the user's zone (set_time_zone wraps the request in
|
|
143
|
+
# Time.use_zone) so day-edge creatives match the local created_at.to_date
|
|
144
|
+
# we render the marker at; also stays sargable (no DATE() cast on the column).
|
|
145
|
+
Collavre::Creative.active
|
|
146
|
+
.where(user_id: Current.user.id)
|
|
147
|
+
.where(created_at: start_date.beginning_of_day..end_date.end_of_day)
|
|
148
|
+
.where.not(id: plan_anchor_creative_ids)
|
|
149
|
+
.order(created_at: :desc)
|
|
150
|
+
.limit(REGISTRATION_LIMIT)
|
|
151
|
+
.to_a
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Plan#start_date= overwrites the anchor creative's created_at (via
|
|
155
|
+
# update_column), repurposing it as the plan start date — so these creatives
|
|
156
|
+
# already render as plan bars at that date and their created_at is not a true
|
|
157
|
+
# registration. Exclude them from the Registered chip to avoid a duplicate,
|
|
158
|
+
# mislabeled marker. (The Modified chip handles anchors via the immutable
|
|
159
|
+
# plan-label created_at instead; see modification_creatives.)
|
|
160
|
+
def plan_anchor_creative_ids
|
|
161
|
+
Collavre::Plan.where.not(creative_id: nil).select(:creative_id)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def registration_json(creative)
|
|
165
|
+
{
|
|
166
|
+
id: "registration_#{creative.id}",
|
|
167
|
+
type: "registration",
|
|
168
|
+
name: (creative.effective_description(nil, false).presence || I18n.t("collavre.plans.registration_fallback", id: creative.id)),
|
|
169
|
+
created_at: creative.created_at.to_date,
|
|
170
|
+
target_date: creative.created_at.to_date,
|
|
171
|
+
progress: creative.progress,
|
|
172
|
+
path: Collavre::Engine.routes.url_helpers.creative_path(creative),
|
|
173
|
+
deletable: false
|
|
174
|
+
}
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Modified creatives owned by the current user, drawn at their updated_at.
|
|
178
|
+
# We only include creatives genuinely edited after they came into being, so
|
|
179
|
+
# the "Modified" chip stays distinct from "Registered" rather than duplicating
|
|
180
|
+
# it. Owner-scoped and capped, mirroring registration_creatives.
|
|
181
|
+
#
|
|
182
|
+
# Plan#start_date= rewrites an anchor creative's created_at to the chosen plan
|
|
183
|
+
# start date (via update_column, leaving updated_at untouched), so for anchors
|
|
184
|
+
# created_at is the plan start, not a creation time, and updated_at > created_at
|
|
185
|
+
# can't signal a real edit (it's true for almost every anchor). The plan
|
|
186
|
+
# label's own created_at is immutable, so we measure "edited since setup"
|
|
187
|
+
# against the earliest plan's created_at for anchors; COALESCE falls back to
|
|
188
|
+
# created_at for ordinary creatives. This surfaces genuine post-setup edits to
|
|
189
|
+
# planned creatives while still suppressing the setup-only false positive.
|
|
190
|
+
def modification_creatives(start_date, end_date)
|
|
191
|
+
Collavre::Creative.active
|
|
192
|
+
.where(user_id: Current.user.id)
|
|
193
|
+
.where(updated_at: start_date.beginning_of_day..end_date.end_of_day)
|
|
194
|
+
.where(
|
|
195
|
+
"creatives.updated_at > COALESCE(" \
|
|
196
|
+
"(SELECT MIN(plan_labels.created_at) FROM labels plan_labels " \
|
|
197
|
+
"WHERE plan_labels.creative_id = creatives.id " \
|
|
198
|
+
"AND plan_labels.type = ?), creatives.created_at)",
|
|
199
|
+
Collavre::Plan.sti_name
|
|
200
|
+
)
|
|
201
|
+
.order(updated_at: :desc)
|
|
202
|
+
.limit(MODIFICATION_LIMIT)
|
|
203
|
+
.to_a
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def modification_json(creative)
|
|
207
|
+
{
|
|
208
|
+
id: "modification_#{creative.id}",
|
|
209
|
+
type: "modification",
|
|
210
|
+
name: (creative.effective_description(nil, false).presence || I18n.t("collavre.plans.modification_fallback", id: creative.id)),
|
|
211
|
+
created_at: creative.updated_at.to_date,
|
|
212
|
+
target_date: creative.updated_at.to_date,
|
|
213
|
+
progress: creative.progress,
|
|
214
|
+
path: Collavre::Engine.routes.url_helpers.creative_path(creative),
|
|
215
|
+
deletable: false
|
|
216
|
+
}
|
|
217
|
+
end
|
|
218
|
+
|
|
122
219
|
def plan_json(plan, creative_id: nil)
|
|
123
220
|
{
|
|
124
221
|
id: plan.id,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus'
|
|
2
|
+
import { alertDialog } from 'collavre/lib/utils/dialog'
|
|
2
3
|
|
|
3
4
|
// Self-contained controller: attaches to the modal wrapper element
|
|
4
5
|
// and listens for 'plan:open-modal' on document for cross-element communication.
|
|
@@ -119,6 +120,6 @@ export default class extends Controller {
|
|
|
119
120
|
|
|
120
121
|
alert(message) {
|
|
121
122
|
if (!message) return
|
|
122
|
-
|
|
123
|
+
alertDialog(message)
|
|
123
124
|
}
|
|
124
125
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { alertDialog, confirmDialog } from "collavre/lib/utils/dialog";
|
|
2
|
+
|
|
1
3
|
let plansTimelineScriptInitialized = false;
|
|
2
4
|
|
|
3
5
|
if (!plansTimelineScriptInitialized) {
|
|
@@ -24,6 +26,12 @@ if (!plansTimelineScriptInitialized) {
|
|
|
24
26
|
var endDate = new Date(container.dataset.endDate || new Date());
|
|
25
27
|
container.dataset.lastLoadedDate = new Date().toISOString().slice(0, 10);
|
|
26
28
|
|
|
29
|
+
// "Registered" chip: when on, the timeline also draws creatives at their
|
|
30
|
+
// created_at. Default-off, so registrations are fetched lazily on toggle.
|
|
31
|
+
var registrationsEnabled = container.dataset.registrations === 'true';
|
|
32
|
+
// "Modified" chip: same idea, drawing modified creatives at their updated_at.
|
|
33
|
+
var modificationsEnabled = container.dataset.modifications === 'true';
|
|
34
|
+
|
|
27
35
|
var scroll = document.createElement('div');
|
|
28
36
|
scroll.className = 'timeline-scroll';
|
|
29
37
|
container.appendChild(scroll);
|
|
@@ -65,6 +73,8 @@ if (!plansTimelineScriptInitialized) {
|
|
|
65
73
|
function createPlanBar(plan, idx) {
|
|
66
74
|
var el = document.createElement('div');
|
|
67
75
|
el.className = 'plan-bar';
|
|
76
|
+
if (plan.type === 'registration') el.className += ' plan-bar--registration';
|
|
77
|
+
else if (plan.type === 'modification') el.className += ' plan-bar--modification';
|
|
68
78
|
el.dataset.path = plan.path;
|
|
69
79
|
el.dataset.id = plan.id;
|
|
70
80
|
var startDateValue = plan.start_date || plan.created_at;
|
|
@@ -90,9 +100,9 @@ if (!plansTimelineScriptInitialized) {
|
|
|
90
100
|
del.textContent = '×';
|
|
91
101
|
del.className = 'delete-plan-btn';
|
|
92
102
|
el.appendChild(del);
|
|
93
|
-
del.addEventListener('click', function (e) {
|
|
103
|
+
del.addEventListener('click', async function (e) {
|
|
94
104
|
e.stopPropagation();
|
|
95
|
-
if (!
|
|
105
|
+
if (!(await confirmDialog(container.dataset.deleteConfirm, { danger: true }))) return;
|
|
96
106
|
var deleteUrl;
|
|
97
107
|
if (String(plan.id).indexOf('calendar_event_') === 0) {
|
|
98
108
|
deleteUrl = '/calendar_events/' + String(plan.id).replace('calendar_event_', '');
|
|
@@ -190,6 +200,7 @@ if (!plansTimelineScriptInitialized) {
|
|
|
190
200
|
renderPlans();
|
|
191
201
|
updatePlanPositions();
|
|
192
202
|
|
|
203
|
+
var loadSeq = 0;
|
|
193
204
|
function loadPlans(centerDate) {
|
|
194
205
|
var dateStr = centerDate.toISOString().slice(0, 10);
|
|
195
206
|
if (container.dataset.lastLoadedDate === dateStr) return;
|
|
@@ -197,9 +208,16 @@ if (!plansTimelineScriptInitialized) {
|
|
|
197
208
|
var listArea = document.getElementById('plans-list-area')
|
|
198
209
|
var basePlansUrl = (listArea && listArea.dataset.plansUrl) || '/plans.json'
|
|
199
210
|
var separator = basePlansUrl.indexOf('?') >= 0 ? '&' : '?'
|
|
200
|
-
|
|
211
|
+
var requestUrl = basePlansUrl + separator + 'date=' + dateStr
|
|
212
|
+
if (registrationsEnabled) requestUrl += '®istrations=1'
|
|
213
|
+
if (modificationsEnabled) requestUrl += '&modifications=1'
|
|
214
|
+
// Discard out-of-order responses: a slower registrations fetch must not
|
|
215
|
+
// overwrite the result of a later request (e.g. rapid chip on/off).
|
|
216
|
+
var seq = ++loadSeq;
|
|
217
|
+
fetch(requestUrl)
|
|
201
218
|
.then(function (r) { return r.json(); })
|
|
202
219
|
.then(function (newPlans) {
|
|
220
|
+
if (seq !== loadSeq) return;
|
|
203
221
|
plans = newPlans.map(function (p) {
|
|
204
222
|
if (p.start_date) {
|
|
205
223
|
p.start_date = new Date(p.start_date);
|
|
@@ -235,6 +253,38 @@ if (!plansTimelineScriptInitialized) {
|
|
|
235
253
|
todayBtn.addEventListener('click', function () { scrollToDate(new Date()); });
|
|
236
254
|
}
|
|
237
255
|
|
|
256
|
+
// Re-fetch the currently centered window, bypassing the lastLoadedDate guard
|
|
257
|
+
// (used when toggling a chip changes WHAT we request for the same date).
|
|
258
|
+
function reloadCurrentView() {
|
|
259
|
+
var centerOffset = container.scrollLeft + container.clientWidth / 2;
|
|
260
|
+
var daysFromStart = centerOffset / dayWidth;
|
|
261
|
+
var centerDate = new Date(startDate.getTime() + Math.round(daysFromStart) * 86400000);
|
|
262
|
+
container.dataset.lastLoadedDate = '';
|
|
263
|
+
loadPlans(centerDate);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
var registrationsChip = document.getElementById('chip-registrations');
|
|
267
|
+
if (registrationsChip) {
|
|
268
|
+
registrationsChip.addEventListener('click', function () {
|
|
269
|
+
registrationsEnabled = !registrationsEnabled;
|
|
270
|
+
container.dataset.registrations = registrationsEnabled ? 'true' : 'false';
|
|
271
|
+
registrationsChip.classList.toggle('timeline-chip--active', registrationsEnabled);
|
|
272
|
+
registrationsChip.setAttribute('aria-pressed', registrationsEnabled ? 'true' : 'false');
|
|
273
|
+
reloadCurrentView();
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
var modificationsChip = document.getElementById('chip-modifications');
|
|
278
|
+
if (modificationsChip) {
|
|
279
|
+
modificationsChip.addEventListener('click', function () {
|
|
280
|
+
modificationsEnabled = !modificationsEnabled;
|
|
281
|
+
container.dataset.modifications = modificationsEnabled ? 'true' : 'false';
|
|
282
|
+
modificationsChip.classList.toggle('timeline-chip--active', modificationsEnabled);
|
|
283
|
+
modificationsChip.setAttribute('aria-pressed', modificationsEnabled ? 'true' : 'false');
|
|
284
|
+
reloadCurrentView();
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
238
288
|
scrollToDate(new Date());
|
|
239
289
|
|
|
240
290
|
var scrollTimer;
|
|
@@ -257,7 +307,15 @@ if (!plansTimelineScriptInitialized) {
|
|
|
257
307
|
|
|
258
308
|
// Listen for plan creation from delegated handler
|
|
259
309
|
const onPlanCreated = function (e) {
|
|
260
|
-
|
|
310
|
+
// While a chip is active, the planned creative's derived
|
|
311
|
+
// registration/modification marker stops being eligible (plan anchors
|
|
312
|
+
// are excluded server-side), so re-fetch the authoritative view to drop
|
|
313
|
+
// the now-stale marker. Appending alone would leave it as a duplicate.
|
|
314
|
+
if (registrationsEnabled || modificationsEnabled) {
|
|
315
|
+
reloadCurrentView();
|
|
316
|
+
} else {
|
|
317
|
+
addPlan(e.detail);
|
|
318
|
+
}
|
|
261
319
|
};
|
|
262
320
|
document.addEventListener('plan:created', onPlanCreated);
|
|
263
321
|
|
|
@@ -398,7 +456,7 @@ if (!plansTimelineScriptInitialized) {
|
|
|
398
456
|
if (addPlanBtn) addPlanBtn.disabled = true;
|
|
399
457
|
}).catch(function (err) {
|
|
400
458
|
if (err && err.errors) {
|
|
401
|
-
|
|
459
|
+
alertDialog(err.errors.join(', '));
|
|
402
460
|
} else {
|
|
403
461
|
console.error(err);
|
|
404
462
|
}
|
data/config/locales/plans.en.yml
CHANGED
|
@@ -14,3 +14,7 @@ en:
|
|
|
14
14
|
delete_confirm: Are you sure you want to delete this plan?
|
|
15
15
|
today: Today
|
|
16
16
|
select_creative: Select Creative
|
|
17
|
+
chip_registrations: Registered
|
|
18
|
+
chip_modifications: Modified
|
|
19
|
+
registration_fallback: 'Creative #%{id}'
|
|
20
|
+
modification_fallback: 'Creative #%{id}'
|
data/config/locales/plans.ko.yml
CHANGED
|
@@ -14,6 +14,10 @@ ko:
|
|
|
14
14
|
delete_confirm: 이 계획을 삭제하시겠습니까?
|
|
15
15
|
today: 오늘
|
|
16
16
|
select_creative: 크리에이티브 선택
|
|
17
|
+
chip_registrations: 등록
|
|
18
|
+
chip_modifications: 수정
|
|
19
|
+
registration_fallback: '크리에이티브 #%{id}'
|
|
20
|
+
modification_fallback: '크리에이티브 #%{id}'
|
|
17
21
|
activerecord:
|
|
18
22
|
errors:
|
|
19
23
|
models:
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: collavre_plan
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Collavre
|
|
@@ -45,6 +45,7 @@ extensions: []
|
|
|
45
45
|
extra_rdoc_files: []
|
|
46
46
|
files:
|
|
47
47
|
- Rakefile
|
|
48
|
+
- app/assets/stylesheets/collavre_plan/timeline.css
|
|
48
49
|
- app/components/collavre/plans_timeline_component.html.erb
|
|
49
50
|
- app/components/collavre/plans_timeline_component.rb
|
|
50
51
|
- app/controllers/collavre_plan/application_controller.rb
|