├── sample.pdf
├── .gitignore
├── Gemfile
├── config
├── tasks.yaml
└── locales
│ ├── en.yml
│ └── de.yml
├── Gemfile.lock
├── notes.rb
├── CHANGELOG.md
├── config.rb
├── README.md
├── one-on-one.rb
├── shared.rb
└── planner.rb
/sample.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/drewish/planner/HEAD/sample.pdf
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | one-on-one_forms.pdf
2 | time_block_pages.pdf
3 | notes.pdf
4 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source "https://rubygems.org"
4 |
5 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
6 |
7 | gem "prawn"
8 | gem "pry"
9 | gem "i18n"
10 |
--------------------------------------------------------------------------------
/config/tasks.yaml:
--------------------------------------------------------------------------------
1 | # Daily tasks organized by day of the week
2 | # The numbers represent the row position where the task should appear on the planner page
3 |
4 | sunday:
5 | 1: "Plan meals"
6 | 2: "Grocery shopping"
7 |
8 | monday:
9 | 0: "Update weekly goals"
10 |
11 | tuesday:
12 | 0: "Review weekly goals"
13 |
14 | wednesday:
15 | 0: "Review weekly goals"
16 |
17 | thursday:
18 | 0: "Review weekly goals"
19 |
20 | friday:
21 | 0: "Review weekly goals"
22 |
23 | saturday:
24 | 1: "Plan next week"
25 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | coderay (1.1.3)
5 | concurrent-ruby (1.2.3)
6 | i18n (1.14.4)
7 | concurrent-ruby (~> 1.0)
8 | method_source (1.0.0)
9 | pdf-core (0.9.0)
10 | prawn (2.4.0)
11 | pdf-core (~> 0.9.0)
12 | ttfunk (~> 1.7)
13 | pry (0.14.1)
14 | coderay (~> 1.1)
15 | method_source (~> 1.0)
16 | ttfunk (1.7.0)
17 |
18 | PLATFORMS
19 | ruby
20 |
21 | DEPENDENCIES
22 | i18n
23 | prawn
24 | pry
25 |
26 | BUNDLED WITH
27 | 2.2.33
28 |
--------------------------------------------------------------------------------
/notes.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require_relative './shared'
4 | FILE_NAME = "notes.pdf"
5 |
6 | puts "Generating a notes page into #{FILE_NAME}"
7 |
8 | options = { locale: 'en' }
9 | OptionParser.new do |parser|
10 | parser.banner = "Usage: #{$PROGRAM_NAME} [options]"
11 | parser.on('-l', '--locale LOCALE', 'Locale to use for internationalization')
12 | parser.on("-h", "--help", "Prints this help") do
13 | puts parser
14 | exit
15 | end
16 | end.parse!(into: options)
17 |
18 | init_i18n(options[:locale])
19 |
20 | pdf = init_pdf
21 | hole_punches pdf
22 |
23 | heading_left = I18n.t('notes_heading')
24 | notes_page pdf, heading_left
25 | begin_new_page pdf, :left
26 | notes_page pdf, heading_left
27 |
28 | pdf.render_file FILE_NAME
29 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## 2025-06
4 | - Load appointments from YAML file.
5 | - Load tasks from YAML file.
6 |
7 | ## 2024-11
8 | - Allow periodic scheduling of 1:1s.
9 |
10 | ## 2024-10
11 | - Improved the date range format.
12 |
13 | ## 2024-06
14 | - I18n for 1:1 pages.
15 |
16 | ## 2024-05
17 | - Localize notes pages.
18 |
19 | ## 2024-04
20 | - I18n for the planner pages by Sumidu.
21 | - Added the --weeks option.
22 | - Added ability to schedule daily appointments.
23 |
24 | ## 2024-01
25 | - Size of metrics block is now configurable.
26 |
27 | ## 2023-12
28 | - Added script to generate notes pages.
29 |
30 | ## 2023-09
31 | - Fix wrapping with longer strings.
32 | - Added a back to the 1:1 pages.
33 |
34 | ## 2023-05
35 | - Split the one-on-ones into a separate script.
36 |
37 | ## 2023-03
38 | - Added a quarterly planning page.
39 | - Options to control start of quarters.
40 |
41 | ## 2023-02
42 | - Switched to 2-week sprints.
43 | - Option to sort 1:1 pages by name.
44 |
45 | ## 2023-01
46 | - Links to more forks.
47 |
48 | ## 2022-12
49 | - Links to forks.
50 |
51 | ## 2022-11
52 | - Options to prefill reoccurring daily tasks.
53 |
54 |
55 | ...and plenty of older stuff
56 |
--------------------------------------------------------------------------------
/config.rb:
--------------------------------------------------------------------------------
1 | # Hours shown on the day schedule. You can leave nils if you want a blank to write in.
2 | HOUR_LABELS = [nil, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, nil, nil]
3 | HOUR_COUNT = HOUR_LABELS.length
4 | COLUMN_COUNT = 4
5 | LIGHT_COLOR = 'AAAAAA'
6 | MEDIUM_COLOR = '888888'
7 | DARK_COLOR = '000000'
8 | OSX_FONT_PATH = "/System/Library/Fonts/Supplemental/Futura.ttc"
9 | FONTS = {
10 | 'Futura' => {
11 | normal: { file: OSX_FONT_PATH, font: 'Futura Medium' },
12 | italic: { file: OSX_FONT_PATH, font: 'Futura Medium Italic' },
13 | bold: { file: OSX_FONT_PATH, font: 'Futura Condensed ExtraBold' },
14 | condensed: { file: OSX_FONT_PATH, font: 'Futura Condensed Medium' },
15 | }
16 | }
17 | PAGE_SIZE = 'LETTER' # Could also do 'A4'
18 | # Order is top, right, bottom, left
19 | LEFT_PAGE_MARGINS = [36, 72, 36, 36]
20 | RIGHT_PAGE_MARGINS = [36, 36, 36, 72]
21 |
22 | # Adjust the quarters to a fiscal year, 1 for Jan, 2 for Feb, etc.
23 | Q1_START_MONTH = 2
24 | QUARTERS_BY_MONTH = (1..12).map { |month| (month / 3.0).ceil }.rotate(1 - Q1_START_MONTH).unshift(nil)
25 |
26 | # Adjust the start of semesters
27 | SUMMER_SEMESTER_START = 4 # April
28 | WINTER_SEMESTER_START = 10 # October
29 |
30 | # Use these if you have sprints of a weekly interval
31 | SPRINT_EPOCH = Date.parse('2023-01-04')
32 | SPRINT_LENGTH = 14
33 |
34 | # Returns nested array, names by day of week, 0 is Sunday.
35 | def one_on_ones_for sunday
36 | # Weekly
37 | sun = []
38 | mon = []
39 | tue = %w(Randy)
40 | wed = %w(Jose Jason)
41 | thr = %w(Amulya)
42 | fri = []
43 | sat = []
44 |
45 | # Biweekly
46 | cweek = sunday.cweek
47 | wed << 'Jose Luis' if cweek % 2 == 0
48 | wed << 'Mamatha' if cweek % 2 == 1
49 |
50 | # Monthly
51 | tue << 'Tyler' if cweek % 4 == 1
52 | wed << 'Guerrero' if cweek % 4 == 3
53 |
54 | [sun, mon, tue, wed, thr, fri, sat]
55 | end
56 |
57 |
58 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Generate Time-Block Planner Pages
2 |
3 | I'm a big fan of [Cal Newport's Time-Block Planner](https://www.timeblockplanner.com)
4 | but I didn't like having unused weekend pages and got tired of writing in the
5 | dates so I wrote this script to generate my own version of it. It generates a
6 | PDF with a week's worth of 8.5 x 11 inch pages.
7 |
8 | I'm also a fan of [Manager Tools' 1-on-1s](https://www.manager-tools.com/map-universe/one-ones),
9 | so I incorporated a version of their meeting form. You specify which people you
10 | meet every week, and you'll get a page for each.
11 |
12 | Take a look at a [sample](sample.pdf) and see what you think. If it's not to
13 | your liking, feel free to customize it, or try out some of the other variations people have put together:
14 | - [jlorenzetti's fork](https://github.com/jlorenzetti/planner) generates A4
15 | pages in Helvetica, and omits the 1-on-1 forms.
16 | - [pzula's fork](https://github.com/pzula/planner) is based off of jlorenzetti's but scales it down to A5.
17 | - [Hyunggilwoo's fork](https://github.com/Hyunggilwoo/planner) uses UbuntuMono
18 | and omits 1-on-1 forms. It looks like a good choice for Ubuntu users.
19 | - [dianalow's fork](https://github.com/dianalow/time-block-planner) is scaled to fit in the [TRAVELER’S notebook](https://travelerscompanyusa.com/travelers-notebook-story/), and as usual omits, the 1:1 forms.
20 |
21 | ## Installation
22 |
23 | Assuming you've got [Ruby](http://www.ruby-lang.org/en/) and [Bundler](https://bundler.io)
24 | installed you can just run:
25 | ```
26 | git clone git@github.com:drewish/planner.git
27 | cd planner
28 | bundle install
29 | ```
30 |
31 | ## Usage
32 |
33 | ### Planner Pages
34 |
35 | You can generate planner pages for the current week:
36 | ```sh
37 | ./planner.rb
38 | ```
39 |
40 | Or, you can generate a different week's pages by passing in the date:
41 | ```sh
42 | ./planner.rb 2023-05-01
43 | ```
44 |
45 | If you'd like to generate multiple weeks at once:
46 | ```sh
47 | ./planner.rb --weeks 4
48 | ```
49 |
50 | On a Mac you can send the PDF directly to your printer:
51 | ```sh
52 | lpr time_block_pages.pdf
53 | ```
54 |
55 | ### One-on-one Pages
56 |
57 | The script that generates the 1-on-1 forms supports the same options:
58 | ```sh
59 | ./one-on-one.rb -weeks 2 2023-05-01
60 | ```
61 |
62 | ### Notes Pages
63 |
64 | You can also generate a PDF of some simple lined pages:
65 | ```sh
66 | ./notes.rb
67 | ```
68 |
69 | ## Limitations
70 |
71 | Probably only works on a Mac since it hardcodes the font path.
72 |
73 | ## Thanks
74 |
75 | - [@Sumidu](https://github.com/Sumidu) for contributing the internationalization code
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | en:
2 | date:
3 | day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
4 | abbr_day_names: [Su, Mo, Tu, We, Th, Fr, Sa]
5 | month_names:
6 | [
7 | ~,
8 | January,
9 | February,
10 | March,
11 | April,
12 | May,
13 | June,
14 | July,
15 | August,
16 | September,
17 | October,
18 | November,
19 | December,
20 | ]
21 | abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
22 | formats:
23 | default: "%m/%d/%Y"
24 | short: "%b %d"
25 | medium: "%B %-d, %Y"
26 | long: "%A, %B %-d, %Y"
27 | weekday: "%A"
28 | year: "%Y"
29 | range_start: "%B %-d, %Y – "
30 | range_end: "%B %-d, %Y"
31 | range_start_same_year: "%B %-d – "
32 | range_end_same_year: "%B %-d, %Y"
33 | range_start_same_month: "%B %-d – "
34 | range_end_same_month: "%-d, %Y"
35 | business_days_in_year:
36 | zero: "last business day of the year"
37 | one: "one more business day in the year"
38 | other: "%{count} business days in the year"
39 | days_left_in_sprint:
40 | zero: "last day in the sprint"
41 | one: "one more day in the sprint"
42 | other: "%{count} more days in the sprint"
43 | notes: "Notes:"
44 | tasks: "Tasks:"
45 | daily_metrics: "Daily Metrics"
46 | shutdown_complete: "Shutdown complete"
47 | semester: "Semester"
48 | quarter: "Quarter %{number}"
49 | week: "Week"
50 | day: "Day"
51 | week_plan_heading: "Weekly plan"
52 | semester_plan_heading: "Semester plan"
53 | quarter_plan_heading: "Quarterly plan"
54 | notes_heading: "Notes"
55 | summer: "Summer semester"
56 | winter: "Winter semester"
57 | document_title: "Semesterplan"
58 | content_outline: "Outline of content"
59 | semester_overview: "Semester overview"
60 | weekly_overview: "Weekly overviews"
61 | personal_notes: "Personal/Notes:"
62 | personal_notes_example: "(Spouse, children, pets, hobbies, friends, history, etc.)"
63 | their_update: "Their update:"
64 | their_update_instructions: "(Notes you take from their “10 minutes”)"
65 | my_update: "My update:"
66 | my_update_instructions: "(Notes you make to prepare for your “10 minutes”)"
67 | future: "Future/Follow Up:"
68 | future_instructions: "(Where are they headed? Items to review at the next 1:1)"
69 | additional_notes: "Additional Notes:"
70 | feedback: "Feedback:"
71 | questions_to_ask: "Questions to ask:"
72 | questions_left: |
73 | • Tell me about what you’ve been working on.
74 | • Tell me about your week – what’s it been like?
75 | • Tell me about your family/weekend/activities?
76 | • Where are you on ( ) project?
77 | • Are you on track to meet the deadline?
78 | • What questions do you have about the project?
79 | • What did ( ) say about this?
80 | questions_right: |
81 | • Is there anything I need to do, and if so by when?
82 | • How are you going to approach this?
83 | • What do you think you should do?
84 | • So, you’re going to do “( )” by “( )”, right?
85 | • What can you/we do differently next time?
86 | • Any ideas/suggestions/improvements?
87 |
--------------------------------------------------------------------------------
/config/locales/de.yml:
--------------------------------------------------------------------------------
1 | de:
2 | date:
3 | day_names: [Sonntag, Montag, Dienstag, Mittwoch, Donnerstag, Freitag, Samstag]
4 | abbr_day_names: [So, Mo, Di, Mi, Do, Fr, Sa]
5 | month_names:
6 | [
7 | ~,
8 | Januar,
9 | Februar,
10 | März,
11 | April,
12 | Mai,
13 | Juni,
14 | Juli,
15 | August,
16 | September,
17 | Oktober,
18 | November,
19 | Dezember,
20 | ]
21 | abbr_month_names: [~, Jan, Feb, Mär, Apr, Mai, Jun, Jul, Aug, Sep, Okt, Nov, Dez]
22 | formats:
23 | default: "%d.%m.%Y"
24 | short: "%d. %b"
25 | medium: "%-d. %B"
26 | long: "%d. %B %Y"
27 | weekday: "%A"
28 | year: "%Y"
29 | range_start: "%-d. %B, %Y – "
30 | range_end: "%-d. %B, %Y"
31 | range_start_same_year: "%-d. %B – "
32 | range_end_same_year: "%-d. %B, %Y"
33 | range_start_same_month: "%-d. – "
34 | range_end_same_month: "%-d. %B, %Y"
35 | business_days_in_year:
36 | zero: "letzter Arbeitstag im Jahr"
37 | one: "ein weiterer Arbeitstag im Jahr"
38 | other: "%{count} Arbeitstage im Jahr"
39 | days_left_in_sprint:
40 | zero: "letzter Tag im Sprint"
41 | one: "ein weiterer Tag im Sprint"
42 | other: "%{count} weitere Tage im Sprint"
43 | notes: "Notizen:"
44 | tasks: "Aufgaben:"
45 | daily_metrics: "Daily Metrics"
46 | shutdown_complete: "Shutdown complete"
47 | semester: "Semester"
48 | quarter: "Quartal %{number}"
49 | week: "Woche"
50 | day: "Tag"
51 | week_plan_heading: "Die bevorstehende Woche"
52 | semester_plan_heading: "Semesterplan"
53 | quarter_plan_heading: "Quartalsplan"
54 | notes_heading: "Notizen"
55 | summer: "Sommersemester"
56 | winter: "Wintersemester"
57 | document_title: "Semesterplan"
58 | content_outline: "Inhaltsübersicht"
59 | semester_overview: "Semesterübersicht"
60 | weekly_overview: "Wochenübersicht"
61 | personal_notes: "Persönliches/Notizen:"
62 | personal_notes_example: "(Partner, Kinder, Haustiere, Hobbies, Freunde, Vorgeschichte, etc.)"
63 | their_update: "Ihr Update:"
64 | their_update_instructions: "(Notizen aus ihren “10 Minuten”)"
65 | my_update: "Mein Update:"
66 | my_update_instructions: "(Notizen als Vorbereitung für meine “10 Minuten”)"
67 | future: "Zukunft/Follow Up:"
68 | future_instructions: "(Was kommt als nächstes? Dinge für das nächste 1:1)"
69 | additional_notes: "Zusätzliche Notizen:"
70 | feedback: "Feedback:"
71 | questions_to_ask: "Leitfragen:"
72 | questions_left: |
73 | • An was hast Du diese Woche gearbeitet?
74 | • Wie ist es Dir seit letzter Woche ergangen??
75 | • Hast Du Dich am Wochende erholen können? ?
76 | • Wie läuft es mit Projekt ( )?
77 | • Ist die Deadline noch zu erreichen?
78 | • Benötigst Du irgendwelchen Input von mir dazu?
79 | • Was hat ( ) dazu gesagt ?
80 | questions_right: |
81 | • Gibt es etwas was ich tun muss und bis wann?
82 | • Hast Du Dir überlegt, wie Du das angehen möchtest?
83 | • Was glaubst Du solltest Du tun?
84 | • Also wirst Du “( )” bis “( )” gemacht haben, richtig?
85 | • Was kannst Du/Wir nächstes Mal anders machen?
86 | • Hast Du Ideen/Anregungen/Verbesserungsvorschläge dazu?
87 |
--------------------------------------------------------------------------------
/one-on-one.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require_relative './shared'
4 | FILE_NAME = "one-on-one_forms.pdf"
5 |
6 |
7 | def sections pdf, first_row, last_row, headings
8 | (first_row..last_row).each do |row|
9 | pdf.grid([row, 0],[row, 3]).bounding_box do
10 | if headings[row]
11 | pdf.text headings[row], inline_format: true, valign: :bottom
12 | else
13 | pdf.stroke_line pdf.bounds.bottom_left, pdf.bounds.bottom_right
14 | end
15 | end
16 | end
17 | end
18 |
19 |
20 | def one_on_one_page pdf, name, date
21 | header_row_count = 2
22 | body_row_count = HOUR_COUNT * 2
23 | total_row_count = header_row_count + body_row_count
24 | pdf.define_grid(columns: COLUMN_COUNT, rows: total_row_count, gutter: 0)
25 | # pdf.grid.show_all
26 |
27 | pdf.grid([0, 0],[1, 1]).bounding_box do
28 | pdf.text name, heading_format(align: :left)
29 | end
30 | pdf.grid([1, 0],[1, 1]).bounding_box do
31 | pdf.text I18n.l(date, format: :long), subheading_format(align: :left)
32 | end
33 | # grid([0, 2],[0, 3]).bounding_box do
34 | # text "right heading", heading_format(align: :right)
35 | # end
36 |
37 | sections(pdf, 2, body_row_count, {
38 | 2 => "#{I18n.t('personal_notes')} #{I18n.t('personal_notes_example')}",
39 | 5 => "#{I18n.t('their_update')} #{I18n.t('their_update_instructions')}",
40 | 15 => "#{I18n.t('my_update')} #{I18n.t('my_update_instructions')}",
41 | 24 => "#{I18n.t('future')} #{I18n.t('future_instructions')}",
42 | })
43 |
44 | # Back of the page
45 | begin_new_page pdf, :left
46 |
47 | pdf.grid([0, 0],[1, 1]).bounding_box do
48 | pdf.text name, heading_format(align: :left)
49 | end
50 | pdf.grid([1, 0],[1, 1]).bounding_box do
51 | pdf.text I18n.l(date, format: :long), subheading_format(align: :left)
52 | end
53 |
54 | question_start = 25
55 | question_end = question_start + 4
56 |
57 | sections(pdf, 2, question_start - 1, {
58 | 2 => I18n.t('additional_notes'),
59 | 20 => I18n.t('feedback'),
60 | })
61 |
62 | pdf.grid([question_start, 0],[question_start, 3]).bounding_box do
63 | pdf.text I18n.t('questions_to_ask'), valign: :bottom, color: DARK_COLOR
64 | end
65 | pdf.grid([question_start + 1, 0],[question_end, 1]).bounding_box do
66 | pdf.text I18n.t('questions_left'), size: 10, color: MEDIUM_COLOR
67 | end
68 | pdf.grid([question_start + 1, 2],[question_end, 3]).bounding_box do
69 | pdf.text I18n.t('questions_right'), size: 10, color: MEDIUM_COLOR
70 | end
71 | end
72 |
73 |
74 | options = parse_options
75 | init_i18n(options[:locale])
76 | puts "#{options[:date_source]} Will save to #{FILE_NAME}"
77 | sunday = options[:date]
78 |
79 | pdf = init_pdf
80 |
81 | options[:weeks].times do |week|
82 | begin_new_page(pdf, :right) unless week.zero?
83 |
84 | monday = sunday.next_day(1)
85 | next_sunday = sunday.next_day(7)
86 | puts "Generating one-on-one forms for #{date_range(monday, next_sunday)}"
87 |
88 | names_and_dates = one_on_ones_for(sunday)
89 | .each_with_index
90 | .reject { |names, _| names.nil? }
91 | .flat_map { |names, wday| names.map {|name| [name, sunday.next_day(wday)] } }
92 |
93 | # Show who we're meeting each day
94 | names_and_dates
95 | .group_by { |name, date| date }
96 | .transform_values{ |day| day.map{ |name, _| name }.sort }
97 | .map { |date, names| puts "#{I18n.l(date, format: :long)}\n- #{names.join("\n- ")}" }
98 |
99 | hole_punches pdf
100 |
101 | names_and_dates
102 | .sort_by { |name, date| "#{name}#{date.iso8601}" } # Sort by name or date, as you like
103 | .each_with_index { |name_and_date, index|
104 | begin_new_page(pdf, :right) unless index.zero?
105 | one_on_one_page(pdf, *name_and_date)
106 | }
107 |
108 | sunday = sunday.next_day(7)
109 | end
110 |
111 | pdf.render_file FILE_NAME
112 |
--------------------------------------------------------------------------------
/shared.rb:
--------------------------------------------------------------------------------
1 | require 'prawn'
2 | require 'prawn/measurement_extensions'
3 | require 'pry'
4 | require 'date'
5 | require 'i18n'
6 | require 'optparse'
7 | require_relative './config'
8 |
9 | def init_pdf
10 | pdf = Prawn::Document.new(margin: RIGHT_PAGE_MARGINS, print_scaling: :none)
11 | pdf.font_families.update(FONTS)
12 | pdf.font(FONTS.keys.first)
13 | pdf.stroke_color MEDIUM_COLOR
14 | pdf.line_width(0.5)
15 | pdf
16 | end
17 |
18 | def init_i18n(locale)
19 | I18n.load_path += Dir[File.expand_path("config/locales") + "/*.yml"]
20 | I18n.default_locale = locale if locale
21 | end
22 |
23 | def parse_options
24 | options = { weeks: 1, locale: 'en' }
25 | OptionParser.new do |parser|
26 | parser.banner = "Usage: #{$PROGRAM_NAME} [options] [STARTDATE]"
27 | parser.on('-l', '--locale LOCALE', 'Locale to use for internationalization')
28 | parser.on('-w', '--weeks WEEKS', OptionParser::DecimalInteger, 'Number of weeks to generatate at once')
29 | parser.on("-h", "--help", "Prints this help") do
30 | puts parser
31 | exit
32 | end
33 | end.parse!(into: options)
34 |
35 | abort("Weeks must be greater than zero") unless options[:weeks] > 0
36 |
37 | # Figure out the start date
38 | if ARGV.empty?
39 | source = "No date argument provided, "
40 | date = DateTime.now.to_date
41 | if date.wday > 2
42 | source += "defaulting to next week."
43 | date = date.next_day(7 - date.wday)
44 | else
45 | source += "defaulting to current week."
46 | date = date.prev_day(date.wday)
47 | end
48 | else
49 | date = DateTime.parse(ARGV.first).to_date
50 | source = "Parsed #{date} from date argument."
51 | date = date.prev_day(date.wday)
52 | end
53 | options.merge(date: date, date_source: source)
54 | end
55 |
56 | def begin_new_page pdf, side
57 | margin = side == :left ? LEFT_PAGE_MARGINS : RIGHT_PAGE_MARGINS
58 | pdf.start_new_page size: PAGE_SIZE, layout: :portrait, margin: margin
59 | if side == :right
60 | hole_punches pdf
61 | end
62 | end
63 |
64 | def hole_punches pdf
65 | pdf.canvas do
66 | x = 25
67 | # Measuring it on the page it should be `[(1.25).in, (5.5).in, (9.75).in]`,
68 | # but depending on the printer driver it might do some scaling. With one
69 | # driver I printed a bunch of test pages and found that `[72, 392, 710]`
70 | # put it in the right place so your milage may vary.
71 | [(1.25).in, (5.5).in, (9.75).in].each do |y|
72 | pdf.horizontal_line x - 5, x + 5, at: y
73 | pdf.vertical_line y - 5, y + 5, at: x
74 | end
75 | end
76 | end
77 |
78 | def heading_format(overrides = {})
79 | { size: 20, color: DARK_COLOR }.merge(overrides)
80 | end
81 |
82 | def subheading_format(overrides = {})
83 | { size: 12, color: MEDIUM_COLOR }.merge(overrides)
84 | end
85 |
86 | def draw_checkbox pdf, checkbox_padding = 6, label = nil
87 | checkbox_size = pdf.grid.row_height - (2 * checkbox_padding)
88 | no_label = label.nil? || label.empty?
89 | original_color = pdf.stroke_color
90 | pdf.stroke_color(LIGHT_COLOR)
91 | pdf.dash([1, 2], phase: 0.5) if no_label
92 | pdf.rectangle [pdf.bounds.top_left[0] + checkbox_padding, pdf.bounds.top_left[1] - checkbox_padding], checkbox_size, checkbox_size
93 | pdf.stroke
94 | pdf.undash if no_label
95 | pdf.stroke_color(original_color)
96 |
97 | unless no_label
98 | pdf.translate checkbox_size + (2 * checkbox_padding), 0 do
99 | pdf.text label, color: MEDIUM_COLOR, valign: :center
100 | end
101 | end
102 | end
103 |
104 | # Caller needs to start the page, so this could be the first page.
105 | def notes_page pdf, heading_left, subheading_left = nil, heading_right = nil, subheading_right = nil
106 | header_row_count = 2
107 | body_row_count = HOUR_COUNT * 2
108 | first_column = 0
109 | last_column = COLUMN_COUNT - 1
110 | first_row = header_row_count
111 | last_row = header_row_count + body_row_count - 1
112 |
113 | pdf.define_grid(columns: COLUMN_COUNT, rows: header_row_count + body_row_count, gutter: 0)
114 | # grid.show_all
115 |
116 | # Header Left
117 | if heading_left
118 | pdf.grid([0, first_column],[0, last_column]).bounding_box do
119 | pdf.text heading_left, heading_format(align: :left)
120 | end
121 | end
122 | if subheading_left
123 | pdf.grid([1, first_column],[1, last_column]).bounding_box do
124 | pdf.text subheading_left, subheading_format(align: :left)
125 | end
126 | end
127 | # Header Right
128 | if heading_right
129 | pdf.grid([0, 3],[0, last_column]).bounding_box do
130 | pdf.text heading_right, heading_format(align: :right)
131 | end
132 | end
133 | if subheading_right
134 | pdf.grid([1, 3],[1, last_column]).bounding_box do
135 | pdf.text subheading_right, subheading_format(align: :right)
136 | end
137 | end
138 |
139 | # Horizontal lines
140 | (first_row..last_row).each do |row|
141 | pdf.grid([row, first_column], [row, last_column]).bounding_box do
142 | pdf.stroke_line pdf.bounds.bottom_left, pdf.bounds.bottom_right
143 | end
144 | end
145 |
146 | # Checkboxes
147 | ((first_row + 1)..last_row).each do |row|
148 | pdf.grid(row, 0).bounding_box do
149 | draw_checkbox pdf
150 | end
151 | end
152 | end
153 |
154 | def date_range(start, finish)
155 | formats =
156 | if start.year != finish.year
157 | # different years, print full dates
158 | [:range_start, :range_end]
159 | elsif start.month != finish.month
160 | # same year, diff month
161 | [:range_start_same_year, :range_end_same_year]
162 | else
163 | # same year and month
164 | [:range_start_same_month, :range_end_same_month]
165 | end
166 | [I18n.l(start, format: formats.first), I18n.l(finish, format: formats.last)].join
167 | end
168 |
--------------------------------------------------------------------------------
/planner.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require_relative './shared'
4 | require 'yaml'
5 |
6 | FILE_NAME = "time_block_pages.pdf"
7 |
8 | def load_weekly_data_from_yaml(yaml_file, data_type = "data")
9 | begin
10 | data = YAML.load_file(yaml_file)
11 | rescue Errno::ENOENT
12 | puts "Warning: #{yaml_file} not found. Using empty #{data_type} list."
13 | return Array.new(7) { {} }
14 | rescue Psych::SyntaxError => e
15 | puts "Error parsing #{yaml_file}: #{e.message}"
16 | puts "Using empty #{data_type} list."
17 | return Array.new(7) { {} }
18 | end
19 |
20 | # Convert from day name keys to array indexed by day of week (0=Sunday, 1=Monday, etc.)
21 | day_names = %w[sunday monday tuesday wednesday thursday friday saturday]
22 | data_by_wday = []
23 |
24 | day_names.each_with_index do |day_name, wday|
25 | data_by_wday[wday] = data[day_name] || {}
26 | end
27 |
28 | data_by_wday
29 | end
30 |
31 | # From https://stackoverflow.com/a/24753003/203673
32 | #
33 | # Calculates the number of business days in range (start_date, end_date]
34 | #
35 | # @param start_date [Date]
36 | # @param end_date [Date]
37 | #
38 | # @return [Fixnum]
39 | def business_days_between(start_date, end_date)
40 | days_between = (end_date - start_date).to_i
41 | return 0 unless days_between > 0
42 |
43 | # Assuming we need to calculate days from 9th to 25th, 10-23 are covered
44 | # by whole weeks, and 24-25 are extra days.
45 | #
46 | # Su Mo Tu We Th Fr Sa # Su Mo Tu We Th Fr Sa
47 | # 1 2 3 4 5 # 1 2 3 4 5
48 | # 6 7 8 9 10 11 12 # 6 7 8 9 ww ww ww
49 | # 13 14 15 16 17 18 19 # ww ww ww ww ww ww ww
50 | # 20 21 22 23 24 25 26 # ww ww ww ww ed ed 26
51 | # 27 28 29 30 31 # 27 28 29 30 31
52 | whole_weeks, extra_days = days_between.divmod(7)
53 |
54 | unless extra_days.zero?
55 | # Extra days start from the week day next to start_day,
56 | # and end on end_date's week date. The position of the
57 | # start date in a week can be either before (the left calendar)
58 | # or after (the right one) the end date.
59 | #
60 | # Su Mo Tu We Th Fr Sa # Su Mo Tu We Th Fr Sa
61 | # 1 2 3 4 5 # 1 2 3 4 5
62 | # 6 7 8 9 10 11 12 # 6 7 8 9 10 11 12
63 | # ## ## ## ## 17 18 19 # 13 14 15 16 ## ## ##
64 | # 20 21 22 23 24 25 26 # ## 21 22 23 24 25 26
65 | # 27 28 29 30 31 # 27 28 29 30 31
66 | #
67 | # If some of the extra_days fall on a weekend, they need to be subtracted.
68 | # In the first case only corner days can be days off,
69 | # and in the second case there are indeed two such days.
70 | tomorrow = start_date.next_day(1)
71 | extra_days -= if tomorrow.wday <= end_date.wday
72 | [tomorrow.sunday?, end_date.saturday?].count(true)
73 | else
74 | 2
75 | end
76 | end
77 |
78 | (whole_weeks * 5) + extra_days
79 | end
80 |
81 | def business_days_left_in_year(date)
82 | days = business_days_between(date, Date.new(date.year, 12, 31))
83 | I18n.t('business_days_in_year', count: days)
84 | end
85 |
86 | def business_days_left_in_sprint(date)
87 | # Use this if you have sprints that start on the 1st and 15th.
88 | #sprint_end = Date.new(date.year, date.month, date.mday <= 15 ? 15 : -1)
89 |
90 | # Use this if you have two week sprints from a given day.
91 | sprint_start = SPRINT_EPOCH.step(date, SPRINT_LENGTH).to_a.last
92 | sprint_end = sprint_start.next_day(SPRINT_LENGTH - 1)
93 |
94 | days = business_days_between(date, sprint_end)
95 | I18n.t('days_left_in_sprint', count: days)
96 | end
97 |
98 | def quarter(date)
99 | QUARTERS_BY_MONTH[date.month]
100 | end
101 |
102 | # pick summer or winter semester depending on the month
103 | def semester_year(date)
104 | if date.month >= SUMMER_SEMESTER_START && date.month < WINTER_SEMESTER_START
105 | I18n.l(date, format: :year)
106 | else
107 | "#{I18n.l(date, format: :year)} / #{I18n.l(date.next_year, format: :year)}"
108 | end
109 | end
110 |
111 | # * * *
112 |
113 | def quarter_ahead pdf, first_day, last_day
114 | heading_left = I18n.t('quarter_plan_heading')
115 | subheading_left = date_range(first_day, last_day)
116 | heading_right = I18n.t('quarter', number: quarter(first_day))
117 | subheading_right = I18n.l(last_day, format: :year)
118 |
119 | # We let the caller start our page for us but we'll do both sides
120 | hole_punches pdf
121 | notes_page pdf, heading_left, subheading_left, heading_right, subheading_right
122 | begin_new_page pdf, :left
123 | notes_page pdf, heading_left, subheading_left, heading_right, subheading_right
124 | begin_new_page pdf, :right
125 | end
126 |
127 | def week_ahead_page pdf, first_day, last_day
128 | heading_left = I18n.t('week_plan_heading')
129 | subheading_left = date_range(first_day, last_day)
130 | heading_right = first_day.strftime("#{I18n.t('week')} %W")
131 | subheading_right = I18n.t('quarter', number: quarter(first_day))
132 |
133 | # We don't start our own page since we don't know if this is the first week or one
134 | # of several weeks in a file.
135 | hole_punches pdf
136 | notes_page pdf, heading_left, subheading_left, heading_right, subheading_right
137 | end
138 |
139 | # Caller needs to start the page, so this could be the first page.
140 | def notes_page pdf, heading_left, subheading_left = nil, heading_right = nil, subheading_right = nil
141 | header_row_count = 2
142 | body_row_count = HOUR_COUNT * 2
143 | first_column = 0
144 | last_column = COLUMN_COUNT - 1
145 | first_row = header_row_count
146 | last_row = header_row_count + body_row_count - 1
147 |
148 | pdf.define_grid(columns: COLUMN_COUNT, rows: header_row_count + body_row_count, gutter: 0)
149 | # pdf.grid.show_all
150 |
151 | # Header Left
152 | if heading_left
153 | pdf.grid([0, first_column],[0, last_column]).bounding_box do
154 | pdf.text heading_left, heading_format(align: :left)
155 | end
156 | end
157 | if subheading_left
158 | pdf.grid([1, first_column],[1, last_column]).bounding_box do
159 | pdf.text subheading_left, subheading_format(align: :left)
160 | end
161 | end
162 | # Header Right
163 | if heading_right
164 | pdf.grid([0, 3],[0, last_column]).bounding_box do
165 | pdf.text heading_right, heading_format(align: :right)
166 | end
167 | end
168 | if subheading_right
169 | pdf.grid([1, 3],[1, last_column]).bounding_box do
170 | pdf.text subheading_right, subheading_format(align: :right)
171 | end
172 | end
173 |
174 | # Horizontal lines
175 | (first_row..last_row).each do |row|
176 | pdf.grid([row, first_column], [row, last_column]).bounding_box do
177 | pdf.stroke_line pdf.bounds.bottom_left, pdf.bounds.bottom_right
178 | end
179 | end
180 |
181 | # Checkboxes
182 | ((first_row + 1)..last_row).each do |row|
183 | pdf.grid(row, 0).bounding_box do
184 | draw_checkbox pdf
185 | end
186 | end
187 | end
188 |
189 | def daily_tasks_page pdf, date, tasks_by_wday, appointments_by_wday, metrics_rows = 5
190 | begin_new_page pdf, :left
191 |
192 | header_row_count = 2
193 | body_row_count = HOUR_COUNT * 2
194 | last_row = header_row_count + body_row_count - 1
195 |
196 | pdf.define_grid(columns: COLUMN_COUNT, rows: header_row_count + body_row_count, gutter: 0)
197 | # pdf.grid.show_all
198 |
199 | # Header
200 | left_header = I18n.l(date, format: :medium)
201 | right_header = I18n.l(date, format: :weekday)
202 | pdf.grid([0, 0],[1, 2]).bounding_box do
203 | pdf.text left_header, heading_format(align: :left)
204 | end
205 | pdf.grid([0, 2],[1, 3]).bounding_box do
206 | pdf.text right_header, heading_format(align: :right)
207 | end
208 |
209 | # Daily metrics
210 | if metrics_rows > 0
211 | pdf.grid([1, 0], [metrics_rows, 3]).bounding_box do
212 | pdf.dash [1, 2]
213 | pdf.stroke_bounds
214 | pdf.undash
215 |
216 | pdf.translate 6, -6 do
217 | pdf.text I18n.t('daily_metrics'), color: MEDIUM_COLOR
218 | end
219 | end
220 |
221 | pdf.grid([metrics_rows, 2], [metrics_rows, 3]).bounding_box do
222 | draw_checkbox pdf, 6, I18n.t('shutdown_complete')
223 | end
224 | end
225 |
226 | # Tasks / Notes
227 | task_note_start = metrics_rows + 1
228 | pdf.grid([task_note_start, 0], [task_note_start, 1]).bounding_box do
229 | pdf.translate 6, 0 do
230 | pdf.text I18n.t('tasks'), color: DARK_COLOR, valign: :center
231 | end
232 | end
233 | pdf.grid([task_note_start, 2], [task_note_start, 3]).bounding_box do
234 | pdf.translate 6, 0 do
235 | pdf.text I18n.t('notes'), color: DARK_COLOR, valign: :center
236 | end
237 | end
238 |
239 | # Horizontal lines
240 | (task_note_start..last_row).each do |row|
241 | pdf.grid([row, 0], [row, 3]).bounding_box do
242 | pdf.stroke_line pdf.bounds.bottom_left, pdf.bounds.bottom_right
243 | end
244 | end
245 |
246 | # Vertical line
247 | pdf.grid([task_note_start + 1, 1], [last_row, 1]).bounding_box do
248 | pdf.dash [1, 2], phase: 2
249 | pdf.stroke_line(pdf.bounds.top_right, pdf.bounds.bottom_right)
250 | pdf.undash
251 | end
252 |
253 | # Checkboxes
254 | checkbox_padding = 6
255 | ((task_note_start + 1)..last_row).each_with_index do |row, index|
256 | # Make the box wider than needed to avoid wrapping if the task name is too long
257 | pdf.grid([row, 0], [row, 4]).bounding_box do
258 | draw_checkbox pdf, checkbox_padding, tasks_by_wday[date.wday][index]
259 | end
260 | end
261 | end
262 |
263 | def daily_calendar_page pdf, date, appointments_by_wday
264 | begin_new_page pdf, :right
265 |
266 | header_row_count = 2
267 | body_row_count = HOUR_COUNT * 2
268 | first_column = 0
269 | last_column = COLUMN_COUNT - 1
270 | fist_hour_row = header_row_count
271 | last_hour_row = header_row_count + body_row_count - 1
272 |
273 | pdf.define_grid(columns: COLUMN_COUNT, rows: header_row_count + body_row_count, gutter: 0)
274 |
275 | # Header
276 | left_header = I18n.l(date, format: :medium)
277 | right_header = I18n.l(date, format: :weekday)
278 | left_subhed = date.strftime("#{I18n.t('quarter', number: quarter(date))} #{I18n.t('week')} %W #{I18n.t('day')} %j")
279 | # right_subhed = business_days_left_in_year(date)
280 | right_subhed = business_days_left_in_sprint(date)
281 | pdf.grid([0, first_column],[1, 1]).bounding_box do
282 | pdf.text left_header, heading_format(align: :left)
283 | end
284 | pdf.grid([0, 2],[0, last_column]).bounding_box do
285 | pdf.text right_header, heading_format(align: :right)
286 | end
287 | pdf.grid([1, first_column],[1, last_column]).bounding_box do
288 | pdf.text left_subhed, subheading_format(align: :left)
289 | end
290 | pdf.grid([1, first_column],[1, last_column]).bounding_box do
291 | pdf.text right_subhed, subheading_format(align: :right)
292 | end
293 |
294 | (0...HOUR_COUNT).each do |hour|
295 | row = hour * 2 + fist_hour_row
296 | # Hour labels
297 | if hour_label = HOUR_LABELS[hour]
298 | pdf.grid(row, -1).bounding_box do
299 | pdf.translate(-4, 0) { pdf.text hour_label.to_s, align: :right, valign: :center }
300 | end
301 | end
302 |
303 | # Default appointments
304 | if appointment_label = appointments_by_wday[date.wday][HOUR_LABELS[hour]]
305 | pdf.grid([row, first_column], [row, last_column]).bounding_box do
306 | pdf.translate(4, 0) do
307 | pdf.text appointment_label.to_s, color: MEDIUM_COLOR, align: :left, valign: :center
308 | end
309 | end
310 | end
311 | end
312 |
313 | # Horizontal lines
314 | ## Top line
315 | pdf.stroke_color MEDIUM_COLOR
316 | overhang = 24
317 | pdf.grid([fist_hour_row, first_column], [fist_hour_row, last_column]).bounding_box do
318 | pdf.stroke_line([pdf.bounds.top_left[0] - overhang, pdf.bounds.top_left[1]], pdf.bounds.top_right)
319 | end
320 | (fist_hour_row..last_hour_row).step(2) do |row|
321 | ## Half hour lines
322 | pdf.dash [1, 2], phase: 2
323 | pdf.grid([row, first_column], [row, last_column]).bounding_box do
324 | pdf.stroke_line([pdf.bounds.bottom_left[0] - overhang, pdf.bounds.bottom_left[1]], pdf.bounds.bottom_right)
325 | end
326 | pdf.undash
327 | ## Hour lines
328 | pdf.grid([row + 1, first_column], [row + 1, last_column]).bounding_box do
329 | pdf.stroke_line([pdf.bounds.bottom_left[0] - overhang, pdf.bounds.bottom_left[1]], pdf.bounds.bottom_right)
330 | end
331 | end
332 |
333 | # Vertical lines
334 | (0..COLUMN_COUNT).each do |col|
335 | pdf.grid([header_row_count, col], [last_hour_row, col]).bounding_box do
336 | pdf.dash [1, 2], phase: 2
337 | pdf.stroke_line(pdf.bounds.top_left, pdf.bounds.bottom_left)
338 | pdf.undash
339 | end
340 | end
341 | end
342 |
343 |
344 | def weekend_page pdf, saturday, sunday, tasks_by_wday, appointments_by_wday
345 | begin_new_page pdf, :left
346 |
347 | header_row_count = 2
348 | hour_row_count = HOUR_COUNT
349 | # TODO should have one constant for grid's number of rows to use here.
350 | # instead we'll just assume it's always 2x hours. We print a row per hour
351 | # and one blank line as a divider.
352 | task_row_count = 2 * HOUR_COUNT - hour_row_count - 1
353 | body_row_count = header_row_count + task_row_count + hour_row_count
354 |
355 | # Use a grid to do the math to divide the page into two columns:
356 | pdf.define_grid(columns: 2, rows: 1, column_gutter: 24, row_gutter: 0)
357 | first = pdf.grid(0,0)
358 | second = pdf.grid(0,1)
359 | # Then use that to build a bounding box for each column and redefine the grid in there.
360 | work_areas = [
361 | [saturday, first.top_left, { width: first.width, height: first.height }],
362 | [sunday, second.top_left, { width: second.width, height: second.height }]
363 | ].each do |date, point, options|
364 | pdf.bounding_box(point, options) do
365 | pdf.define_grid(columns: 2, rows: body_row_count, gutter: 0)
366 | # pdf.grid.show_all
367 |
368 | # Header
369 | left_header = I18n.l(date, format: :weekday)
370 | left_sub_header = I18n.l(date, format: :medium)
371 | pdf.grid([0, 0],[0, 1]).bounding_box do
372 | pdf.text left_header, heading_format(align: :left)
373 | end
374 | pdf.grid([1, 0],[1, 1]).bounding_box do
375 | pdf.text left_sub_header, subheading_format(align: :left)
376 | end
377 |
378 | task_start_row = header_row_count
379 | task_last_row = task_start_row + task_row_count - 1
380 |
381 | # Task lable
382 | pdf.grid([task_start_row, 0], [task_start_row, 1]).bounding_box do
383 | pdf.translate 6, 0 do
384 | pdf.text I18n.t('tasks'), color: DARK_COLOR, valign: :center
385 | end
386 | end
387 |
388 | # Horizontal lines
389 | (task_start_row..task_last_row).each do |row|
390 | pdf.grid([row, 0], [row, 1]).bounding_box do
391 | pdf.stroke_line pdf.bounds.bottom_left, pdf.bounds.bottom_right
392 | end
393 | end
394 |
395 | # Checkboxes
396 | checkbox_padding = 6
397 | ((task_start_row + 1)..task_last_row).each_with_index do |row, index|
398 | pdf.grid([row, 0], [row, 1]).bounding_box do
399 | draw_checkbox pdf, checkbox_padding, tasks_by_wday[date.wday][index]
400 | end
401 | end
402 |
403 | # Hour Grid
404 | hour_start_row = task_last_row + 1
405 | hour_last_row = hour_start_row + hour_row_count - 1
406 |
407 | # Horizontal Lines
408 | (hour_start_row..hour_last_row).each do |row|
409 | pdf.grid([row, 0], [row, 1]).bounding_box do
410 | pdf.stroke_line pdf.bounds.bottom_left, pdf.bounds.bottom_right
411 | end
412 | end
413 |
414 | # Vertical lines
415 | overhang = 24
416 | pdf.dash [1, 2]
417 | pdf.grid([hour_start_row + 1, 0], [hour_last_row, 0]).bounding_box do
418 | 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]])
419 | end
420 | # half plus change
421 | pdf.grid([hour_start_row + 1, 0], [hour_last_row, 0]).bounding_box do
422 | 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]])
423 | end
424 | pdf.grid([hour_start_row + 1, 1], [hour_last_row, 1]).bounding_box do
425 | pdf.stroke_line(pdf.bounds.top_right, pdf.bounds.bottom_right)
426 | end
427 | pdf.undash
428 |
429 | # Hour labels
430 | (0...HOUR_COUNT).each do |hour|
431 | row = hour + hour_start_row + 1
432 | if hour_label = HOUR_LABELS[hour]
433 | pdf.grid(row, -1).bounding_box do
434 | pdf.translate(20, 0) { pdf.text hour_label.to_s, align: :right, valign: :center }
435 | end
436 | end
437 |
438 | if appointment_label = appointments_by_wday[date.wday][HOUR_LABELS[hour]]
439 | pdf.grid([row, 0], [row, 2]).bounding_box do
440 | pdf.translate(overhang + 4, 0) {
441 | pdf.text appointment_label.to_s, color: MEDIUM_COLOR, align: :left, valign: :center
442 | }
443 | end
444 | end
445 | end
446 | end
447 | end
448 | end
449 |
450 |
451 | options = parse_options
452 | init_i18n(options[:locale])
453 | puts "#{options[:date_source]} Will save to #{FILE_NAME}"
454 | sunday = options[:date]
455 |
456 | tasks_by_wday = load_weekly_data_from_yaml(File.join(File.dirname(__FILE__), 'config', 'tasks.yaml'), 'task')
457 | appointments_by_wday = load_weekly_data_from_yaml(File.join(File.dirname(__FILE__), 'config', 'appointments.yaml'), 'appointments')
458 |
459 | pdf = init_pdf
460 |
461 | options[:weeks].times do |week|
462 | begin_new_page(pdf, :right) unless week.zero?
463 |
464 | monday = sunday.next_day(1)
465 | next_sunday = sunday.next_day(7)
466 |
467 | # Quarterly goals
468 | if sunday.month != next_sunday.month && (next_sunday.month % 3) == Q1_START_MONTH
469 | first = Date.new(next_sunday.year, next_sunday.month, 1)
470 | last = first.next_month(3).prev_day
471 | puts "Generating quarterly goals page for Q#{quarter(first)} #{date_range(first, last)}"
472 | quarter_ahead(pdf, first, last)
473 | end
474 |
475 | puts "Generating planner pages for #{date_range(monday, next_sunday)}"
476 |
477 | # Weekly goals
478 | week_ahead_page pdf, monday, next_sunday
479 |
480 | # Daily pages
481 | (1..5).each do |i|
482 | day = sunday.next_day(i)
483 | daily_tasks_page pdf, day, tasks_by_wday, appointments_by_wday
484 | daily_calendar_page pdf, day, appointments_by_wday
485 | end
486 |
487 | # Weekend page
488 | weekend_page pdf, sunday.next_day(6), next_sunday, tasks_by_wday, appointments_by_wday
489 |
490 | sunday = sunday.next_day(7)
491 | end
492 |
493 | pdf.render_file FILE_NAME
494 |
--------------------------------------------------------------------------------