-
Notifications
You must be signed in to change notification settings - Fork 31
/
planner.rb
executable file
·465 lines (399 loc) · 15.2 KB
/
planner.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
#!/usr/bin/env ruby
require_relative './shared'
FILE_NAME = "time_block_pages.pdf"
# From https://stackoverflow.com/a/24753003/203673
#
# Calculates the number of business days in range (start_date, end_date]
#
# @param start_date [Date]
# @param end_date [Date]
#
# @return [Fixnum]
def business_days_between(start_date, end_date)
days_between = (end_date - start_date).to_i
return 0 unless days_between > 0
# Assuming we need to calculate days from 9th to 25th, 10-23 are covered
# by whole weeks, and 24-25 are extra days.
#
# Su Mo Tu We Th Fr Sa # Su Mo Tu We Th Fr Sa
# 1 2 3 4 5 # 1 2 3 4 5
# 6 7 8 9 10 11 12 # 6 7 8 9 ww ww ww
# 13 14 15 16 17 18 19 # ww ww ww ww ww ww ww
# 20 21 22 23 24 25 26 # ww ww ww ww ed ed 26
# 27 28 29 30 31 # 27 28 29 30 31
whole_weeks, extra_days = days_between.divmod(7)
unless extra_days.zero?
# Extra days start from the week day next to start_day,
# and end on end_date's week date. The position of the
# start date in a week can be either before (the left calendar)
# or after (the right one) the end date.
#
# Su Mo Tu We Th Fr Sa # Su Mo Tu We Th Fr Sa
# 1 2 3 4 5 # 1 2 3 4 5
# 6 7 8 9 10 11 12 # 6 7 8 9 10 11 12
# ## ## ## ## 17 18 19 # 13 14 15 16 ## ## ##
# 20 21 22 23 24 25 26 # ## 21 22 23 24 25 26
# 27 28 29 30 31 # 27 28 29 30 31
#
# If some of the extra_days fall on a weekend, they need to be subtracted.
# In the first case only corner days can be days off,
# and in the second case there are indeed two such days.
tomorrow = start_date.next_day(1)
extra_days -= if tomorrow.wday <= end_date.wday
[tomorrow.sunday?, end_date.saturday?].count(true)
else
2
end
end
(whole_weeks * 5) + extra_days
end
def business_days_left_in_year(date)
days = business_days_between(date, Date.new(date.year, 12, 31))
I18n.t('business_days_in_year', count: days)
end
def business_days_left_in_sprint(date)
# Use this if you have sprints that start on the 1st and 15th.
#sprint_end = Date.new(date.year, date.month, date.mday <= 15 ? 15 : -1)
# Use this if you have two week sprints from a given day.
sprint_start = SPRINT_EPOCH.step(date, SPRINT_LENGTH).to_a.last
sprint_end = sprint_start.next_day(SPRINT_LENGTH - 1)
days = business_days_between(date, sprint_end)
I18n.t('days_left_in_sprint', count: days)
end
def quarter(date)
QUARTERS_BY_MONTH[date.month]
end
# pick summer or winter semester depending on the month
def semester_year(date)
if date.month >= SUMMER_SEMESTER_START && date.month < WINTER_SEMESTER_START
I18n.l(date, format: :year)
else
"#{I18n.l(date, format: :year)} / #{I18n.l(date.next_year, format: :year)}"
end
end
# * * *
def quarter_ahead pdf, first_day, last_day
heading_left = I18n.t('quarter_plan_heading')
subheading_left = date_range(first_day, last_day)
heading_right = I18n.t('quarter', number: quarter(first_day))
subheading_right = I18n.l(last_day, format: :year)
# We let the caller start our page for us but we'll do both sides
hole_punches pdf
notes_page pdf, heading_left, subheading_left, heading_right, subheading_right
begin_new_page pdf, :left
notes_page pdf, heading_left, subheading_left, heading_right, subheading_right
begin_new_page pdf, :right
end
def week_ahead_page pdf, first_day, last_day
heading_left = I18n.t('week_plan_heading')
subheading_left = date_range(first_day, last_day)
heading_right = first_day.strftime("#{I18n.t('week')} %W")
subheading_right = I18n.t('quarter', number: quarter(first_day))
# We don't start our own page since we don't know if this is the first week or one
# of several weeks in a file.
hole_punches pdf
notes_page pdf, heading_left, subheading_left, heading_right, subheading_right
end
# Caller needs to start the page, so this could be the first page.
def notes_page pdf, heading_left, subheading_left = nil, heading_right = nil, subheading_right = nil
header_row_count = 2
body_row_count = HOUR_COUNT * 2
first_column = 0
last_column = COLUMN_COUNT - 1
first_row = header_row_count
last_row = header_row_count + body_row_count - 1
pdf.define_grid(columns: COLUMN_COUNT, rows: header_row_count + body_row_count, gutter: 0)
# pdf.grid.show_all
# Header Left
if heading_left
pdf.grid([0, first_column],[0, last_column]).bounding_box do
pdf.text heading_left, heading_format(align: :left)
end
end
if subheading_left
pdf.grid([1, first_column],[1, last_column]).bounding_box do
pdf.text subheading_left, subheading_format(align: :left)
end
end
# Header Right
if heading_right
pdf.grid([0, 3],[0, last_column]).bounding_box do
pdf.text heading_right, heading_format(align: :right)
end
end
if subheading_right
pdf.grid([1, 3],[1, last_column]).bounding_box do
pdf.text subheading_right, subheading_format(align: :right)
end
end
# Horizontal lines
(first_row..last_row).each do |row|
pdf.grid([row, first_column], [row, last_column]).bounding_box do
pdf.stroke_line pdf.bounds.bottom_left, pdf.bounds.bottom_right
end
end
# Checkboxes
((first_row + 1)..last_row).each do |row|
pdf.grid(row, 0).bounding_box do
draw_checkbox pdf
end
end
end
def daily_tasks_page pdf, date, metrics_rows = 5
begin_new_page pdf, :left
header_row_count = 2
body_row_count = HOUR_COUNT * 2
last_row = header_row_count + body_row_count - 1
pdf.define_grid(columns: COLUMN_COUNT, rows: header_row_count + body_row_count, gutter: 0)
# pdf.grid.show_all
# Header
left_header = I18n.l(date, format: :medium)
right_header = I18n.l(date, format: :weekday)
pdf.grid([0, 0],[1, 2]).bounding_box do
pdf.text left_header, heading_format(align: :left)
end
pdf.grid([0, 2],[1, 3]).bounding_box do
pdf.text right_header, heading_format(align: :right)
end
# Daily metrics
if metrics_rows > 0
pdf.grid([1, 0], [metrics_rows, 3]).bounding_box do
pdf.dash [1, 2]
pdf.stroke_bounds
pdf.undash
pdf.translate 6, -6 do
pdf.text I18n.t('daily_metrics'), color: MEDIUM_COLOR
end
end
pdf.grid([metrics_rows, 2], [metrics_rows, 3]).bounding_box do
draw_checkbox pdf, 6, I18n.t('shutdown_complete')
end
end
# Tasks / Notes
task_note_start = metrics_rows + 1
pdf.grid([task_note_start, 0], [task_note_start, 1]).bounding_box do
pdf.translate 6, 0 do
pdf.text I18n.t('tasks'), color: DARK_COLOR, valign: :center
end
end
pdf.grid([task_note_start, 2], [task_note_start, 3]).bounding_box do
pdf.translate 6, 0 do
pdf.text I18n.t('notes'), color: DARK_COLOR, valign: :center
end
end
# Horizontal lines
(task_note_start..last_row).each do |row|
pdf.grid([row, 0], [row, 3]).bounding_box do
pdf.stroke_line pdf.bounds.bottom_left, pdf.bounds.bottom_right
end
end
# Vertical line
pdf.grid([task_note_start + 1, 1], [last_row, 1]).bounding_box do
pdf.dash [1, 2], phase: 2
pdf.stroke_line(pdf.bounds.top_right, pdf.bounds.bottom_right)
pdf.undash
end
# Checkboxes
checkbox_padding = 6
((task_note_start + 1)..last_row).each_with_index do |row, index|
# Make the box wider than needed to avoid wrapping if the task name is too long
pdf.grid([row, 0], [row, 4]).bounding_box do
draw_checkbox pdf, checkbox_padding, TASKS_BY_WDAY[date.wday][index]
end
end
end
def daily_calendar_page pdf, date
begin_new_page pdf, :right
header_row_count = 2
body_row_count = HOUR_COUNT * 2
first_column = 0
last_column = COLUMN_COUNT - 1
fist_hour_row = header_row_count
last_hour_row = header_row_count + body_row_count - 1
pdf.define_grid(columns: COLUMN_COUNT, rows: header_row_count + body_row_count, gutter: 0)
# Header
left_header = I18n.l(date, format: :medium)
right_header = I18n.l(date, format: :weekday)
left_subhed = date.strftime("#{I18n.t('quarter', number: quarter(date))} #{I18n.t('week')} %W #{I18n.t('day')} %j")
# right_subhed = business_days_left_in_year(date)
right_subhed = business_days_left_in_sprint(date)
pdf.grid([0, first_column],[1, 1]).bounding_box do
pdf.text left_header, heading_format(align: :left)
end
pdf.grid([0, 2],[0, last_column]).bounding_box do
pdf.text right_header, heading_format(align: :right)
end
pdf.grid([1, first_column],[1, last_column]).bounding_box do
pdf.text left_subhed, subheading_format(align: :left)
end
pdf.grid([1, first_column],[1, last_column]).bounding_box do
pdf.text right_subhed, subheading_format(align: :right)
end
(0...HOUR_COUNT).each do |hour|
row = hour * 2 + fist_hour_row
# Hour labels
if hour_label = HOUR_LABELS[hour]
pdf.grid(row, -1).bounding_box do
pdf.translate(-4, 0) { pdf.text hour_label.to_s, align: :right, valign: :center }
end
end
# Default appointments
if appointment_label = APPOINTMENTS_BY_WDAY[date.wday][HOUR_LABELS[hour]]
pdf.grid([row, first_column], [row, last_column]).bounding_box do
pdf.translate(4, 0) do
pdf.text appointment_label.to_s, color: MEDIUM_COLOR, align: :left, valign: :center
end
end
end
end
# Horizontal lines
## Top line
pdf.stroke_color MEDIUM_COLOR
overhang = 24
pdf.grid([fist_hour_row, first_column], [fist_hour_row, last_column]).bounding_box do
pdf.stroke_line([pdf.bounds.top_left[0] - overhang, pdf.bounds.top_left[1]], pdf.bounds.top_right)
end
(fist_hour_row..last_hour_row).step(2) do |row|
## Half hour lines
pdf.dash [1, 2], phase: 2
pdf.grid([row, first_column], [row, last_column]).bounding_box do
pdf.stroke_line([pdf.bounds.bottom_left[0] - overhang, pdf.bounds.bottom_left[1]], pdf.bounds.bottom_right)
end
pdf.undash
## Hour lines
pdf.grid([row + 1, first_column], [row + 1, last_column]).bounding_box do
pdf.stroke_line([pdf.bounds.bottom_left[0] - overhang, pdf.bounds.bottom_left[1]], pdf.bounds.bottom_right)
end
end
# Vertical lines
(0..COLUMN_COUNT).each do |col|
pdf.grid([header_row_count, col], [last_hour_row, col]).bounding_box do
pdf.dash [1, 2], phase: 2
pdf.stroke_line(pdf.bounds.top_left, pdf.bounds.bottom_left)
pdf.undash
end
end
end
def weekend_page pdf, saturday, sunday
begin_new_page pdf, :left
header_row_count = 2
hour_row_count = HOUR_COUNT
# TODO should have one constant for grid's number of rows to use here.
# instead we'll just assume it's always 2x hours. We print a row per hour
# and one blank line as a divider.
task_row_count = 2 * HOUR_COUNT - hour_row_count - 1
body_row_count = header_row_count + task_row_count + hour_row_count
# Use a grid to do the math to divide the page into two columns:
pdf.define_grid(columns: 2, rows: 1, column_gutter: 24, row_gutter: 0)
first = pdf.grid(0,0)
second = pdf.grid(0,1)
# Then use that to build a bounding box for each column and redefine the grid in there.
work_areas = [
[saturday, first.top_left, { width: first.width, height: first.height }],
[sunday, second.top_left, { width: second.width, height: second.height }]
].each do |date, point, options|
pdf.bounding_box(point, options) do
pdf.define_grid(columns: 2, rows: body_row_count, gutter: 0)
# pdf.grid.show_all
# Header
left_header = I18n.l(date, format: :weekday)
left_sub_header = I18n.l(date, format: :medium)
pdf.grid([0, 0],[0, 1]).bounding_box do
pdf.text left_header, heading_format(align: :left)
end
pdf.grid([1, 0],[1, 1]).bounding_box do
pdf.text left_sub_header, subheading_format(align: :left)
end
task_start_row = header_row_count
task_last_row = task_start_row + task_row_count - 1
# Task lable
pdf.grid([task_start_row, 0], [task_start_row, 1]).bounding_box do
pdf.translate 6, 0 do
pdf.text I18n.t('tasks'), color: DARK_COLOR, valign: :center
end
end
# Horizontal lines
(task_start_row..task_last_row).each do |row|
pdf.grid([row, 0], [row, 1]).bounding_box do
pdf.stroke_line pdf.bounds.bottom_left, pdf.bounds.bottom_right
end
end
# Checkboxes
checkbox_padding = 6
((task_start_row + 1)..task_last_row).each_with_index do |row, index|
pdf.grid([row, 0], [row, 1]).bounding_box do
draw_checkbox pdf, checkbox_padding, TASKS_BY_WDAY[date.wday][index]
end
end
# Hour Grid
hour_start_row = task_last_row + 1
hour_last_row = hour_start_row + hour_row_count - 1
# Horizontal Lines
(hour_start_row..hour_last_row).each do |row|
pdf.grid([row, 0], [row, 1]).bounding_box do
pdf.stroke_line pdf.bounds.bottom_left, pdf.bounds.bottom_right
end
end
# Vertical lines
overhang = 24
pdf.dash [1, 2]
pdf.grid([hour_start_row + 1, 0], [hour_last_row, 0]).bounding_box do
pdf.stroke_line([pdf.bounds.top_left[0] + overhang, pdf.bounds.top_left[1]], [pdf.bounds.bottom_left[0] + overhang, pdf.bounds.bottom_left[1]])
end
# half plus change
pdf.grid([hour_start_row + 1, 0], [hour_last_row, 0]).bounding_box do
pdf.stroke_line([pdf.bounds.top_right[0] + overhang * 0.5, pdf.bounds.top_right[1]], [pdf.bounds.bottom_right[0] + overhang * 0.5, pdf.bounds.bottom_right[1]])
end
pdf.grid([hour_start_row + 1, 1], [hour_last_row, 1]).bounding_box do
pdf.stroke_line(pdf.bounds.top_right, pdf.bounds.bottom_right)
end
pdf.undash
# Hour labels
(0...HOUR_COUNT).each do |hour|
row = hour + hour_start_row + 1
if hour_label = HOUR_LABELS[hour]
pdf.grid(row, -1).bounding_box do
pdf.translate(20, 0) { pdf.text hour_label.to_s, align: :right, valign: :center }
end
end
if appointment_label = APPOINTMENTS_BY_WDAY[date.wday][HOUR_LABELS[hour]]
pdf.grid([row, 0], [row, 2]).bounding_box do
pdf.translate(overhang + 4, 0) {
pdf.text appointment_label.to_s, color: MEDIUM_COLOR, align: :left, valign: :center
}
end
end
end
end
end
end
options = parse_options
init_i18n(options[:locale])
puts "#{options[:date_source]} Will save to #{FILE_NAME}"
sunday = options[:date]
pdf = init_pdf
options[:weeks].times do |week|
begin_new_page(pdf, :right) unless week.zero?
monday = sunday.next_day(1)
next_sunday = sunday.next_day(7)
# Quarterly goals
if sunday.month != next_sunday.month && (next_sunday.month % 3) == Q1_START_MONTH
first = Date.new(next_sunday.year, next_sunday.month, 1)
last = first.next_month(3).prev_day
puts "Generating quarterly goals page for Q#{quarter(first)} #{date_range(first, last)}"
quarter_ahead(pdf, first, last)
end
puts "Generating planner pages for #{date_range(monday, next_sunday)}"
# Weekly goals
week_ahead_page pdf, monday, next_sunday
# Daily pages
(1..5).each do |i|
day = sunday.next_day(i)
daily_tasks_page pdf, day
daily_calendar_page pdf, day
end
# Weekend page
weekend_page pdf, sunday.next_day(6), next_sunday
sunday = sunday.next_day(7)
end
pdf.render_file FILE_NAME