├── 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 | --------------------------------------------------------------------------------