Skip to content

Commit

Permalink
handle autosave of additional expenses
Browse files Browse the repository at this point in the history
  • Loading branch information
thejonroberts committed Sep 21, 2024
1 parent 018f7bb commit 2745911
Show file tree
Hide file tree
Showing 16 changed files with 381 additions and 69 deletions.
32 changes: 32 additions & 0 deletions app/controllers/additional_expenses_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
class AdditionalExpensesController < ApplicationController
def create
@additional_expense = AdditionalExpense.new(additional_expense_params)
authorize @additional_expense

respond_to do |format|
if @additional_expense.save
format.json { render json: @additional_expense, status: :created }
else
format.json { render json: @additional_expense.errors, status: :unprocessable_entity }
end
end
end

def destroy
@additional_expense = AdditionalExpense.find(params[:id])
authorize @additional_expense

@additional_expense.destroy!

respond_to do |format|
format.json { head :no_content }
end
end

private

def additional_expense_params
params.require(:additional_expense)
.permit(:case_contact_id, :other_expense_amount, :other_expenses_describe)
end
end
2 changes: 1 addition & 1 deletion app/controllers/case_contacts/form_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def update
if @case_contact.update(case_contact_params)
render json: @case_contact, status: :ok
else
render json: @case_contact.errors, status: :unprocessable_entity
render json: @case_contact.errors.full_messages, status: :unprocessable_entity
end
end
end
Expand Down
23 changes: 20 additions & 3 deletions app/javascript/controllers/autosave_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,29 @@ export default class extends Controller {
}).then(response => {
if (response.ok) {
this.goodAlert()
const event = new CustomEvent('autosave:success', { bubbles: true }) // eslint-disable-line no-undef
this.element.dispatchEvent(event)
} else {
if (response.status === 504) {
return Promise.reject(response)
}
}).catch(error => {
console.error(error.status, error.statusText)
switch (error.status) {
case 504:
this.badAlert('Connection lost: Changes will be saved when connection is restored.')
} else {
break
case 422:
error.json().then(errorJson => {
console.error('errorJson', errorJson)
const errorMessage = errorJson.join('. ')
this.badAlert(`Unable to save: ${errorMessage}`)
})
break
case 401:
this.badAlert('You must be signed in to save changes.')
break
default:
this.badAlert('Error: Unable to save changes.')
}
}
})
}
Expand Down
54 changes: 44 additions & 10 deletions app/javascript/controllers/casa_nested_form_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import NestedForm from '@stimulus-components/rails-nested-form'

