Skip to content

Instantly share code, notes, and snippets.

@bradherman
Created February 6, 2021 00:22
Show Gist options
  • Save bradherman/dfd7495a5e020eaeabf4310daf064f36 to your computer and use it in GitHub Desktop.
Save bradherman/dfd7495a5e020eaeabf4310daf064f36 to your computer and use it in GitHub Desktop.
import { action, computed, decorate, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import React, { Component } from 'react';
import RelativeTime from 'react-relative-time';
import { NavLink } from 'react-router-dom';
import { Badge, Button } from 'reactstrap';
import { client } from '../../helpers/client';
import { EarningModal } from '../Modals/EarningModal';
import { SmartTable } from '../SmartTable/SmartTable';
import { Earning } from '../../models/Earning';
import ClipboardLink from '../../components/Common/ClipboardLink';
export const EarningsTable = observer(class EarningsTable extends Component {
selectedEarning
totalPayouts
addModalOpen = false
constructor(props) {
super(props)
if (this.props.filter === 'pending') {
this.fetchTotals()
}
}
fetchTotals = async () => {
const result = await client.get('/earnings/payout_total')
this.totalPayouts = result.total
}
idFormatter = (cell, row) => (
<><a
href={`#${row.id}`}
key={row.id}
onClick={
() => {
runInAction(() => {
row.amount = row.amount.cents / 100;
this.addModalOpen = true;
this.selectedEarning = row;
});
}
}
>
{cell.split('-')[0]}
</a><ClipboardLink text={cell} /></>
)
createdByFormatter = (cell, _row) => {
if (!cell) {
return 'System'
} else {
return cell.name
}
}
earnableFormatter = (cell, row) => {
return <a href={`/booking/${row.bookingId}`}>
{cell}
</a>
}
driverFormatter = (cell, row) => {
const url = `/driver/${cell.id}`;
return (
<NavLink strict to={url}>
{ cell.name }
</NavLink>
);
}
createdAtFormatter = (cell, row) => {
if (cell) {
return (<RelativeTime value={cell} />);
}
}
emailFormatter = (cell, row) => {
if (cell) {
return <a href={`mailto:${cell}`}>{cell}</a>;
}
}
statusFormatter = (cell, _row) => {
switch (cell) {
case 'pending':
return <Badge color="warning" className="mr-1">Pending</Badge>;
case 'approved':
return <Badge className="badge-soft-warning mr-1">Approved</Badge>;
case 'paid':
return <Badge className="badge-soft-success mr-1">{ cell.toUpperCase() }</Badge>;
case 'cancelled':
return <Badge className="badge-soft-info mr-1">{ cell.toUpperCase() }</Badge>;
case 'failed':
return <Badge color="danger" className="mr-1">{ cell.toUpperCase() }</Badge>;
default:
return <Badge color="danger" className="mr-1">unknown</Badge>;
}
}
amountFormatter = (cell, _row) => cell.pretty
transferFormatter = (cell, _row) => {
if (cell) {
return <a href={`/transfers/${cell}`}>{ cell.split('-')[0] }</a>;
}
}
approveAllEarnings = async (earnings) => {
if (window.confirm('Are you sure you want to approve all selected earnings?')) {
await Promise.allSettled(earnings.map(async (earning) => {
await client.put(`/earnings/${earning.id}/`, { earning: { status: 'approved' } });
runInAction(() => {earning.status = 'approved'});
}))
}
}
columns = [
{
dataField: 'id',
text: 'ID',
formatter: this.idFormatter,
sort: true,
headerStyle: () => ({ width: '100px' }),
},
{
dataField: 'driver',
text: 'Driver',
formatter: this.driverFormatter,
sort: true,
},
{
dataField: 'amount',
text: 'Amount',
formatter: this.amountFormatter,
sort: true,
},
{
dataField: 'earnableType',
text: 'Type',
formatter: this.earnableFormatter,
},
{
dataField: 'status',
text: 'Status',
sort: true,
formatter: this.statusFormatter,
headerStyle: () => ({ width: '90px' }),
},
{
dataField: 'stripeTransferId',
text: 'Transfer',
formatter: this.transferFormatter,
headerStyle: () => ({ width: '75px' }),
},
{
dataField: 'createdAt',
text: 'Created At',
formatter: this.createdAtFormatter,
sort: true,
},
{
dataField: 'expectedPayDate',
text: 'Expected Pay Date',
},
{
dataField: 'admin',
text: 'Created By',
formatter: this.createdByFormatter,
headerStyle: () => ({ width: '90px' }),
},
{
dataField: 'issuedAt',
text: 'Issued At',
formatter: this.createdAtFormatter,
},
]
render() {
return (
<>
<EarningModal
isOpen={this.addModalOpen}
toggle={() => {
runInAction(() => {
this.addModalOpen = !this.addModalOpen
});
}}
title={this.modalTitle}
submitHandler={this.submitEarningHandler}
earning={this.selectedEarning}
/>
<SmartTable
model={Earning}
dataUrl={this.props.dataUrl}
title={this.props.filter || 'All'}
baseTitle={this.props.baseTitle || 'Earnings'}
columns={this.columns}
embedded={this.props.embedded}
defaultLimit={this.limit}
filter={this.props.filter}
reactions={[this.fetchTotals]}
singleAction={
this.props.filter === 'pending' && <Button
color={ "primary" }
onClick={ this.issuePayouts }
>
Payout Approved Earnings: {this.earningTotal}
</Button>
}
actions={{
'Approve Checked': rows => this.approveAllEarnings(rows),
}}
addAction={this.props.addEnabled && (() => {
runInAction(() => {
this.selectedEarning = null;
this.addModalOpen = !this.addModalOpen;
});
})}
/>
</>
);
}
});
decorate(EarningsTable, {
selectedEarning: observable,
earningTotal: computed,
totalPayouts: observable,
fetchTotals: action,
limit: computed,
approveAllEarnings: action,
addModalOpen: observable,
modalTitle: computed,
});
import {
action, computed, decorate, observable, runInAction,
} from 'mobx';
import { observer } from 'mobx-react';
import React, { Component } from 'react';
import BootstrapTable from 'react-bootstrap-table-next';
import 'react-bootstrap-table-next/dist/react-bootstrap-table2.min.css';
import filterFactory, { textFilter } from 'react-bootstrap-table2-filter';
import paginationFactory from 'react-bootstrap-table2-paginator';
import {
ButtonDropdown, Card, CardBody, CardTitle, Col, DropdownItem, DropdownMenu, DropdownToggle, Row, Spinner,
} from 'reactstrap';
import { client } from '../../helpers/client';
// Import Breadcrumb
import Breadcrumbs from '../Common/Breadcrumb';
import ToolkitProvider, { Search } from 'react-bootstrap-table2-toolkit';
import 'react-bootstrap-table2-toolkit/dist/react-bootstrap-table2-toolkit.min.css';
import debounce from 'lodash.debounce';
const { SearchBar } = Search;
export const SmartTable = observer(class SmartTable extends Component {
page = 1
perPage = 25
totalRecords
dataRows = []
sortField
sortOrder
loading = false
selectedRows = []
showActionDropdown = false
searchText
params = {}
constructor(props) {
super(props);
if (this.props.defaultLimit) {
this.perPage = this.props.defaultLimit;
}
if (!this.props.skipInitLoad) {
this.fetchData();
}
}
fetchData = async (params) => {
this.loading = true;
this.dataRows.clear();
if (!params) { params = {}; }
this.params = params
if (this.props.filter) { params.filter = this.props.filter; }
if (this.sortField) { params.sort_column = this.sortField; }
if (this.sortOrder) { params.sort_dir = this.sortOrder; }
if (this.perPage) { params.limit = this.perPage; }
if (this.page) { params.page = this.page; }
if (this.searchText) { params.query = this.searchText; }
const response = await client.get(this.props.dataUrl, params);
if (this.props.model) {
for (const row of response.data.map(item => new this.props.model(item))) {
this.dataRows.push(row)
}
} else {
for (const row of response.data) {
this.dataRows.push(row) ;
}
}
this.page = response.meta.page;
this.totalRecords = response.meta.total;
this.perPage = response.meta.perPage;
this.runReactions()
this.loading = false;
}
runReactions = async () => {
if (this.props.reactions) {
for (const reaction of this.props.reactions) {
await reaction()
}
}
}
_handleTableChange = (type, {
sortField, sortOrder, data, filters, page, sizePerPage, searchText,
}) => {
if (type === 'sort') {
this.sortField = sortField.replace('attributes.', '');
this.sortOrder = sortOrder;
this.fetchData();
} else if (type === 'search') {
this.searchText = searchText
this.fetchData()
}
}
get rows() {
return this.dataRows.slice()
}
render() {
return (
<>
<div className={`${!this.props.embedded && 'page-content'}`}>
<div className={`${!this.props.embedded && 'container-fluid'}`}>
{ !this.props.embedded && <Breadcrumbs title={this.props.baseTitle} breadcrumbItem={this.props.title} /> }
<Row>
<Col className="col-12">
<Card>
<CardBody>
<CardTitle
style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between' }}
>
{this.props.baseTitle}
{' '}
{this.props.title && `- ${this.props.title}`}
{/* there's a bug with the table not updating on the loading change, this line helps fix it for now */}
{ this.loading ? ' ' : ' ' }
<div className="btn-group mb-2">
{ this.props.addAction
&& (
<button type="button" className="btn btn-primary waves-effect waves-light" onClick={this.props.addAction}>
<i className="bx bx-plus font-size-16 align-middle mr-1" />
{' '}
Add
</button>
)}
{ this.props.singleAction }
{ this.props.actions && (
<ButtonDropdown
isOpen={this.showActionDropdown}
toggle={() => runInAction(() => {
this.showActionDropdown = !this.showActionDropdown;
})}
>
<DropdownToggle
caret
color="secondary"
className="btn btn-secondary btn-sm"
>
Actions &nbsp;
{' '}
<i className="mdi mdi-chevron-down" />
</DropdownToggle>
<DropdownMenu className="dropdown-menu-right">
{
Object.keys(this.props.actions).map((key, index) => (
<DropdownItem
key={key}
onClick={async () => {
await this.props.actions[key](this.selectedRows);
this.fetchData(this.params);
}}
>
{ key }
</DropdownItem>
))
}
</DropdownMenu>
</ButtonDropdown>
) }
</div>
</CardTitle>
<ToolkitProvider
keyField="id"
data={this.rows}
columns={this.props.columns}
search
>
{
toolkitProps => [
<>{ this.props.searchable &&
<Col sm="12">
<div className="search-box mr-2 mb-2 d-inline-block">
<div className="position-relative">
<SearchBar
{ ...toolkitProps.searchProps }
searchText={toolkitProps.searchProps.searchtext}
onSearch={debounce(toolkitProps.searchProps.onSearch, 1000)}
onClear={toolkitProps.searchProps.onClear}
placeholder={`Search ${this.props.baseTitle}`}
/ >
<i className="bx bx-search-alt search-icon" />
</div>
</div>
</Col>
}</>,
<BootstrapTable
{ ...toolkitProps.baseProps }
remote={{ sort: true, pagination: true, search: true }}
loading={this.loading}
responsive
hover
bordered
condensed
selectRow={this.props.actions && {
mode: 'checkbox',
style: { backgroundColor: '#c8e6c9' },
onSelect: (row, isSelect, rowIndex, e) => {
if (isSelect) {
this.selectedRows.push(row);
} else {
const filteredObjects = this.selectedRows.filter((r) => r.id !== row.id);
this.selectedRows.replace(filteredObjects);
}
},
onSelectAll: (isSelect, rows, e) => {
if (isSelect) {
this.selectedRows.replace(rows);
} else {
this.selectedRows.replace([]);
}
},
}}
noDataIndication={this.loading ? <Spinner className="mr-2" color="primary" /> : <div>No Data</div>}
filter={filterFactory()}
pagination={paginationFactory({
page: this.page,
showTotal: true,
sizePerPage: this.perPage,
totalSize: this.totalRecords,
onSizePerPageChange: (sizePerPage, page) => {
this.page = page;
this.perPage = sizePerPage;
this.fetchData({
page,
limit: sizePerPage,
});
},
onPageChange: (page, sizePerPage) => {
this.page = page;
this.perPage = sizePerPage;
this.fetchData({
page,
limit: sizePerPage,
});
},
})}
onTableChange={this._handleTableChange}
/>
]
}
</ToolkitProvider>
</CardBody>
</Card>
</Col>
</Row>
</div>
</div>
</>
);
}
});
decorate(SmartTable, {
dataCols: observable,
dataRows: observable,
sortField: observable,
sortOrder: observable,
fetchData: action,
page: observable,
perPage: observable,
totalRecords: observable,
loading: observable,
showActionDropdown: observable,
selectedRows: observable,
searchText: observable,
rows: computed,
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment