spaceport/webclient/src/Transactions.js

438 lines
13 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect, useRef } from 'react';
import { Link, useParams } from 'react-router-dom';
import moment from 'moment-timezone';
import ReactToPrint from 'react-to-print';
import './light.css';
import { Button, Container, Form, Grid, Header, Message, Segment, Table } from 'semantic-ui-react';
import { MembersDropdown } from './Members.js';
import { isAdmin, BasicTable, requester, useIsMobile } from './utils.js';
import { NotFound } from './Misc.js';
export function TransactionEditor(props) {
const { token, input, setInput, error } = props;
const [prevInput] = useState(input);
const handleValues = (e, v) => setInput({ ...input, [v.name]: v.value });
const handleChange = (e) => handleValues(e, e.currentTarget);
const makeProps = (name) => ({
name: name,
onChange: handleChange,
value: input[name] || '',
error: error[name],
});
const accountOptions = [
{ key: '0', text: 'Cash', value: 'Cash' },
{ key: '1', text: 'Interac e-Transfer', value: 'Interac' },
{ key: '2', text: 'Square', value: 'Square Pmt' },
//{ key: '3', text: 'Dream Payments (Debit/Credit)', value: 'Dream Pmt' },
{ key: '4', text: 'Cheque', value: 'TD Chequing' },
//{ key: '5', text: 'Member Balance / Protocash', value: 'Member' },
{ key: '6', text: 'Membership Adjustment / Clearing', value: 'Clearing' },
{ key: '7', text: 'PayPal', value: 'PayPal' },
{ key: '8', text: 'Protocoin', value: 'Protocoin' },
];
const sourceOptions = [
{ key: '0', text: 'Web (Spaceport)', value: 'Web' },
{ key: '1', text: 'Database Edit', value: 'DB Edit' },
{ key: '2', text: 'System', value: 'System' },
{ key: '3', text: 'Receipt or Statement', value: 'Receipt or Stmt' },
{ key: '4', text: 'Quicken Import', value: 'Quicken Import' },
{ key: '5', text: 'PayPal IPN', value: 'PayPal IPN' },
{ key: '6', text: 'Auto', value: 'Auto' },
{ key: '7', text: 'Nexus Database Bulk', value: 'Nexus DB Bulk' },
{ key: '8', text: 'IPN Trigger', value: 'IPN Trigger' },
{ key: '9', text: 'Intranet Receipt', value: 'Intranet Receipt' },
{ key: '10', text: 'Automatic', value: 'Automatic' },
{ key: '11', text: 'Manual', value: 'Manual' },
];
const categoryOptions = [
{ key: '0', text: 'Membership Dues', value: 'Membership' },
{ key: '1', text: 'Course Fee', value: 'OnAcct' },
{ key: '2', text: 'Snacks / Pop / Coffee', value: 'Snacks' },
{ key: '3', text: 'Donation (Explain in Memo)', value: 'Donation' },
{ key: '4', text: 'Consumables (Explain in Memo)', value: 'Consumables' },
{ key: '5', text: 'Purchase of Locker / Materials / Stock', value: 'Purchases' },
{ key: '6', text: 'Purchase of Protocoin', value: 'Exchange' },
{ key: '7', text: 'Reimbursement (Not for Refunds)', value: 'Reimburse' },
{ key: '8', text: 'Other (Explain in Memo)', value: 'Other' },
];
return (
<div className='transaction-editor'>
<Form.Group widths='equal'>
<Form.Field error={error.member_id}>
<label>Member (search)</label>
<MembersDropdown
token={token}
{...makeProps('member_id')}
onChange={handleValues}
initial={input.member_name}
/>
</Form.Field>
<Form.Input
label='Date'
fluid
{...makeProps('date')}
/>
</Form.Group>
<Form.Group widths='equal'>
<Form.Select
label='Payment Method'
fluid
options={accountOptions}
{...makeProps('account_type')}
onChange={handleValues}
/>
{input.account_type && (input.account_type === 'Protocoin' ?
<Form.Input
label='Protocoin Delta (+/-)'
fluid
{...makeProps('protocoin')}
/>
:
<Form.Input
label='Amount ($)'
fluid
{...makeProps('amount')}
/>
)}
</Form.Group>
{input?.account_type !== prevInput?.account_type && input?.account_type === 'PayPal' &&
<Message visible warning>
<Message.Header>Are you sure?</Message.Header>
<p>PayPal transactions are automatic. Double check there's no duplicate. They may take 24h to appear.</p>
</Message>
}
{input?.account_type !== prevInput?.account_type && input?.account_type === 'Protocoin' &&
<Message visible warning>
<Message.Header>Are you sure?</Message.Header>
<p>Protocoin spending transactions are automatic. Do you want the "Purchase of Protocoin" category below?</p>
{input.protocoin > 0 && <p>Also, the value should be a <b>negative</b> number if they are spending Protocoin.</p>}
</Message>
}
<Form.Group widths='equal'>
<Form.Select
label='Category'
fluid
options={categoryOptions}
{...makeProps('category')}
onChange={handleValues}
/>
{input.category === 'Membership' &&
<Form.Input
label='Membership Months (+/-)'
fluid
{...makeProps('number_of_membership_months')}
/>
}
{input.category === 'Exchange' &&
<Form.Input
label='Protocoin Purchased'
fluid
{...makeProps('amount')} // trick the user
/>
}
</Form.Group>
<Form.Group widths='equal'>
<Form.Input
label='Reference Number'
fluid
{...makeProps('reference_number')}
/>
<Form.Input
label='Memo / Notes'
fluid
{...makeProps('memo')}
/>
</Form.Group>
</div>
);
};
function EditTransaction(props) {
const { transaction, setTransaction, token, refreshUser } = props;
const [input, setInput] = useState(transaction);
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const { id } = useParams();
const handleSubmit = (e) => {
if (loading) return;
setLoading(true);
setSuccess(false);
const data = { ...input, report_type: null, report_memo: '' };
requester('/transactions/'+id+'/', 'PUT', token, data)
.then(res => {
setLoading(false);
setSuccess(true);
setError(false);
setInput(res);
setTransaction(res);
if (refreshUser) {
refreshUser();
}
})
.catch(err => {
setLoading(false);
console.log(err);
setError(err.data);
});
};
return (
<div>
<Header size='medium'>Edit Transaction</Header>
<Form onSubmit={handleSubmit}>
<TransactionEditor token={token} input={input} setInput={setInput} error={error} />
<Form.Button loading={loading} error={error.non_field_errors}>
{transaction.report_type ? 'Save & Unreport' : 'Save'}
</Form.Button>
{success && <div>Success!</div>}
</Form>
</div>
);
};
export function TransactionList(props) {
const { transactions, noMember, noCategory } = props;
const isMobile = useIsMobile();
return (
<Table basic='very'>
{!isMobile && <Table.Header>
<Table.Row>
<Table.HeaderCell>Date</Table.HeaderCell>
{!noMember && <Table.HeaderCell>Member</Table.HeaderCell>}
<Table.HeaderCell>Amount</Table.HeaderCell>
<Table.HeaderCell>Method</Table.HeaderCell>
{!noCategory && <Table.HeaderCell>Category</Table.HeaderCell>}
<Table.HeaderCell>Memo</Table.HeaderCell>
</Table.Row>
</Table.Header>}
<Table.Body>
{transactions.length ?
transactions.map(x =>
<Table.Row key={x.id}>
<Table.Cell style={{ minWidth: '8rem' }}>
<Link to={'/transactions/'+x.id}>{moment(x.date).format('ll')}</Link>
</Table.Cell>
{!noMember && <Table.Cell>
{x.member_id ?
<Link to={'/members/'+x.member_id}>
{x.member_name}
</Link>
:
x.member_name
}
</Table.Cell>}
<Table.Cell style={{ minWidth: '8rem' }}>{isMobile && 'Amount: '}{x.protocoin !== '0.00' ? '₱ ' + x.protocoin : '$' + x.amount}</Table.Cell>
<Table.Cell>{isMobile && 'Method: '}{x.account_type}</Table.Cell>
{!noCategory && <Table.Cell>{isMobile && 'Category: '}{x.category}</Table.Cell>}
<Table.Cell>{x.memo || x.report_memo}</Table.Cell>
</Table.Row>
)
:
<Table.Row><Table.Cell>None</Table.Cell></Table.Row>
}
</Table.Body>
</Table>
);
};
export function Transactions(props) {
const { user } = props;
return (
<Container>
<Header size='large'>Your Transactions</Header>
<TransactionList noMember noCategory transactions={user.transactions} />
</Container>
);
};
class TransactionTable extends React.Component {
render() {
const transaction = this.props.transaction;
const user = this.props.user;
return (
<BasicTable>
<Table.Body>
<Table.Row>
<Table.Cell>Member:</Table.Cell>
{isAdmin(user) && transaction.member_id ?
<Table.Cell>
<Link to={'/members/'+transaction.member_id}>
{transaction.member_name}
</Link>
</Table.Cell>
:
<Table.Cell>{transaction.member_name}</Table.Cell>
}
</Table.Row>
<Table.Row>
<Table.Cell>Number:</Table.Cell>
<Table.Cell>{transaction.id}</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>Date:</Table.Cell>
<Table.Cell>{transaction.date}</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>Amount:</Table.Cell>
<Table.Cell>${transaction.amount}</Table.Cell>
</Table.Row>
{transaction.protocoin !== '0.00' && <Table.Row>
<Table.Cell>Protocoin:</Table.Cell>
<Table.Cell>{transaction.protocoin}</Table.Cell>
</Table.Row>}
<Table.Row>
<Table.Cell>Category:</Table.Cell>
<Table.Cell>{transaction.category}</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>Method:</Table.Cell>
<Table.Cell>{transaction.account_type}</Table.Cell>
</Table.Row>
{/* <Table.Row>
<Table.Cell>Payment Method:</Table.Cell>
<Table.Cell>{transaction.payment_method}</Table.Cell>
</Table.Row> */}
<Table.Row>
<Table.Cell>Info Source:</Table.Cell>
<Table.Cell>{transaction.info_source}</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>Reference:</Table.Cell>
<Table.Cell>{transaction.reference_number}</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>Memo:</Table.Cell>
<Table.Cell>{transaction.memo}</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>Recorder:</Table.Cell>
<Table.Cell>{transaction.recorder || 'System'}</Table.Cell>
</Table.Row>
{!!transaction.report_type && <Table.Row>
<Table.Cell>Report Type:</Table.Cell>
<Table.Cell>{transaction.report_type}</Table.Cell>
</Table.Row>}
{!!transaction.report_memo && <Table.Row>
<Table.Cell>Report Memo:</Table.Cell>
<Table.Cell>{transaction.report_memo}</Table.Cell>
</Table.Row>}
</Table.Body>
</BasicTable>
);
}
}
class TransactionPrint extends React.Component {
render() {
const transaction = this.props.transaction;
const user = this.props.user;
return (
<div style={{padding: '1in', background: 'white', width: '100%', height: '100%'}}>
<Header size='large'>Protospace Transaction Receipt</Header>
<p>Calgary Protospace Ltd.</p>
<p>Bay 108, 1530 - 27th Ave NE<br />Calgary, AB T2E 7S6<br />protospace.ca</p>
<TransactionTable user={user} transaction={transaction} />
<p>Thank you!</p>
</div>
);
}
}
export function TransactionDetail(props) {
const { token, user } = props;
const { id } = useParams();
const ownTransaction = user.transactions.find(x => x.id === id);
const [transaction, setTransaction] = useState(ownTransaction || false);
const [error, setError] = useState(false);
const printRef = useRef();
useEffect(() => {
requester('/transactions/'+id+'/', 'GET', token)
.then(res => {
setTransaction(res);
setError(false);
})
.catch(err => {
console.log(err);
setError(true);
});
}, [ownTransaction]);
return (
<Container>
{!error ?
transaction ?
<div>
<Header size='large'>Transaction Receipt</Header>
<Grid stackable columns={2}>
<Grid.Column width={6}>
<TransactionTable user={user} transaction={transaction} />
<div style={{ display: 'none' }}>
<TransactionPrint ref={printRef} user={user} transaction={transaction} />
</div>
<ReactToPrint
trigger={() => <Button>Print Receipt</Button>}
content={() => printRef.current}
/>
</Grid.Column>
<Grid.Column width={10}>
{isAdmin(user) ?
<Segment padded>
<EditTransaction transaction={transaction} setTransaction={setTransaction} {...props} />
</Segment>
:
<Segment padded>
<Header size='medium'>Report Transaction</Header>
<p>If there's anything wrong with this transaction or it was made in error please email the Protospace Directors:</p>
<p><a href='mailto:directors@protospace.ca' target='_blank' rel='noopener noreferrer'>directors@protospace.ca</a></p>
<p>Please include a link to this transaction and any relevant details.</p>
</Segment>
}
</Grid.Column>
</Grid>
</div>
:
<p>Loading...</p>
:
<NotFound />
}
</Container>
);
};