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 @@ + + + + + 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; + } +}