diff --git a/frontend/assets/style/components/_forms.scss b/frontend/assets/style/components/_forms.scss
index 24f7d76b6c..26f5ce2b70 100644
--- a/frontend/assets/style/components/_forms.scss
+++ b/frontend/assets/style/components/_forms.scss
@@ -165,6 +165,17 @@ legend.legend {
padding-left: 1rem;
min-width: 6.5rem;
+ &:disabled {
+ cursor: default;
+ color: $text_1;
+ opacity: 0.7;
+
+ &:hover,
+ &:focus {
+ border-color: $general_0;
+ }
+ }
+
option:disabled,
&.is-empty {
color: $text_1;
diff --git a/frontend/utils/lazyLoadedView.js b/frontend/utils/lazyLoadedView.js
index 83d07e9a29..72f6618931 100644
--- a/frontend/utils/lazyLoadedView.js
+++ b/frontend/utils/lazyLoadedView.js
@@ -97,6 +97,7 @@ lazyModalFullScreen('PropositionsAllModal', () => import('../views/containers/pr
lazyComponent('PaymentDetail', () => import('../views/containers/payments/PaymentDetail.vue'))
lazyComponent('RecordPayment', () => import('../views/containers/payments/RecordPayment.vue'))
lazyComponent('SendPaymentsViaLightning', () => import('../views/containers/payments/SendPaymentsViaLightning.vue'))
+lazyComponent('ExportPaymentsModal', () => import('../views/containers/payments/ExportPaymentsModal.vue'))
lazyComponent('Appearence', () => import('../views/containers/user-settings/Appearence.vue'))
lazyComponent('NotificationSettings', () => import('../views/containers/user-settings/NotificationSettings.vue'))
diff --git a/frontend/views/containers/payments/ExportPaymentsModal.vue b/frontend/views/containers/payments/ExportPaymentsModal.vue
new file mode 100644
index 0000000000..27f7eeef54
--- /dev/null
+++ b/frontend/views/containers/payments/ExportPaymentsModal.vue
@@ -0,0 +1,195 @@
+
+modal-template(ref='modal' :a11yTitle='modalTitle')
+ template(slot='title')
+ span {{ modalTitle }}
+
+ .c-sub-title.has-text-1 {{ exportInstructions }}
+
+ label.field
+ .label
+ i18n Payment period
+
+ .selectbox
+ select.select.c-period-select(
+ name='period'
+ required=''
+ v-model='form.period'
+ :class='{ "is-empty": form.period === "choose" }'
+ )
+ option(
+ value='choose'
+ :disabled='true'
+ )
+ i18n Select payment period
+
+ option(value='all')
+ i18n Export all periods
+
+ option(
+ v-for='period in ephemeral.periodOpts'
+ :key='period'
+ :value='period'
+ ) {{ displayPeriod(period) }}
+
+ .buttons.c-btns-container
+ i18n.is-outlined(
+ tag='button'
+ type='button'
+ @click='close'
+ ) Cancel
+
+ i18n(
+ tag='button'
+ @click='exportToCSV'
+ :disabled='form.period === "choose"'
+ ) Export payments
+
+ a.c-invisible-download-helper(
+ ref='downloadHelper'
+ :download='ephemeral.downloadName'
+ )
+
+
+
+
+
diff --git a/frontend/views/containers/payments/PaymentDetail.vue b/frontend/views/containers/payments/PaymentDetail.vue
index ab7659ae60..55191ca308 100644
--- a/frontend/views/containers/payments/PaymentDetail.vue
+++ b/frontend/views/containers/payments/PaymentDetail.vue
@@ -106,7 +106,8 @@ export default ({
// NOTE: Only for the historical payments, there is 'period'
const { id, period } = this.$route.query
const payment = this.lightningPayment || // TODO: to be re-worked once lightning network is implemented.
- this.currentGroupState.payments[id] || await this.getHistoricalPaymentByHashAndPeriod(id, period)
+ this.currentGroupState.payments[id] ||
+ (await this.getHistoricalPaymentDetailsByPeriod(period))[id]
if (id) {
sbp('okTurtles.events/emit', SET_MODAL_QUERIES, 'PaymentDetail', { id })
diff --git a/frontend/views/containers/payments/PaymentsMixin.js b/frontend/views/containers/payments/PaymentsMixin.js
index 67b81966fb..9484782316 100644
--- a/frontend/views/containers/payments/PaymentsMixin.js
+++ b/frontend/views/containers/payments/PaymentsMixin.js
@@ -29,15 +29,15 @@ const PaymentsMixin: Object = {
return await this.historicalPeriodStampGivenDate(payment.date)
},
- async getAllPaymentPeriods () {
- return { ...await this.getHistoricalPaymentPeriods(), ...this.groupPeriodPayments }
+ async getAllPeriodPayments () {
+ return { ...await this.getHistoricalPeriodPayments(), ...this.groupPeriodPayments }
},
// Oldest key first.
async getAllSortedPeriodKeys () {
- const historicalPaymentPeriods = await this.getHistoricalPaymentPeriods()
+ const historicalPeriodPayments = await this.getHistoricalPeriodPayments()
return [
- ...Object.keys(historicalPaymentPeriods).sort(),
+ ...Object.keys(historicalPeriodPayments).sort(),
...this.groupSortedPeriodKeys
]
},
@@ -68,7 +68,7 @@ const PaymentsMixin: Object = {
},
// ====================
- async getHistoricalPaymentPeriods () {
+ async getHistoricalPeriodPayments () {
const ourArchiveKey = `paymentsByPeriod/${this.ourUsername}/${this.currentGroupId}`
return await sbp('gi.db/archive/load', ourArchiveKey) ?? {}
},
@@ -77,12 +77,12 @@ const PaymentsMixin: Object = {
const sent = []
const received = []
const todo = cloneDeep(this.ourPayments?.todo ?? [])
- const paymentPeriods = await this.getAllPaymentPeriods()
+ const periodPayments = await this.getAllPeriodPayments()
const sortPayments = (f, l) => f.meta.createdDate > l.meta.createdDate ? 1 : -1
- for (const periodStamp of Object.keys(paymentPeriods).sort().reverse()) {
+ for (const periodStamp of Object.keys(periodPayments).sort().reverse()) {
const paymentsByHash = await this.getPaymentDetailsByPeriod(periodStamp)
- const { paymentsFrom } = paymentPeriods[periodStamp]
+ const { paymentsFrom } = periodPayments[periodStamp]
for (const fromUser of Object.keys(paymentsFrom)) {
for (const toUser of Object.keys(paymentsFrom[fromUser])) {
if (toUser === this.ourUsername || fromUser === this.ourUsername) {
@@ -110,13 +110,12 @@ const PaymentsMixin: Object = {
const paymentHashes = this.paymentHashesForPeriod(period) || []
detailedPayments = Object.fromEntries(paymentHashes.map(hash => [hash, this.currentGroupState.payments[hash]]))
} else {
- const paymentsByPeriod = await this.getHistoricalPaymentPeriods()
+ const paymentsByPeriod = await this.getHistoricalPeriodPayments()
const paymentHashes = paymentHashesFromPaymentPeriod(paymentsByPeriod[period])
+ const historicalPaymentDetails = await this.getHistoricalPaymentDetailsByPeriod(period)
- const paymentsKey = `payments/${this.ourUsername}/${period}/${this.currentGroupId}`
- const payments = await sbp('gi.db/archive/load', paymentsKey) || {}
for (const hash of paymentHashes) {
- detailedPayments[hash] = payments[hash]
+ detailedPayments[hash] = historicalPaymentDetails[hash]
}
}
return detailedPayments
@@ -133,18 +132,18 @@ const PaymentsMixin: Object = {
}
return payments
},
- async getHistoricalPaymentByHashAndPeriod (hash: string, period: string) {
+ async getHistoricalPaymentDetailsByPeriod (period: string) {
const paymentsKey = `payments/${this.ourUsername}/${period}/${this.currentGroupId}`
- const payments = await sbp('gi.db/archive/load', paymentsKey) || {}
+ const paymentDetails = await sbp('gi.db/archive/load', paymentsKey) || {}
- return payments[hash]
+ return paymentDetails
},
async getHaveNeedsSnapshotByPeriod (period: string) {
if (Object.keys(this.groupPeriodPayments).includes(period)) {
return this.groupPeriodPayments[period].haveNeedsSnapshot || []
}
- const paymentsByPeriod = await this.getHistoricalPaymentPeriods()
+ const paymentsByPeriod = await this.getHistoricalPeriodPayments()
return Object.keys(paymentsByPeriod).includes(period)
? paymentsByPeriod[period].haveNeedsSnapshot || []
: []
@@ -171,7 +170,7 @@ const PaymentsMixin: Object = {
// or an empty object if not found.
// TODOs: rename to getPaymentPeriod, and maybe avoid loading all historical payment periods.
async getPaymentPeriod (period: string) {
- return this.groupPeriodPayments[period] ?? (await this.getHistoricalPaymentPeriods())[period] ?? {}
+ return this.groupPeriodPayments[period] ?? (await this.getHistoricalPeriodPayments())[period] ?? {}
},
// Returns a human-readable description of the time interval identified by a given period stamp.
getPeriodFromStartToDueDate (period) {
diff --git a/frontend/views/pages/Payments.vue b/frontend/views/pages/Payments.vue
index a8261382d5..516805fd32 100644
--- a/frontend/views/pages/Payments.vue
+++ b/frontend/views/pages/Payments.vue
@@ -67,8 +67,8 @@ page(
.c-tabs-chip-container.hide-phone
next-distribution-pill
- .c-chip-container-below-tabs
- next-distribution-pill.hide-tablet.c-distribution-pill
+ .c-chip-container-below-tabs.hide-tablet
+ next-distribution-pill.c-distribution-pill
.c-filters(v-if='paymentsListData.length > 0')
.c-method-filters
@@ -121,6 +121,14 @@ page(
@change-rows-per-page='handleRowsPerPageChange'
)
+ .c-export-csv-container
+ i18n.is-outlined.is-small.c-export-csv-btn(
+ v-if='showExportPaymentsButton'
+ tag='button'
+ type='button'
+ @click='openExportPaymentsModal'
+ ) Export CSV
+
.c-container(v-else-if='ephemeral.activeTab === "PaymentRowTodo" && ephemeral.paymentMethodFilter === "lightning"')
p.c-lightning-todo-msg Coming Soon.
@@ -227,8 +235,11 @@ export default ({
this.ephemeral.activeTab = section
} else {
const fromQuery = from?.query || {}
- const isFromPaymentDetailModal = fromQuery.modal === 'PaymentDetail'
- const defaultTab = isFromPaymentDetailModal
+ const isFromTableRelatedModals = [
+ 'PaymentDetail',
+ 'ExportPaymentsModal'
+ ].includes(fromQuery.modal)
+ const defaultTab = isFromTableRelatedModals
// When payment detail modal is closed, the payment table has to remain in the previously active tab.
// (context: https://github.com/okTurtles/group-income/issues/1686)
? fromQuery.section || this.tabSections[0]
@@ -385,6 +396,10 @@ export default ({
showTabSelectionMenu () {
return this.tabItems.length > 0
},
+ showExportPaymentsButton () {
+ return ['PaymentRowSent', 'PaymentRowReceived'].includes(this.ephemeral.activeTab) &&
+ this.paymentsListData.length > 0
+ },
isDevEnv () {
return process.env.NODE_ENV === 'development'
}
@@ -478,6 +493,17 @@ export default ({
if (Object.keys(this.groupSettings).length) {
this.historicalPayments = await this.getAllPaymentsInTypes()
}
+ },
+ openExportPaymentsModal () {
+ const modalTypeMap = {
+ 'PaymentRowSent': 'sent',
+ 'PaymentRowReceived': 'received'
+ }
+
+ sbp('okTurtles.events/emit', OPEN_MODAL, 'ExportPaymentsModal',
+ { type: modalTypeMap[this.ephemeral.activeTab] }, // query params
+ { data: this.paymentsListData }
+ )
}
}
}: Object)
@@ -523,20 +549,11 @@ export default ({
.c-chip-container-below-tabs {
display: flex;
flex-direction: row;
- flex-wrap: wrap-reverse;
- align-items: center;
- justify-content: space-between;
-
- > * {
- margin-bottom: 1.25rem;
-
- @include tablet {
- margin-bottom: 2.5rem;
- }
- }
+ justify-content: flex-start;
+ margin-bottom: 1.25rem;
- h3 {
- margin-right: 1.25rem;
+ @include tablet {
+ margin-bottom: 2.5rem;
}
}
@@ -660,6 +677,7 @@ export default ({
// Footer
.c-footer {
+ position: relative;
padding-top: 1.5rem;
@include tablet {
@@ -708,4 +726,28 @@ export default ({
.c-lightning-todo-msg {
margin-top: 2rem;
}
+
+.c-export-csv-container {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+}
+
+.c-export-csv-btn {
+ margin-top: 0.75rem;
+
+ @include tablet {
+ position: absolute;
+ bottom: 0;
+ right: 11.75rem;
+ margin-top: 0;
+ }
+
+ @media screen and (min-width: $desktop) and (max-width: 1310px) {
+ position: relative;
+ bottom: unset;
+ right: unset;
+ margin-top: 0.75rem;
+ }
+}