Initial Commit with basic functionality

This commit is contained in:
ricky committed 2026-08-06 13:12:19 -04:00
commit 790cefdcfe
22 files changed
+947

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
.bundle
.config
.dockerrc
.vscode
Gemfile.lock
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2026 Rick Barrette
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+100
View File
@@ -0,0 +1,100 @@
# Redmine QuickBooks Calendar
A plugin for Redmine that extends the functionality of the [Redmine QuickBooks Online](https://github.com/rickbarrette/redmine_qbo) plugin.
This plugin allows for adding customer appointment capability to the Redmine calendar.
---
## Requirements
* **Redmine:** 6.1+
* **Parent Plugin:** [Redmine QuickBooks Online](https://github.com/rickbarrette/redmine_qbo) (must be installed and configured)
---
## Compatibility
| Plugin Version | Redmine Version | Parent Plugin Version |
| --- | --- | --- |
| 2026.8.0+ | 7.0.x | 2026.3.9+ |
---
## Features
Add appointments to the redmine calanedar with the abiltiy to attach a QBO customer, QBO Vehicle, QBO Estimate, ect.
---
## Installation
1. **Clone the plugin**
Navigate to your Redmine plugins directory:
```bash
cd /path/to/redmine/plugins
git clone https://github.com/rickbarrette/redmine_qbo_calendar.git
cd redmine_qbo_calendar
# Optional: checkout a specific version
git checkout <tag>
```
2. **Install dependencies**
```bash
bundle install
```
3. **Migrate your database**
```bash
bundle exec rake redmine:plugins:migrate RAILS_ENV=production
```
4. **Restart Redmine**
Restart your application server (Puma, Passenger, etc.) to initialize the plugin hooks.
---
## Configuration
This plugin depends on the **Redmine QuickBooks Online** plugin.
Before using this plugin:
1. Install and configure the parent plugin.
2. Ensure your **QuickBooks Online** company file is connected.
3. Sync Accounts & Items via plugin settings
4. Set default income account for new items via plugin settings
---
## Usage
Add appointments
---
## License
> The MIT License (MIT)
>
> Copyright (c) 2026 Rick Barrette
>
> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,101 @@
#The MIT License (MIT)
#
#Copyright (c) 2026 rick barrette
#
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO CustomerAppointment SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
class CustomerAppointmentsController < ApplicationController
before_action :require_login
before_action :find_project, except: [:index]
before_action :find_appointment, only: [ :show, :edit, :update, :destroy ]
#before_action :authorize
helper :projects
def index
if params[:project_id]
@project = Project.find(params[:project_id])
@appointments = CustomerAppointment.where(project: @project).order(:start_date)
else
@appointments = CustomerAppointment.order(:start_date)
end
end
def show
@appointment = CustomerAppointment.find(params[:id])
end
def new
@appointment = CustomerAppointment.new
@appointment.project = @project
end
def create
@appointment = CustomerAppointment.new(appointment_params)
@appointment.project = @project
if @appointment.save
flash[:notice] = l(:notice_successful_create)
redirect_to customer_appointment_path( @appointment )
else
render :new, status: :unprocessable_entity
end
end
def edit
end
def update
if @appointment.update( appointment_params )
flash[:notice] = l(:notice_successful_update)
redirect_to customer_appointment_path( @appointment )
else
render :edit, status: :unprocessable_entity
end
end
def destroy
@appointment.destroy
flash[:notice] = l(:notice_successful_delete)
redirect_to project_customer_appointments_path( @project)
end
private
def appointment_params
params
.require(:customer_appointment)
.permit(
:subject,
:customer_id,
:phone,
:vehicle_id,
:description,
:status,
:user_id,
:start_date,
:end_date,
:estimated_hours,
:estimate_id,
)
end
def find_project
@project = Project.find(params[:project_id]) if params[:project_id].present?
end
def find_appointment
@appointment = CustomerAppointment.find(params[:id])
end
def log(msg)
Rails.logger.info "[CustomerAppointmentsController] #{msg}"
end
end
@@ -0,0 +1,39 @@
#The MIT License (MIT)
#
#Copyright (c) 2026 rick barrette
#
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO CustomerAppointment SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module CustomerAppointmentsHelper
def appointment_status_color(status)
case status
when "Scheduled"
"#4CAF50"
when "CheckedIn"
"#2196F3"
when "InProgress"
"#FF9800"
when "Ready"
"#8BC34A"
when "Completed"
"#757575"
else
"#CCCCCC"
end
end
end
+55
View File
@@ -0,0 +1,55 @@
#The MIT License (MIT)
#
#Copyright (c) 2026 rick barrette
#
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
class CustomerAppointment < ActiveRecord::Base
belongs_to :project, optional: true
belongs_to :user, optional: true
belongs_to :customer, optional: true
belongs_to :vehicle, optional: true
belongs_to :estimate, optional: true
validates :subject, presence: true
validates :start_date, presence: true
validate :date_order
STATUSES = %w[
Scheduled
CheckedIn
InProgress
Ready
Completed
Cancelled
]
def due_date
end_date&.to_date
end
def self.visible(user)
return CustomerAppointment.all
end
private
def date_order
return if end_date.blank?
return if start_date.blank?
if end_date < start_date
errors.add( :end_date, "cannot be before start date")
end
end
def log(msg)
Rails.logger.info "[Event] #{msg}"
end
end
+105
View File
@@ -0,0 +1,105 @@
<h1>THIS IS OSOR!!!</h1>
<%= form_tag({}, data: { cm_url: issues_context_menu_path }) do %>
<%= hidden_field_tag 'back_url', url_for(params: request.query_parameters), id: nil %>
<ul class="cal">
<li scope="col" title="<%= l(:label_week) %>" class="calhead week-number"></li>
<% 7.times do |i| %>
<li scope="col" class="calhead">
<%= day_name((calendar.first_wday + i) % 7) %>
</li>
<% end %>
<% calendar.format_month.each_slice(7) do |week| %>
<li class="week-number">
<span class="label-week"><%= l(:label_week) %></span>
<%= calendar.week_number(week.first) %>
</li>
<% week.each do |day| %>
<li class="<%= calendar.day_css_classes(day) %> calbody">
<p class="day-num">
<span class="day-value"><%= day.day %></span>
<span class="abbr-day">(<%= abbr_day_name(day.cwday) %>)</span>
</p>
<% calendar.events_on(day).each do |event| %>
<% if event.is_a?(Issue) %>
<% starting = (day == event.start_date) %>
<% ending = (day == event.due_date) %>
<%= tag.div class: [
event.css_classes,
'tooltip',
'hascontextmenu',
('starting' if starting),
('ending' if ending)
].compact do %>
<%= "#{event.project} -" unless @project && @project == event.project %>
<%= sprite_icon('bullet-go', rtl: true) if starting && !ending %>
<%= sprite_icon('bullet-end', rtl: true) if ending && !starting %>
<%= sprite_icon('bullet-go-end') if starting && ending %>
<%= link_to_issue(event, truncate: 30) %>
<span class="tip">
<%= render_issue_tooltip(event) %>
</span>
<%= check_box_tag 'ids[]',
event.id,
false,
style: 'display:none;',
id: nil %>
<% end %>
<% elsif event.is_a?(Version) %>
<span class="icon icon-package">
<%= sprite_icon 'package' %>
<%= "#{event.project} -" unless @project && @project == event.project %>
<%= link_to_version(event) %>
</span>
<% elsif event.is_a?(CustomerAppointment) %>
<%= render partial: 'customer_appointments/calendar_event',
locals: {
appointment: event,
day: day,
project: @project
} %>
<% else %>
<% Rails.logger.warn("Unknown calendar event: #{event.class}") %>
<% end %>
<% end %>
</li>
<% end %>
<% end %>
</ul>
<% end %>
<%= context_menu %>
@@ -0,0 +1,35 @@
<%= tag.div class: [
'calendar-appointment',
'tooltip'
] do %>
<% if appointment.user %>
<%= avatar(
appointment.user,
size: 20,
class: 'appointment-avatar'
) %>
<% end %>
<span >
<%= link_to appointment.subject,
customer_appointment_path(appointment) %>
</span>
<span class="tip">
<strong><%= appointment.subject %></strong><br>
Customer:
<%= appointment.customer&.name %><br>
Vehicle:
<%= appointment.vehicle&.name %><br>
Technician:
<%= appointment.user&.name %><br>
Hours:
<%= appointment.estimated_hours %>
</span>
<% end %>
@@ -0,0 +1,79 @@
<%= labelled_form_for @appointment do |f| %>
<p>
<%= f.text_field :subject %>
</p>
<p>
<%= f.text_field :customer, class: "customer-name", autocomplete: "off", data: { autocomplete_url: "/customers/autocomplete"} %>
<%= f.hidden_field :customer_id, id: "customer_id", onchange: "this.form.requestSubmit()" %>
</p>
<p>
<p>
<%= f.collection_select( :vehicle_id, @appointment.customer&.vehicles || [], :id, :name, include_blank: true ) %>
</p>
<p>
<%= f.collection_select( :estimate_id, @appointment.customer&.estimates || [], :id, :name, include_blank: true ) %>
</p>
<p>
<%= f.collection_select( :user_id, User.active.sorted,:id,:name,include_blank: true ) %>
</p>
<p>
<%= f.date_field :start_date %>
</p>
<p>
<%= f.date_field :end_date %>
</p>
<p>
<%= f.number_field :estimated_hours, step: 0.1 %>
</p>
<p>
<%= f.number_field :duration, step: 1 %>
</p>
<p>
<%= f.select(:status, CustomerAppointment::STATUSES) %>
</p>
<p>
<%= f.text_area :description,
rows: 8 %>
</p>
<p>
<%= submit_tag l(:button_save) %>
</p>
<% end %>
Whitespace-only changes.
@@ -0,0 +1,3 @@
<h2>Edit Appointment</h2>
<%= render "form" %>
@@ -0,0 +1,89 @@
<h2>Customer Appointments</h2>
<p>
<%= link_to(
l(:button_add),
new_project_event_path(@project)
) %>
</p>
<table class="list">
<thead>
<tr>
<th>Customer</th>
<th>Vehicle</th>
<th>Appointment</th>
<th>Assigned</th>
<th>Date</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<% @appointments.each do |appointment| %>
<tr>
<td>
<%= link_to(
appointment.customer_name,
customer_appointment_path(
appointment
)
) %>
</td>
<td>
<%= appointment.vehicle %>
</td>
<td>
<%= appointment.subject %>
</td>
<td>
<%= appointment.user&.name %>
</td>
<td>
<%= format_date(
appointment.start_date
) %>
</td>
<td>
<%= appointment.status %>
</td>
</tr>
<% end %>
</tbody>
</table>
@@ -0,0 +1,3 @@
<h2>New Appointment</h2>
<%= render "form" %>
@@ -0,0 +1,53 @@
<h2><%= @appointment.subject %></h2>
<table class="attributes">
<tr>
<th><%= t(:customer) %></th>
<td><%= link_to @appointment&.customer %></td>
</tr>
<tr>
<th><%= t(:vehicle) %></th>
<td><%= link_to@appointment&.vehicle %></td>
</tr>
<tr>
<th><%= t(:primary_phone) %></th>
<td><%= @appointment&.customer&.primary_phone %></td>
</tr>
<tr>
<th><%= t(:technician) %></th>
<td><%= link_to @appointment.user %></td>
</tr>
<tr>
<th><%= t(:status) %></th>
<td><%= @appointment.status %></td>
</tr>
<tr>
<th><%= t(:estimate) %></th>
<td><%= link_to @appointment.estimate unless @appointment.estimate.nil? %></td>
</tr>
<tr>
<th><%= t(:duration) %></th>
<td><%= @appointment.duration %> <%= t(:days) %></td>
</tr>
<tr>
<th><%= t(:estimated_hours) %></th>
<td><%= @appointment.estimated_hours %></td>
</tr>
<tr>
<th><%= t(:description) %></th>
<td><%= simple_format(@appointment.description) %></td>
</tr>
</table>
<%= link_to t(:edit), edit_customer_appointment_path(@appointment), class: "icon icon-edit" %>
<%= link_to t(:delete), customer_appointment_path(@appointment), method: :delete, data: { confirm: t(:are_you_sure) }, class: "icon icon-del" %>
@@ -0,0 +1,20 @@
.customer-appointment {
background: #4caf50;
color: white;
border-radius: 4px;
padding: 2px 4px;
margin-bottom: 2px;
}
.customer-appointment a {
color: white;
text-decoration: none;
}
.customer-appointment.starting {
border-left: 3px solid #1b5e20;
}
.customer-appointment.ending {
border-right: 3px solid #1b5e20;
}
+24
View File
@@ -0,0 +1,24 @@
#The MIT License (MIT)
#
#Copyright (c) 2026 rick barrette
#
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# English strings go here for Rails i18n
# Usage I18n.t(:label)
en:
label_event: "Event"
permission_view_customer_appointments: View Customer Appointments
permission_manage_customer_appointments: Manage Customer Appointments
label_customer_calendar: Customer Calendar
label_customer_appointment: Customer Appointment
field_customer_name: Customer
field_vehicle: Vehicle
field_phone: Phone
field_license_plate: License Plate
are_you_sure: "Are you sure?"
+15
View File
@@ -0,0 +1,15 @@
#The MIT License (MIT)
#
#Copyright (c) 2026 rick barrette
#
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
RedmineApp::Application.routes.draw do
resources :customer_appointments
end
@@ -0,0 +1,37 @@
#The MIT License (MIT)
#
#Copyright (c) 2026 rick barrette
#
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
class CreateCustomerAppointments < ActiveRecord::Migration[7.0]
def change
create_table :customer_appointments do |t|
t.text :subject, null: false
t.text :description, null: false
t.datetime :start_date, null: false
t.datetime :end_date
t.integer :duration, default: 1
t.integer :estimated_hours, default: 1
t.string :event_type
t.string :status
t.references :project
t.references :user
t.references :customer
t.references :vehicle
t.references :estimate
end
add_index :customer_appointments, :start_date
end
end
+61
View File
@@ -0,0 +1,61 @@
#The MIT License (MIT)
#
#Copyright (c) 2026 rick barrette
#
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Redmine::Plugin.register :redmine_qbo_calendar do
# About
name 'Redmine QBO Calendar plugin'
author 'Rick Barrette'
description 'A plugin for Redmine to extend the capabilitys of the Redmine QuickBooks Online plugin to create customer appointments for the Redmine Calendar'
version '2026.8.0'
url 'https://github.com/rickbarrette/redmine_qbo_calendar'
author_url 'https://barrettefabrication.com'
requires_redmine version_or_higher: '7.0.0'
settings partial: 'calendar/settings'
# Ensure redmine_qbo is installed
begin
requires_redmine_plugin :redmine_qbo, version_or_higher: '2026.3.9'
rescue Redmine::PluginNotFound
raise 'Please install the redmine_qbo plugin (https://github.com/rickbarrette/redmine_qbo)'
end
project_module :customer_calendar do
permission :view_customer_appointments,
{ customer_appointments: [:index, :show] }
permission :manage_customer_appointments,
{
customer_appointments: [
:new,
:create,
:edit,
:update,
:destroy
]
}
end
menu :project_menu,
:customer_calendar,
{ controller: 'events',
action: 'index' },
caption: 'Customer Calendar',
after: :calendar,
param: :project_id
end
Rails.configuration.to_prepare do
ApplicationController.prepend_view_path(
File.expand_path("app/views", __dir__)
)
end
RedmineQboCalendar.setup
+16
View File
@@ -0,0 +1,16 @@
#The MIT License (MIT)
#
#Copyright (c) 2026 rick barrette
#
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module RedmineQboCalendar
def self.setup
CalendarsController.prepend(RedmineQboCalendar::Patches::CalendarsControllerPatch)
RedmineQboCalendar::Hooks::ViewHookListener
end
end
@@ -0,0 +1,26 @@
#The MIT License (MIT)
#
#Copyright (c) 2026 rick barrette
#
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module RedmineQboCalendar
module Hooks
class ViewHookListener < Redmine::Hook::ViewListener
# Load the javascript to support the autocomplete forms
def view_layouts_base_html_head(context = {})
safe_join([
# javascript_include_tag("redmine_qbo_calendar", plugin: :redmine_qbo_calendar),
stylesheet_link_tag('customer_appoitments', plugin: :redmine_qbo_calendar)
])
end
end
end
end
@@ -0,0 +1,60 @@
#The MIT License (MIT)
#
#Copyright (c) 2026 rick barrette
#
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module RedmineQboCalendar
module Patches
module CalendarsControllerPatch
def show
log "show"
super
return unless @calendar
# Grab whatever Redmine already loaded
events = @calendar.instance_variable_get(:@events).dup
log "events: #{events.inspect}"
appointments = CustomerAppointment.visible(User.current)
log "appointments: #{appointments.inspect}"
# If we're inside a project, only show that project's appointments
# if @project
# appointments = appointments.where(project_id: @project.id)
# end
# Only appointments visible in the displayed month
# appointments = appointments.where(
# "(start_date BETWEEN :start AND :finish)
# OR
# (end_date BETWEEN :start AND :finish)
# OR
# (start_date <= :start AND end_date >= :finish)",
# start: @calendar.startdt.beginning_of_day,
# finish: @calendar.enddt.end_of_day
# )
events.concat(appointments)
# Rebuild Redmine's internal event maps
@calendar.events = events
end
private
def log(msg)
Rails.logger.info "[CalendarsControllerPatch] #{msg}"
end
end
end
end