Created
February 6, 2021 00:22
-
-
Save bradherman/dfd7495a5e020eaeabf4310daf064f36 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
{' '} | |
<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