Skip to content

Commit

Permalink
handle autosave of additional expenses
Browse files Browse the repository at this point in the history
form html/css tidy
  • Loading branch information
thejonroberts committed Sep 22, 2024
1 parent 403fabb commit 97bc3cf
Show file tree
Hide file tree
Showing 18 changed files with 417 additions and 99 deletions.
12 changes: 12 additions & 0 deletions app/assets/stylesheets/pages/case_contacts_form.scss
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@
border-bottom-right-radius: 0;
}

h2 {
font-size: 28px;
}

h3 {
font-size: medium;
}

legend {
font-size: medium;
}

.case-contacts-form {
&-buttons {
display: flex;
Expand Down
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
60 changes: 47 additions & 13 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,15 +29,45 @@ 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. */
add = (e) => {
add (e) {
super.add(e)
}

/* Creates a new record for the added item (before submission). */
addAndCreate = (e) => {
addAndCreate (e) {
this.add(e)
const items = this.element.querySelectorAll(this.wrapperSelectorValue)
const addedItem = items[items.length - 1]
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 @@ -99,33 +128,38 @@ export default class extends NestedForm {
}

/* Destroys a record when removing the item (before submission). */
destroyAndRemove = (e) => {
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
10 changes: 6 additions & 4 deletions app/javascript/controllers/case_contact_form_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import { Controller } from '@hotwired/stimulus'
// Connects to data-controller="case-contact-form"
export default class extends Controller {
static targets = [
'expenseAmount',
'expenseDescribe',
'expenseDestroy',
'milesDriven',
'volunteerAddress',
'reimbursementForm',
Expand All @@ -16,8 +15,10 @@ 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 records.
// if autosaved, nested form controller will remove destroy: true items
// if form submitted, it will be destroyed.
this.expenseDestroyTargets.forEach(el => (el.value = '1'))
}

clearMileage = () => {
Expand All @@ -28,6 +29,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
2 changes: 0 additions & 2 deletions app/models/case_contact.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,7 @@ def active_or_notes?
end

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

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
Loading

0 comments on commit 97bc3cf

Please sign in to comment.