diff --git a/.vscode/extensions.json b/.vscode/extensions.json index d75c235e1..a2e30e4a0 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -22,6 +22,6 @@ "open-southeners.laravel-pint", "damianbal.vs-phpclassgen", "bmewburn.vscode-intelephense-client", - "vue.volar" + "octref.vetur" ] } diff --git a/Changelog.md b/Changelog.md index 7bc74a91c..15c5c05c8 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,9 @@ # Changelog +## 5.8.0 + +* Consolidated donor/donation export dialogs into separate page, with more options, and improved performance of Excel export + ## 5.7.0 * Added option to select year for donors export diff --git a/app/Exports/Fundraising/DonationsExport.php b/app/Exports/Fundraising/DonationsExport.php index b1f6b9da2..d69227515 100644 --- a/app/Exports/Fundraising/DonationsExport.php +++ b/app/Exports/Fundraising/DonationsExport.php @@ -25,7 +25,7 @@ private function getDonationsQuery() { return $this->donor != null ? $this->donor->donations() - : Donation::query(); + : Donation::query()->with('donor'); } public function sheets(): array diff --git a/app/Exports/Fundraising/DonorsExport.php b/app/Exports/Fundraising/DonorsExport.php index 3ff7e782e..e12c07d8a 100644 --- a/app/Exports/Fundraising/DonorsExport.php +++ b/app/Exports/Fundraising/DonorsExport.php @@ -25,7 +25,7 @@ class DonorsExport extends BaseExport implements FromQuery, WithColumnFormatting */ private array $years; - public function __construct(?int $year = null) + public function __construct(?int $year = null, private bool $includeChannels = false, private bool $showAllDonors = true) { $this->orientation = PageOrientation::Landscape; @@ -34,29 +34,41 @@ public function __construct(?int $year = null) now()->year, ]; - $this->usedCurrenciesChannels = Donation::select('currency', 'channel') - ->selectRaw('YEAR(date) as year') - ->selectRaw('SUM(amount) as amount') - ->having('amount', '>', 0) - ->where(function (Builder $qry) { - foreach ($this->years as $year) { - $qry->orWhereYear('date', '=', $year); - } - }) - ->groupBy('currency') - ->groupBy('channel') - ->groupBy('year') - ->orderBy('year') - ->orderBy('currency') - ->orderBy('channel') - ->get(); + if ($includeChannels) { + $this->usedCurrenciesChannels = Donation::select('currency', 'channel') + ->selectRaw('YEAR(date) as year') + ->selectRaw('SUM(amount) as amount') + ->having('amount', '>', 0) + ->where(function (Builder $qry) { + foreach ($this->years as $year) { + $qry->orWhereYear('date', '=', $year); + } + }) + ->groupBy('currency') + ->groupBy('channel') + ->groupBy('year') + ->orderBy('year') + ->orderBy('currency') + ->orderBy('channel') + ->get(); + } } public function query(): Builder { - return Donor::orderBy('first_name') + return Donor::query() + ->with(['comments', 'tags']) + ->orderBy('first_name') ->orderBy('last_name') - ->orderBy('company'); + ->orderBy('company') + ->when(! $this->showAllDonors, fn (Builder $q) => $q->whereHas('donations', function (Builder $query) { + $query->where(function (Builder $qry) { + foreach ($this->years as $year) { + $qry->orWhereYear('date', $year); + } + }); + }) + ); } public function title(): string @@ -86,8 +98,10 @@ public function headings(): array foreach ($this->years as $year) { $headings[] = __('Donations').' '.$year; } - foreach ($this->usedCurrenciesChannels as $cc) { - $headings[] = $cc->currency.' via '.$cc->channel.' in '.$cc->year; + if ($this->includeChannels) { + foreach ($this->usedCurrenciesChannels as $cc) { + $headings[] = $cc->currency.' via '.$cc->channel.' in '.$cc->year; + } } } @@ -119,12 +133,14 @@ public function map($donor): array foreach ($this->years as $year) { $map[] = $donor->amountPerYear($year) ?? 0; } - $amounts = $donor->amountByChannelCurrencyYear(); - foreach ($this->usedCurrenciesChannels as $cc) { - $map[] = optional($amounts->where('year', $cc->year) - ->where('currency', $cc->currency) - ->where('channel', $cc->channel) - ->first())->total ?? null; + if ($this->includeChannels) { + $amounts = $donor->amountByChannelCurrencyYear(); + foreach ($this->usedCurrenciesChannels as $cc) { + $map[] = optional($amounts->where('year', $cc->year) + ->where('currency', $cc->currency) + ->where('channel', $cc->channel) + ->first())->total ?? null; + } } } @@ -137,13 +153,17 @@ public function columnFormats(): array if (Auth::user()->can('viewAny', Donation::class)) { foreach ($this->years as $year) { $formats['O'] = config('fundraising.base_currency_excel_format'); - $formats['P'] = config('fundraising.base_currency_excel_format'); + if (count($this->years) == 2) { + $formats['P'] = config('fundraising.base_currency_excel_format'); + } } - $i = Coordinate::columnIndexFromString(count($this->years) == 2 ? 'P' : 'O'); - foreach ($this->usedCurrenciesChannels as $cc) { - $i++; - $column = Coordinate::stringFromColumnIndex($i); - $formats[$column] = config('fundraising.currencies_excel_format')[$cc->currency]; + if ($this->includeChannels) { + $i = Coordinate::columnIndexFromString(count($this->years) == 2 ? 'P' : 'O'); + foreach ($this->usedCurrenciesChannels as $cc) { + $i++; + $column = Coordinate::stringFromColumnIndex($i); + $formats[$column] = config('fundraising.currencies_excel_format')[$cc->currency]; + } } } diff --git a/app/Http/Controllers/Fundraising/API/DonationController.php b/app/Http/Controllers/Fundraising/API/DonationController.php index cecd804f6..26df7aa82 100644 --- a/app/Http/Controllers/Fundraising/API/DonationController.php +++ b/app/Http/Controllers/Fundraising/API/DonationController.php @@ -188,17 +188,17 @@ public function export(Request $request): BinaryFileResponse $extension = $request->input('format', 'xlsx'); + $includeAddress = $request->boolean('includeAddress'); + $year = $request->input('year', null); + $file_name = sprintf( '%s - %s (%s).%s', config('app.name'), - __('Donations'), - Carbon::now()->toDateString(), + __('Donations').($year !== null ? (' '.intval($year)) : ''), + __('as of :date', ['date' => Carbon::now()->isoFormat('LL')]), $extension ); - $includeAddress = $request->boolean('includeAddress'); - $year = $request->input('year', null); - return (new DonationsExport(includeAddress: $includeAddress, year: $year))->download($file_name); } diff --git a/app/Http/Controllers/Fundraising/API/DonorController.php b/app/Http/Controllers/Fundraising/API/DonorController.php index 523727c2d..32a2b1564 100644 --- a/app/Http/Controllers/Fundraising/API/DonorController.php +++ b/app/Http/Controllers/Fundraising/API/DonorController.php @@ -195,22 +195,26 @@ public function export(Request $request): BinaryFileResponse $request->validate([ 'format' => Rule::in('xlsx'), + 'includeChannels' => 'boolean', + 'showAllDonors' => 'boolean', 'year' => ['integer', Rule::in(Donation::years())], ]); $extension = $request->input('format', 'xlsx'); + $year = $request->input('year', null); + $includeChannels = $request->boolean('includeChannels'); + $showAllDonors = $request->boolean('showAllDonors'); + $file_name = sprintf( '%s - %s (%s).%s', config('app.name'), - __('Donors'), - Carbon::now()->toDateString(), + __('Donors').($year !== null ? (' '.intval($year)) : ''), + __('as of :date', ['date' => Carbon::now()->isoFormat('LL')]), $extension ); - $year = $request->input('year', null); - - return (new DonorsExport($year))->download($file_name); + return (new DonorsExport(year: $year, includeChannels: $includeChannels, showAllDonors: $showAllDonors))->download($file_name); } public function budgets(Donor $donor): JsonResource diff --git a/lang/de.json b/lang/de.json index 2af3d0407..3fdf53f63 100644 --- a/lang/de.json +++ b/lang/de.json @@ -842,5 +842,9 @@ "Export donations": "Spenden exportieren", "Include address of donor": "Inlusive Adressen der Spender", "Export donors": "Spender exportieren", - "Current and last year": "Dieses und letztes Jahr" + "Current and last year": "Dieses und letztes Jahr", + "as of :date": "Stand :date", + "Include channels and currencies": "Inklusive Kanälen und Währungen", + "Show donations of year": "Zeige Spenden vom Jahr", + "Include all donors which did not donate in the selected year": "Inklusive sämtlicher Spender welche im ausgewählten Jahr nichts gespendet haben" } diff --git a/resources/js/api/fundraising/donations.js b/resources/js/api/fundraising/donations.js index a42e06a96..70ca4cc8e 100644 --- a/resources/js/api/fundraising/donations.js +++ b/resources/js/api/fundraising/donations.js @@ -37,6 +37,6 @@ export default { }, async export(params) { const url = route("api.fundraising.donations.export", params); - return await api.get(url); + return await api.download(url); }, } diff --git a/resources/js/api/fundraising/donors.js b/resources/js/api/fundraising/donors.js index 9f6743261..8b8308d4c 100644 --- a/resources/js/api/fundraising/donors.js +++ b/resources/js/api/fundraising/donors.js @@ -62,5 +62,9 @@ export default { ...params }); return await api.get(url); - } + }, + async export(params) { + const url = route("api.fundraising.donors.export", params); + return await api.download(url); + }, } diff --git a/resources/js/components/fundraising/DonationsExportDialog.vue b/resources/js/components/fundraising/DonationsExportDialog.vue deleted file mode 100644 index d28fe196f..000000000 --- a/resources/js/components/fundraising/DonationsExportDialog.vue +++ /dev/null @@ -1,121 +0,0 @@ - - - diff --git a/resources/js/components/fundraising/DonorsExportDialog.vue b/resources/js/components/fundraising/DonorsExportDialog.vue deleted file mode 100644 index edcf790ad..000000000 --- a/resources/js/components/fundraising/DonorsExportDialog.vue +++ /dev/null @@ -1,112 +0,0 @@ - - - diff --git a/resources/js/pages/fundraising/DonationsIndexPage.vue b/resources/js/pages/fundraising/DonationsIndexPage.vue index 36fb72d5f..1f73d8bd8 100644 --- a/resources/js/pages/fundraising/DonationsIndexPage.vue +++ b/resources/js/pages/fundraising/DonationsIndexPage.vue @@ -24,22 +24,17 @@ -

