houdini/javascripts/src/components/edit_payment_pane/EditPaymentPane.tsx

435 lines
13 KiB
TypeScript

// License: LGPL-3.0-or-later
import * as React from 'react';
import {observer} from 'mobx-react';
import {InjectedIntlProps, injectIntl} from 'react-intl';
import {action, computed, observable, runInAction} from "mobx";
import {Field, FieldDefinition, Form} from "../../../../types/mobx-react-form";
import {HoudiniForm} from "../../lib/houdini_form";
import ProgressableButton from "../common/ProgressableButton";
import AriaModal = require('react-aria-modal');
import {centsToDollars, dollarsToCents, readableInterval, readableKind} from "../../lib/format";
import {NonprofitTimezonedDates, readable_date} from '../../lib/date';
import {UpdateDonationModel, PutDonation} from "../../lib/api/put_donation";
import {ApiManager} from "../../lib/api_manager";
import * as CustomAPIS from "../../lib/apis";
import {CSRFInterceptor} from "../../lib/csrf_interceptor";
import {BasicField, SelectField, TextareaField} from '../common/fields';
import ReactTextarea from "../common/form/ReactTextarea";
import {TwoColumnFields} from "../common/layout";
import {Validations} from "../../lib/vjf_rules";
import _ = require("lodash");
import {Dedication, parseDedication, serializeDedication} from '../../lib/dedication';
interface Charge {
status: string
}
interface RecurringDonation {
interval?: number
time_unit?: string
created_at: string
}
interface Donation {
designation?: string
comment?: string
event?: { id: number }
campaign?: { id: number }
dedication?: string
recurring_donation?: RecurringDonation
id:number
}
interface PaymentData {
gross_amount: number
fee_total: number
date: string
offsite_payment: OffsitePayment
donation: Donation
kind: string
id: string
refund_total: number
net_amount: number
origin_url?: string
charge?: Charge,
nonprofit: {id:number}
}
interface OffsitePayment {
check_number: string
kind: string
}
export interface EditPaymentPaneProps {
data: PaymentData
events: FundraiserInfo[]
campaigns: FundraiserInfo[]
onClose: () => void
modalActive: boolean
nonprofitTimezone?: string
preupdateDonationAction:() => void
postUpdateSuccess: () => void
}
export interface FundraiserInfo {
id: number
name: string
}
class EditPaymentPanelForm extends HoudiniForm {
}
@observer
class EditPaymentPane extends React.Component<EditPaymentPaneProps & InjectedIntlProps, {}> {
constructor(props: EditPaymentPaneProps & InjectedIntlProps) {
super(props);
this.putDonation = new ApiManager(CustomAPIS.APIS as Array<any>, CSRFInterceptor).get(PutDonation)
this.loadFormFromData()
}
@computed
get nonprofitTimezonedDates():NonprofitTimezonedDates {
return new NonprofitTimezonedDates(this.props.nonprofitTimezone)
}
@computed
get dedication(): Dedication|null {
return parseDedication(this.props.data.donation && this.props.data.donation.dedication)
}
putDonation : PutDonation
@action.bound
async updateDonation() {
if (this.props.preupdateDonationAction) {
this.props.preupdateDonationAction()
}
let updateData:UpdateDonationModel = {
id: Number(this.props.data.donation.id),
donation: {
designation: this.form.$('designation').value,
comment: this.form.$('comment').value,
campaign_id: this.form.$('campaign').value,
event_id: this.form.$('event').value,
gross_amount: dollarsToCents(this.form.$('gross_amount').get('value')),
fee_total: dollarsToCents(this.form.$('fee_total').get('value')),
date: this.form.$('date').get('value')
}
}
if (this.form.$('dedication.type').get('value') != '') {
updateData.donation.dedication = serializeDedication({
type: this.form.$('dedication.type').get('value'),
supporter_id: this.form.$('dedication.supporter_id').get('value'),
name: this.form.$('dedication.name').get('value'),
contact:this.form.$('dedication.contact').get('value'),
note: this.form.$('dedication.note').get('value')
})
}
else {
updateData.donation.dedication = ""
}
if (this.form.has('check_number'))
{
updateData.donation.check_number = this.form.$('check_number').value
}
await this.putDonation.putDonation(updateData, this.props.data.nonprofit.id)
if(this.props.postUpdateSuccess){
try {
this.props.postUpdateSuccess()
}
catch {}
}
this.props.onClose()
}
createDefinition<TInputType>(fieldDef:FieldDefinition<TInputType>) : FieldDefinition<TInputType> {
return fieldDef;
}
@action.bound
loadFormFromData() {
const eventId = this.props.data.donation.event && this.props.data.donation.event.id;
const campaignId = this.props.data.donation.campaign && this.props.data.donation.campaign.id;
let params: {[name:string]:FieldDefinition} = {
'event': {name: 'event', label: 'Event', value: eventId},
'campaign': {name: 'campaign', label: 'Campaign', value: campaignId},
'gross_amount': {name: 'gross_amount', label: 'Gross Amount', value: centsToDollars(this.props.data.gross_amount)},
'fee_total': {name: 'fee_total', label: 'Fees', value: centsToDollars(this.props.data.fee_total)},
'date': this.createDefinition({name: 'date', label: 'Date',
value: this.props.data.date,
input: (isoTime:string) => this.nonprofitTimezonedDates.readable_date(isoTime),
output:(date:string) => this.nonprofitTimezonedDates.readable_date_time_to_iso(date)}),
'dedication': {name: 'dedication', label: 'Dedication', fields: [
this.createDefinition({name:'type', label: 'Dedication Type', value: this.dedication.type}),
this.createDefinition({name: 'supporter_id', type: 'hidden', value: this.dedication.supporter_id}),
this.createDefinition({name:'name', label:'Person dedicated for', value: this.dedication.name}),
this.createDefinition({name: 'contact', type: 'hidden', value: this.dedication.contact}),
this.createDefinition({name: 'note', value: this.dedication.note})
]},
'designation': {name: 'designation', label: 'Designation', value: this.props.data.donation.designation},
'comment': {name: 'comment', label: 'Note', value: this.props.data.donation.comment}
};
if (this.props.data.kind == 'OffsitePayment') {
params.check_number = {name: 'check_number', label: 'Check Number', value: this.props.data.offsite_payment.check_number}
params.date.validators = [Validations.isDate('MM/DD/YYYY')]
params.gross_amount.validators = [Validations.isGreaterThanOrEqualTo(0.01)];
params.fee_total.validators = [Validations.optional(Validations.isLessThanOrEqualTo(0))];
}
return new EditPaymentPanelForm({fields: _.values(params)}, {
hooks: {
onSubmit: async (e: Field) => {
await this.updateDonation()
}
}
})
}
@computed get form(): EditPaymentPanelForm {
//add this.props because we need to reload on prop change
return this.props && this.loadFormFromData()
}
@computed get dateFormatter(): NonprofitTimezonedDates {
return new NonprofitTimezonedDates(this.props.nonprofitTimezone)
}
render() {
let rd = this.props.data.donation && this.props.data.donation.recurring_donation;
let initialTable = <table className='table--small u-marginBottom--10'>
<thead>
<tr>
<th>Payment Info</th>
<th/>
</tr>
</thead>
<tbody>
<tr>
<td>Date</td>
<td>
{this.dateFormatter.readable_date(this.props.data.date)}
</td>
</tr>
<tr>
<td>Type</td>
<td>
{readableKind(this.props.data.kind)}
{
this.props.data.offsite_payment && this.props.data.offsite_payment ?
<span>
&nbsp; ({this.props.data.offsite_payment.kind})
</span> : false
}
</td>
</tr>
{
this.props.data.kind === 'RecurringDonation' ?
<tr>
<td>Recurring</td>
<td>
{rd ? readableInterval(rd.interval, rd.time_unit) : false}
since
{rd ? this.dateFormatter.readable_date(rd.created_at) : false}
</td>
</tr> : false
}
<tr className='test-grossAmount'>
<td>Gross Amount</td>
<td>
${centsToDollars(this.props.data.gross_amount)}
</td>
</tr>
<tr>
<td>Processing Fees</td>
<td>
${centsToDollars(this.props.data.fee_total)}
</td>
</tr>
{
this.props.data.refund_total && this.props.data.refund_total > 0 ?
<tr>
<td>Total Refunds</td>
<td>
${centsToDollars(this.props.data.fee_total)}
</td>
</tr> : false
}
<tr>
<td>Net Amount</td>
<td>
${centsToDollars(this.props.data.net_amount)}
</td>
</tr>
{
this.props.data.origin_url ?
<tr>
<td>Origin</td>
<td>
<a target='_blank' href={this.props.data.origin_url}>
{this.props.data.origin_url}
</a>
</td>
</tr> : false
}
{
this.props.data.charge ?
<tr>
<td>Status</td>
<td>{this.props.data.charge.status}</td>
</tr> : false
}
{this.props.data.offsite_payment && this.props.data.offsite_payment.check_number ?
<tr>
<td>Check #</td>
<td>
{this.props.data.offsite_payment.check_number}
</td>
</tr> : false
}
<tr>
<td>ID</td>
<td>{this.props.data.id}</td>
</tr>
</tbody>
</table>;
let checkNumber = this.props.data.offsite_payment && this.props.data.offsite_payment.check_number ?
<BasicField field={this.form.$('check_number')} label={"Check or Payment Number/ID"}/> : false;
let offsitePayment = this.props.data.kind === "OffsitePayment" ? (<div>
<TwoColumnFields>
<BasicField field={this.form.$('gross_amount')} label={"Gross Amount"}/>
<BasicField field={this.form.$('fee_total')} label={"Processing Fees"}/>
</TwoColumnFields>
<BasicField field={this.form.$('date')} label={"Date"} />
{checkNumber}
</div>) : undefined;
const modal = this.props.modalActive ?
<AriaModal mounted={this.props.modalActive} titleText={'Edit Donation'} focusDialog={true}
onExit={this.props.onClose} dialogStyle={{minWidth:'768px'}}>
<header className='modal-header'>
<h4 className='modal-header-title'>Edit Donation</h4>
</header>
<div className="modal-body">
<div className={"tw-bs"}>
<div>
{initialTable}
<form className='u-marginTop--20'>
{offsitePayment}
<SelectField field={this.form.$('campaign')}
label={"Campaign"}
options={this.props.campaigns}/>
<SelectField field={this.form.$('event')}
label={"Event"}
options={this.props.events} />
<TextareaField field={this.form.$('designation')} label={"Designation"} rows={3} />
<div className="panel panel-default">
<div className="panel-heading"><label>Dedication <small> (optional)</small></label></div>
<div className="panel-body">
<SelectField field={this.form.$('dedication.type')} label={"Dedication Type"} options={[{id: null, name: ''}, {id: 'honor', name: 'In honor of'}, {id:'memory', name: 'In memory of'}]} />
{ this.form.$('dedication.type').get('value') != '' ? <div> <div className={"panel panel-default"}>
<div className="panel-heading"><label>Dedicated to:</label></div>
<div className={'panel-body'}>
<table className='table--small u-marginBottom--10'>
<tr>
<th>Name</th>
<td><input {...this.form.$('dedication.name').bind()}/></td>
</tr>
<tr>
<th > Supporter
ID</th>
<td>{this.dedication.supporter_id}<input {...this.form.$('dedication.supporter_id').bind()}/></td>
</tr>
</table>
</div>
</div>
<TextareaField rows={3} placeholder={"Dedication"} field={this.form.$('dedication.note')} label={"Dedication Note"}/></div>
: undefined }
</div>
</div>
<TextareaField field={this.form.$('comment')} label={"Notes"} rows={3} />
<ProgressableButton buttonText={'Save'}
buttonTextOnProgress={'Updating...'} className={'button'}
inProgress={this.form.submitting}
disabled={!this.form.isValid}
disableOnProgress={true} onClick={this.form.onSubmit}/>
</form>
</div>
</div>
</div>
</AriaModal> : false;
return (<div>{modal}</div>)
}
}
export default injectIntl(observer(EditPaymentPane))