// Connects to data-controller="casa-nested-form"
// Allows nested forms to be used with autosave controller, adding and deleting records
// so that autosave updates do not create/destroy nested records multiple times.
// so that autosave does not create/destroy nested records multiple times.
// add() & remove() are kept standard, can be used without autosave just fine:
// no values are necessary in that case.
// Created for the CaseContact form, see its usage there.
// Created for the CaseContact form (details), see usage there.
export default class extends NestedForm {
static values = {
route: String, // path to create/destroy a record, e.g. "/contact_topic_answers"
Expand All @@ -29,6 +29,36 @@ export default class extends NestedForm {
headers.append('X-CSRF-Token', tokenTag.content)
}
this.headers = headers

document.addEventListener('autosave:success', (e) => {
this.onAutosaveSuccess(e)
})
}

getRecordId (wrapper) {
const recordInput = wrapper.querySelector('input[name*="id"]')
if (!recordInput) {
console.warn('id input not found for nested item:', wrapper)
return ''
}
return recordInput.value
}

/* removes any items that have been marked destroy: true on autosave:success */
/* can be marked for destroy elsewhere, as in case_contact_form_controller hide reimbursement */
onAutosaveSuccess (_e) {
const wrappers = this.element.querySelectorAll(this.wrapperSelectorValue)
wrappers.forEach(wrapper => {
const destroyInput = wrapper.querySelector("input[name*='_destroy']")
if (!destroyInput) {
console.warn('Destroy input not found for nested item:', wrapper)
return
}
if (destroyInput.value === '1') {
// autosave has already destroyed the record, remove the element from DOM
wrapper.remove()
}
})
}

/* Adds item to the form. Item will not be created until form submission. */
Expand Down Expand Up @@ -83,7 +113,6 @@ export default class extends NestedForm {
const idField = document.querySelector(`#${idAttr}`)
idField.setAttribute('value', data.id)
addedItem.dataset.newRecord = false
addedItem.dataset.recordId = data.id
})
.catch(error => {
console.error(error.status, error.statusText)
Expand All @@ -101,31 +130,36 @@ export default class extends NestedForm {
/* Destroys a record when removing the item (before submission). */
destroyAndRemove = (e) => {
const wrapper = e.target.closest(this.wrapperSelectorValue)
const recordId = wrapper.dataset.recordId
const recordId = this.getRecordId(wrapper)
if (wrapper.dataset.newRecord === 'false' && (recordId.length > 0)) {
fetch(`${this.routeValue}/${recordId}`, {
method: 'DELETE',
headers: this.headers
})
.then(response => {
if (response.ok) {
// deleted from db; update as non-persisted, remove as usual.
// destroy successful; remove as if new record
wrapper.dataset.newRecord = true
wrapper.dataset.recordId = ''
this.remove(e)
} else {
return Promise.reject(response)
}
})
.catch(error => {
console.error(error.status, error.statusText)
error.json().then(errorJson => {
console.error('errorJson', errorJson)
})
if (error.status === 404) {
// NOT FOUND: already deleted -> remove as if new record
wrapper.dataset.newRecord = true
this.remove(e)
} else {
error.json().then(errorJson => {
console.error('errorJson', errorJson)
})
}
})
} else {
console.warn(
'Not enough information to destroy record:', {
'Conflicting information while trying to destroy record:', {
wrapperDatasetNewRecord: wrapper.dataset.newRecord,
recordId
}
Expand Down
7 changes: 5 additions & 2 deletions app/javascript/controllers/case_contact_form_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export default class extends Controller {
static targets = [
'expenseAmount',
'expenseDescribe',
'expenseDestroy',
'milesDriven',
'volunteerAddress',
'reimbursementForm',
Expand All @@ -16,8 +17,9 @@ export default class extends Controller {
}

clearExpenses = () => {
this.expenseDescribeTargets.forEach(el => (el.value = ''))
this.expenseAmountTargets.forEach(el => (el.value = ''))
// mark for destruction. autosave has already created the record.
// if submitted, it will be destroyed. if autosaved, it will be removed by nested form controller.
this.expenseDestroyTargets.forEach(el => (el.value = '1'))
}

clearMileage = () => {
Expand All @@ -28,6 +30,7 @@ export default class extends Controller {
setReimbursementFormVisibility = () => {
if (this.wantDrivingReimbursementTarget.checked) {
this.reimbursementFormTarget.classList.remove('d-none')
this.expenseDestroyTargets.forEach(el => (el.value = '0'))
} else {
this.clearExpenses()
this.clearMileage()
Expand Down
12 changes: 10 additions & 2 deletions app/models/additional_expense.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
class AdditionalExpense < ApplicationRecord
belongs_to :case_contact
has_one :casa_case, through: :case_contact
has_one :casa_org, through: :casa_case

# validates :other_expense_amount, presence: true
validates :other_expenses_describe, presence: {message: "Expense description cannot be blank."}
validates :other_expenses_describe, presence: true, if: :describe_required?

alias_attribute :amount, :other_expense_amount
alias_attribute :describe, :other_expenses_describe

def describe_required?
other_expense_amount&.positive?
end
end

# == Schema Information
Expand Down
4 changes: 2 additions & 2 deletions app/models/case_contact.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ def active_or_notes?
notes? || active?
end

accepts_nested_attributes_for :additional_expenses, reject_if: :all_blank, allow_destroy: true
validates_associated :additional_expenses
accepts_nested_attributes_for :additional_expenses, allow_destroy: true, reject_if: :all_blank

# TODO: is this used?
accepts_nested_attributes_for :casa_case
accepts_nested_attributes_for :contact_topic_answers, allow_destroy: true,
reject_if: proc { |attrs| attrs["contact_topic_id"].blank? && attrs["value"].blank? } # .notes sent without topic_id, but must have a value.
Expand Down
36 changes: 36 additions & 0 deletions app/policies/additional_expense_policy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
class AdditionalExpensePolicy < ApplicationPolicy
class Scope < ApplicationPolicy::Scope
def resolve
case user
when CasaAdmin, Supervisor
scope.joins([:case_contact, :casa_case]).where(casa_case: {casa_org_id: user.casa_org.id})
when Volunteer
scope.where(case_contact: user.case_contacts)
else
scope.none
end
end
end

def create?
case user
when Volunteer
user.case_contacts.include?(record.case_contact)
when CasaAdmin, Supervisor
same_org?
else
false
end
end

def destroy?
case user
when Volunteer
user.case_contacts.include?(record.case_contact)
when CasaAdmin, Supervisor
same_org?
else
false
end
end
end
3 changes: 1 addition & 2 deletions app/views/case_contacts/form/_contact_topic_answer.html.erb
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
<div class="nested-form-wrapper"
data-new-record="<%= form.object.new_record? %>"
data-casa-nested-form-target="wrapper"
data-child-index="<%= form.options[:child_index] %>"
data-record-id="<%= form.object.id || "" %>">
data-child-index="<%= form.options[:child_index] %>">
<div class="d-flex gap-1 flex-wrap">
<div class="col-md-4">
<%= form.label :contact_topic_id, "Discussion Topic" %>
Expand Down
10 changes: 8 additions & 2 deletions app/views/case_contacts/form/details.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,11 @@
<!-- ADDITIONAL EXPENSES -->
<div id="contact-form-expenses"
class="other-expenses"
data-controller="casa-nested-form">
data-controller="casa-nested-form"
data-casa-nested-form-route-value="/additional_expenses"
data-casa-nested-form-parent-name-value="case_contact"
data-casa-nested-form-parent-id-value="<%= @case_contact.id %>"
data-casa-nested-form-model-name-value="additional_expense">

<template data-casa-nested-form-target="template">
<%= form.fields_for :additional_expenses, AdditionalExpense.new, child_index: "NEW_RECORD" do |expense_fields| %>
Expand All @@ -240,7 +244,9 @@

<div data-casa-nested-form-target="target"></div>

<button type="button" class="btn btn-link" data-action="casa-nested-form#add">
<!--<button type="button" class="btn btn-link" data-action="casa-nested-form#add">-->
<button type="button" class="btn btn-link"
data-action="casa-nested-form#addAndCreate">
+ Add Expense
</button>
</div>
Expand Down
11 changes: 8 additions & 3 deletions app/views/shared/_additional_expense_form.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<div class="nested-form-wrapper" data-new-record="<%= form.object.new_record? %>">
<div class="nested-form-wrapper"
data-new-record="<%= form.object.new_record? %>"
data-casa-nested-form-target="wrapper"
data-child-index="<%= form.options[:child_index] %>">
<div class="d-flex gap-1">
<div class="col-md-4">
<%= form.label :other_expense_amount, "Expense amount" %>
Expand All @@ -25,11 +28,13 @@
<div class="flex-shrink-1">
<button type="button"
class="remove-expense-button mt-4 btn btn-sm danger-btn-outline btn-hover"
data-action="casa-nested-form#remove">
data-action="casa-nested-form#destroyAndRemove">
<!--data-action="casa-nested-form#remove">-->
Remove
</button>
</div>

<%= form.hidden_field :_destroy %>
<%= form.hidden_field :id, value: form.object.id %>
<%= form.hidden_field :_destroy, data: {case_contact_form_target: "expenseDestroy"} %>
</div>
</div>
6 changes: 6 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ en:
occurred_at: Date
contact_types: Contact Type(s)
case_contact_contact_types: Contact Type(s)
case_contact/additional_expenses:
other_expenses_describe: Expense description
other_expenses_amount: Expense amount
additional_expense:
other_expenses_describe: Description
other_expenses_amount: Amount
errors:
messages:
cant_be_future: can't be in the future
Expand Down
3 changes: 3 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@
end
resources :case_court_orders, only: %i[destroy]

resources :additional_expenses, only: %i[create destroy],
constraints: lambda { |req| req.format == :json }

namespace :all_casa_admins do
resources :casa_orgs, only: [:new, :create, :show] do
resources :casa_admins, only: [:new, :create, :edit, :update] do
Expand Down
Loading

0 comments on commit 2745911

Please sign in to comment.