gopromotional-event-planner 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +59 -0
- data/exe/gopromotional-event-planner +5 -0
- data/lib/gopromotional/event_planner/cli.rb +81 -0
- data/lib/gopromotional/event_planner/version.rb +7 -0
- data/lib/gopromotional/event_planner.rb +110 -0
- data/lib/gopromotional-event-planner.rb +1 -0
- metadata +53 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 669cde9f18ffe0247e676e6d896f66ab9a9912e078ee926eb4079158ffe5a33d
|
|
4
|
+
data.tar.gz: e2d25654ae355a5fad56fd3684abbddcea548d49c017ab46444f2f3a720aa3b1
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 670fa9c1725886ad2b9d571cf0442e45feeada7a8e12e41905eeee483a92ead68d02aae905d05b189a427b5ac6b45c63a94b6d3a54da62ef1bb50c5a45bb49c7
|
|
7
|
+
data.tar.gz: c3deaa384b0be635f4428603315420b2ffb3a5999a73c1297cd6f5254f1fbf7d99b52d7abcc60978a117596767bbdf9c6ff14b9383bf4d4f3100031d27918e7a
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GoPromotional
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# gopromotional-event-planner
|
|
2
|
+
|
|
3
|
+
Plan an event or trade show from the command line or your code: a countdown, a 12-step planning timeline with due dates, an estimate of how many giveaway items to order, and the latest order date for printed merchandise in working days.
|
|
4
|
+
|
|
5
|
+
- **Countdown** to the event in days, and in weeks + days.
|
|
6
|
+
- **Planning timeline**: 12 tasks from 8 weeks out to the post-event follow-up, each with a due date, days left and an overdue flag.
|
|
7
|
+
- **Giveaway quantity**: attendees × days × uptake × items per person, plus a buffer, rounded up.
|
|
8
|
+
- **Order-by date**: production + delivery + safety margin counted back in working days (Mon–Fri).
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
gem install gopromotional-event-planner
|
|
14
|
+
# or in a Gemfile:
|
|
15
|
+
gem "gopromotional-event-planner"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Command line
|
|
19
|
+
|
|
20
|
+
```console
|
|
21
|
+
$ gopromotional-event-planner countdown 2026-12-03 --today 2026-10-19
|
|
22
|
+
45 days to go (6 weeks 3 days) until 2026-12-03.
|
|
23
|
+
|
|
24
|
+
$ gopromotional-event-planner giveaways --attendees 500 --days 2 --uptake 0.6
|
|
25
|
+
Order 660 items (600 expected + 60 buffer).
|
|
26
|
+
|
|
27
|
+
$ gopromotional-event-planner order-by 2026-12-03 --today 2026-10-19
|
|
28
|
+
Order by 2026-11-12 (15 working days before the event), 24 days from today.
|
|
29
|
+
|
|
30
|
+
$ gopromotional-event-planner timeline 2026-12-03 --today 2026-10-19
|
|
31
|
+
! 2026-10-08 Confirm budget and attendee estimate (overdue)
|
|
32
|
+
! 2026-10-15 Book the venue (overdue)
|
|
33
|
+
2026-10-22 Order branded merchandise and giveaways
|
|
34
|
+
2026-10-29 Send invitations or open registration
|
|
35
|
+
2026-11-05 Confirm speakers and agenda
|
|
36
|
+
...
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Every command accepts `--json` for machine-readable output. Run `gopromotional-event-planner --help` for all options.
|
|
40
|
+
|
|
41
|
+
## Library
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
require "gopromotional/event_planner"
|
|
45
|
+
EP = GoPromotional::EventPlanner
|
|
46
|
+
|
|
47
|
+
EP.countdown("2026-12-03", today: "2026-10-19")[:days] # => 45
|
|
48
|
+
EP.giveaway_quantity(attendees: 500, days: 2) # => {base: 600, buffer: 60, total: 660}
|
|
49
|
+
EP.order_by_date("2026-12-03", today: "2026-10-19")[:order_by] # => "2026-11-12"
|
|
50
|
+
EP.timeline("2026-12-03", today: "2026-10-19").select { |r| r[:overdue] }
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Pure Ruby, no runtime dependencies, Ruby 3.0+. `require "gopromotional-event-planner"` also works.
|
|
54
|
+
|
|
55
|
+
## Links
|
|
56
|
+
|
|
57
|
+
- Website: [https://www.gopromotional.co.uk/](https://www.gopromotional.co.uk/)
|
|
58
|
+
- Source code: [https://github.com/nbkdevs/GoPromotional](https://github.com/nbkdevs/GoPromotional) (`packages/homepage/rubygems`)
|
|
59
|
+
- Licence: MIT
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "optparse"
|
|
5
|
+
require_relative "../event_planner"
|
|
6
|
+
|
|
7
|
+
module GoPromotional
|
|
8
|
+
module EventPlanner
|
|
9
|
+
# Command line interface: countdown | timeline | giveaways | order-by
|
|
10
|
+
module CLI
|
|
11
|
+
USAGE = <<~TXT
|
|
12
|
+
Usage: gopromotional-event-planner <command> [options]
|
|
13
|
+
|
|
14
|
+
Commands:
|
|
15
|
+
countdown <date> days until the event
|
|
16
|
+
timeline <date> planning checklist with due dates
|
|
17
|
+
giveaways --attendees N [--days N] [--uptake 0.6] [--per-person 1] [--buffer 0.1]
|
|
18
|
+
order-by <date> [--production 10] [--delivery 3] [--safety 2]
|
|
19
|
+
|
|
20
|
+
Options: --json, --today YYYY-MM-DD, -v/--version, -h/--help
|
|
21
|
+
TXT
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
def run(argv, out: $stdout, err: $stderr)
|
|
26
|
+
o = { json: false, today: Date.today.iso8601, days: 1.0, uptake: 0.6, per_person: 1.0, buffer: 0.1,
|
|
27
|
+
production: 10, delivery: 3, safety: 2 }
|
|
28
|
+
parser = OptionParser.new do |p|
|
|
29
|
+
p.banner = USAGE
|
|
30
|
+
p.on("--json") { o[:json] = true }
|
|
31
|
+
p.on("--today DATE") { |v| o[:today] = v }
|
|
32
|
+
p.on("--attendees N", Float) { |v| o[:attendees] = v }
|
|
33
|
+
p.on("--days N", Float) { |v| o[:days] = v }
|
|
34
|
+
p.on("--uptake N", Float) { |v| o[:uptake] = v }
|
|
35
|
+
p.on("--per-person N", Float) { |v| o[:per_person] = v }
|
|
36
|
+
p.on("--buffer N", Float) { |v| o[:buffer] = v }
|
|
37
|
+
p.on("--production N", Integer) { |v| o[:production] = v }
|
|
38
|
+
p.on("--delivery N", Integer) { |v| o[:delivery] = v }
|
|
39
|
+
p.on("--safety N", Integer) { |v| o[:safety] = v }
|
|
40
|
+
p.on("-v", "--version") { out.puts VERSION; return 0 }
|
|
41
|
+
p.on("-h", "--help") { out.puts USAGE; return 0 }
|
|
42
|
+
end
|
|
43
|
+
args = parser.parse(argv)
|
|
44
|
+
cmd = args.shift
|
|
45
|
+
need_date = -> { args.first or raise ArgumentError, "#{cmd} needs an event date (YYYY-MM-DD)" }
|
|
46
|
+
case cmd
|
|
47
|
+
when "countdown"
|
|
48
|
+
r = EventPlanner.countdown(need_date.call, today: o[:today])
|
|
49
|
+
text = case r[:status]
|
|
50
|
+
when "today" then "#{r[:event_date]} is today."
|
|
51
|
+
when "upcoming" then "#{r[:days]} days to go (#{r[:weeks]} weeks #{r[:remainder_days]} days) until #{r[:event_date]}."
|
|
52
|
+
else "#{r[:event_date]} was #{-r[:days]} days ago."
|
|
53
|
+
end
|
|
54
|
+
when "timeline"
|
|
55
|
+
r = EventPlanner.timeline(need_date.call, today: o[:today])
|
|
56
|
+
text = r.map { |x| "#{x[:overdue] ? '!' : ' '} #{x[:due]} #{x[:task]}#{x[:overdue] ? ' (overdue)' : ''}" }.join("\n")
|
|
57
|
+
when "giveaways"
|
|
58
|
+
raise ArgumentError, "giveaways needs --attendees N" unless o[:attendees]
|
|
59
|
+
r = EventPlanner.giveaway_quantity(attendees: o[:attendees], days: o[:days], uptake: o[:uptake],
|
|
60
|
+
per_person: o[:per_person], buffer: o[:buffer])
|
|
61
|
+
text = "Order #{r[:total]} items (#{r[:base]} expected + #{r[:buffer]} buffer)."
|
|
62
|
+
when "order-by"
|
|
63
|
+
r = EventPlanner.order_by_date(need_date.call, production_days: o[:production], delivery_days: o[:delivery],
|
|
64
|
+
safety_days: o[:safety], today: o[:today])
|
|
65
|
+
text = "Order by #{r[:order_by]} (#{r[:working_days]} working days before the event)"
|
|
66
|
+
text += r[:late] ? " - that is #{-r[:days_from_today]} days ago, so ask about express production." : ", #{r[:days_from_today]} days from today."
|
|
67
|
+
when nil
|
|
68
|
+
out.puts USAGE
|
|
69
|
+
return 0
|
|
70
|
+
else
|
|
71
|
+
raise ArgumentError, %(Unknown command "#{cmd}". Run with --help.)
|
|
72
|
+
end
|
|
73
|
+
out.puts(o[:json] ? JSON.pretty_generate(r) : text)
|
|
74
|
+
0
|
|
75
|
+
rescue ArgumentError, OptionParser::ParseError => e
|
|
76
|
+
err.puts "Error: #{e.message}"
|
|
77
|
+
2
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require_relative "event_planner/version"
|
|
5
|
+
|
|
6
|
+
module GoPromotional
|
|
7
|
+
# Event planning helpers: countdown, planning timeline, giveaway quantities and order-by dates.
|
|
8
|
+
module EventPlanner
|
|
9
|
+
TimelineItem = Struct.new(:offset_days, :category, :task, keyword_init: true)
|
|
10
|
+
|
|
11
|
+
TIMELINE_TEMPLATE = [
|
|
12
|
+
[56, "planning", "Confirm budget and attendee estimate"],
|
|
13
|
+
[49, "venue", "Book the venue"],
|
|
14
|
+
[42, "merchandise", "Order branded merchandise and giveaways"],
|
|
15
|
+
[35, "comms", "Send invitations or open registration"],
|
|
16
|
+
[28, "programme", "Confirm speakers and agenda"],
|
|
17
|
+
[21, "venue", "Arrange catering"],
|
|
18
|
+
[14, "merchandise", "Approve print proofs and signage"],
|
|
19
|
+
[7, "comms", "Send a reminder to attendees"],
|
|
20
|
+
[3, "logistics", "Confirm deliveries and the staffing rota"],
|
|
21
|
+
[1, "logistics", "Pack the event kit and giveaways"],
|
|
22
|
+
[0, "logistics", "Event day: set up the stand and registration"],
|
|
23
|
+
[-2, "comms", "Send thank-you and follow-up messages"]
|
|
24
|
+
].map { |o, c, t| TimelineItem.new(offset_days: o, category: c, task: t).freeze }.freeze
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
# Accept a Date or an ISO YYYY-MM-DD string.
|
|
29
|
+
def to_date(value)
|
|
30
|
+
return value if value.is_a?(Date)
|
|
31
|
+
s = value.to_s.strip
|
|
32
|
+
raise ArgumentError, %(Invalid date "#{value}" (expected YYYY-MM-DD)) unless s.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
33
|
+
begin
|
|
34
|
+
Date.iso8601(s)
|
|
35
|
+
rescue Date::Error
|
|
36
|
+
raise ArgumentError, %(Invalid calendar date "#{s}")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Days until an event, split into weeks and days.
|
|
41
|
+
def countdown(event_date, today: Date.today)
|
|
42
|
+
ev = to_date(event_date)
|
|
43
|
+
now = to_date(today)
|
|
44
|
+
days = (ev - now).to_i
|
|
45
|
+
{
|
|
46
|
+
event_date: ev.iso8601, today: now.iso8601, days: days,
|
|
47
|
+
weeks: days.abs / 7, remainder_days: days.abs % 7,
|
|
48
|
+
status: days.positive? ? "upcoming" : (days.zero? ? "today" : "past")
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Planning tasks with due dates, sorted by date. With today: each row gets :days_left and :overdue.
|
|
53
|
+
def timeline(event_date, today: nil, template: TIMELINE_TEMPLATE)
|
|
54
|
+
ev = to_date(event_date)
|
|
55
|
+
now = today && to_date(today)
|
|
56
|
+
template.map do |item|
|
|
57
|
+
due = ev - item.offset_days
|
|
58
|
+
row = { task: item.task, category: item.category, offset_days: item.offset_days, due: due.iso8601 }
|
|
59
|
+
if now
|
|
60
|
+
row[:days_left] = (due - now).to_i
|
|
61
|
+
row[:overdue] = due < now
|
|
62
|
+
end
|
|
63
|
+
row
|
|
64
|
+
end.sort_by { |r| r[:due] }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def ceil_clean(x)
|
|
68
|
+
((x * 1e6).round / 1e6).ceil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Giveaway items: attendees x days x uptake x per_person, plus a buffer (both rounded up).
|
|
72
|
+
def giveaway_quantity(attendees:, days: 1, uptake: 0.6, per_person: 1, buffer: 0.1)
|
|
73
|
+
{ attendees: attendees, days: days, uptake: uptake, per_person: per_person, buffer: buffer }.each do |name, v|
|
|
74
|
+
unless v.is_a?(Numeric) && v.finite? && v >= 0
|
|
75
|
+
raise ArgumentError, "#{name} must be a non-negative number"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
raise ArgumentError, "uptake must be between 0 and 1" if uptake > 1
|
|
79
|
+
base = ceil_clean(attendees * days * uptake * per_person)
|
|
80
|
+
extra = ceil_clean(base * buffer)
|
|
81
|
+
{ base: base, buffer: extra, total: base + extra }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Step back n working days (Monday to Friday).
|
|
85
|
+
def subtract_working_days(day, n)
|
|
86
|
+
d = to_date(day)
|
|
87
|
+
left = n
|
|
88
|
+
while left.positive?
|
|
89
|
+
d -= 1
|
|
90
|
+
left -= 1 if d.cwday <= 5
|
|
91
|
+
end
|
|
92
|
+
d
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Latest order date for printed merchandise: production + delivery + safety, in working days.
|
|
96
|
+
def order_by_date(event_date, production_days: 10, delivery_days: 3, safety_days: 2, today: nil)
|
|
97
|
+
{ production_days: production_days, delivery_days: delivery_days, safety_days: safety_days }.each do |name, v|
|
|
98
|
+
raise ArgumentError, "#{name} must be a non-negative integer" unless v.is_a?(Integer) && v >= 0
|
|
99
|
+
end
|
|
100
|
+
working = production_days + delivery_days + safety_days
|
|
101
|
+
by = subtract_working_days(event_date, working)
|
|
102
|
+
out = { order_by: by.iso8601, working_days: working }
|
|
103
|
+
if today
|
|
104
|
+
out[:days_from_today] = (by - to_date(today)).to_i
|
|
105
|
+
out[:late] = out[:days_from_today].negative?
|
|
106
|
+
end
|
|
107
|
+
out
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require_relative "gopromotional/event_planner"
|
metadata
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: gopromotional-event-planner
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- GoPromotional
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: 'Plan an event or trade show from the command line or your code: a countdown,
|
|
13
|
+
a 12-step planning timeline with due dates, an estimate of how many giveaway items
|
|
14
|
+
to order, and the latest order date for printed merchandise in working days.'
|
|
15
|
+
executables:
|
|
16
|
+
- gopromotional-event-planner
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- LICENSE
|
|
21
|
+
- README.md
|
|
22
|
+
- exe/gopromotional-event-planner
|
|
23
|
+
- lib/gopromotional-event-planner.rb
|
|
24
|
+
- lib/gopromotional/event_planner.rb
|
|
25
|
+
- lib/gopromotional/event_planner/cli.rb
|
|
26
|
+
- lib/gopromotional/event_planner/version.rb
|
|
27
|
+
homepage: https://www.gopromotional.co.uk/
|
|
28
|
+
licenses:
|
|
29
|
+
- MIT
|
|
30
|
+
metadata:
|
|
31
|
+
homepage_uri: https://www.gopromotional.co.uk/
|
|
32
|
+
source_code_uri: https://github.com/nbkdevs/GoPromotional/tree/main/packages/homepage/rubygems
|
|
33
|
+
bug_tracker_uri: https://github.com/nbkdevs/GoPromotional/issues
|
|
34
|
+
rubygems_mfa_required: 'true'
|
|
35
|
+
rdoc_options: []
|
|
36
|
+
require_paths:
|
|
37
|
+
- lib
|
|
38
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '3.0'
|
|
43
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
requirements: []
|
|
49
|
+
rubygems_version: 3.6.7
|
|
50
|
+
specification_version: 4
|
|
51
|
+
summary: Event countdown, planning timeline, giveaway quantities and print order-by
|
|
52
|
+
dates.
|
|
53
|
+
test_files: []
|