- -

diff --git a/resources/js/pages/fundraising/FundraisingIndexPage.vue b/resources/js/pages/fundraising/FundraisingIndexPage.vue index 9439ad099..1da3bc21c 100644 --- a/resources/js/pages/fundraising/FundraisingIndexPage.vue +++ b/resources/js/pages/fundraising/FundraisingIndexPage.vue @@ -5,28 +5,44 @@ :value="error" @retry="fetchData" /> - + - - - - {{ $t('Last registered donor') }}: - {{ data.last_registered_donor.full_name }}
- {{ dateFormat(this.data.last_registered_donor.created_at) }} -
+ + + + + +
- - - - {{ $t('Last registered donation') }}: - {{ data.last_registered_donation.amount }} {{ data.last_registered_donation.currency }}
- {{ data.last_registered_donation.donor }}, {{ dateFormat(this.data.last_registered_donation.created_at) }} -
+ + + + + +
@@ -34,7 +50,7 @@ @@ -75,6 +91,12 @@ export default { icon: "upload", text: this.$t("Import"), show: this.can("manage-fundraising-entities") + }, + { + to: { name: "fundraising.export" }, + icon: "download", + text: this.$t("Export"), + show: this.can("view-fundraising-entities") } ], error: null, diff --git a/resources/js/plugins/bootstrap.js b/resources/js/plugins/bootstrap.js index 2f3c8302c..79da7b25b 100644 --- a/resources/js/plugins/bootstrap.js +++ b/resources/js/plugins/bootstrap.js @@ -28,7 +28,8 @@ import { PopoverPlugin, SpinnerPlugin, TabsPlugin, - TablePlugin + TablePlugin, + SkeletonPlugin } from "bootstrap-vue"; Vue.use(AlertPlugin); @@ -59,3 +60,4 @@ Vue.use(PopoverPlugin); Vue.use(SpinnerPlugin); Vue.use(TabsPlugin); Vue.use(TablePlugin); +Vue.use(SkeletonPlugin); diff --git a/resources/js/router/fundraising.js b/resources/js/router/fundraising.js index 93af5e9c1..31d437d62 100644 --- a/resources/js/router/fundraising.js +++ b/resources/js/router/fundraising.js @@ -246,5 +246,26 @@ export default [ ] }), } + }, + { + path: "/fundraising/export", + name: "fundraising.export", + components: { + default: () => import("@/pages/fundraising/FundraisingExportPage.vue"), + breadcrumbs: BreadcrumbsNav, + }, + props: { + breadcrumbs: () => ({ + items: [ + { + text: i18n.t('Donation Management'), + to: { name: 'fundraising.index' } + }, + { + text: i18n.t('Export'), + } + ] + }), + } } ]; diff --git a/resources/js/vue-i18n-locales.generated.js b/resources/js/vue-i18n-locales.generated.js index 9d63c3658..75342a1a3 100644 --- a/resources/js/vue-i18n-locales.generated.js +++ b/resources/js/vue-i18n-locales.generated.js @@ -844,6 +844,10 @@ export default { "Include address of donor": "Inlusive Adressen der Spender", "Export donors": "Spender exportieren", "Current and last year": "Dieses und letztes Jahr", + "as of {date}": "Stand {date}", + "Include channels and currencies": "Inklusive Kanälen und Währungen", + "Show donations of year": "Zeige Spenden vom Jahr", + "Include all donors which did not donate in the selected year": "Inklusive sämtlicher Spender welche im ausgewählten Jahr nichts gespendet haben", "app": [], "auth": { "failed": "Diese Kombination aus Zugangsdaten wurden nicht in unserer Datenbank gefunden.",