Bugzilla – Attachment 187638 Details for
Bug 39320
Create a 'landing page' for ERM
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 39320: Add ERM specific dashboard widgets
Bug-39320-Add-ERM-specific-dashboard-widgets.patch (text/plain), 22.57 KB, created by
Pedro Amorim
on 2025-10-09 09:01:33 UTC
(
hide
)
Description:
Bug 39320: Add ERM specific dashboard widgets
Filename:
MIME Type:
Creator:
Pedro Amorim
Created:
2025-10-09 09:01:33 UTC
Size:
22.57 KB
patch
obsolete
>From a6e77c73c8395d14e5a0a99cb209579db2d63dee Mon Sep 17 00:00:00 2001 >From: Pedro Amorim <pedro.amorim@ptfs-europe.com> >Date: Tue, 25 Feb 2025 16:06:24 +0000 >Subject: [PATCH] Bug 39320: Add ERM specific dashboard widgets > >Sponsored-by: UKHSA (UK Health Security Agency) > >Signed-off-by: Jeremy Evans <Jeremy.Evans@ukhsa.gov.uk> >--- > .../ModuleDashboard/Widgets/ERMCounts.vue | 144 +++++++++++++ > .../Widgets/ERMLatestSUSHIJobs.vue | 147 +++++++++++++ > .../Widgets/ERMLicensesNeedingAction.vue | 199 ++++++++++++++++++ > .../Widgets/ERMRunUsageReport.vue | 113 ++++++++++ > 4 files changed, 603 insertions(+) > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/ModuleDashboard/Widgets/ERMCounts.vue > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/ModuleDashboard/Widgets/ERMLatestSUSHIJobs.vue > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/ModuleDashboard/Widgets/ERMLicensesNeedingAction.vue > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/ModuleDashboard/Widgets/ERMRunUsageReport.vue > >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ModuleDashboard/Widgets/ERMCounts.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ModuleDashboard/Widgets/ERMCounts.vue >new file mode 100644 >index 00000000000..651c080c5bf >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ModuleDashboard/Widgets/ERMCounts.vue >@@ -0,0 +1,144 @@ >+<template> >+ <WidgetWrapper v-bind="widgetWrapperProps"> >+ <template #default> >+ <p class="text-left"> >+ {{ $__("There are") }} >+ <template >+ v-for="(definition, index) in countDefinitions" >+ :key="index" >+ > >+ <strong> >+ <a >+ v-if="definition.page" >+ href="#" >+ @click.prevent="goToPage(definition.page)" >+ > >+ {{ definition.count }} >+ <span v-if="definition.count === 1">{{ >+ definition.labelSingular >+ }}</span> >+ <span v-else>{{ definition.labelPlural }}</span> >+ </a> >+ <span v-else> >+ {{ definition.count }} >+ <span v-if="definition.count === 1">{{ >+ definition.labelSingular >+ }}</span> >+ <span v-else>{{ definition.labelPlural }}</span> >+ </span> >+ </strong> >+ <template v-if="index < countDefinitions.length - 1" >+ >, </template >+ > >+ <template v-else>.</template> >+ </template> >+ </p> >+ </template> >+ </WidgetWrapper> >+</template> >+<script> >+import { reactive } from "vue"; >+import { useBaseWidget } from "../../../composables/base-widget.js"; >+import { APIClient } from "../../../fetch/api-client.js"; >+import WidgetWrapper from "../WidgetWrapper.vue"; >+import { useRouter } from "vue-router"; >+import { $__ } from "@koha-vue/i18n"; >+ >+export default { >+ name: "ERMCounts", >+ components: { WidgetWrapper }, >+ props: { >+ display: String, >+ }, >+ emits: ["removed", "added", "moveWidget"], >+ setup(props, { emit }) { >+ const router = useRouter(); >+ const baseWidget = useBaseWidget( >+ { >+ id: "ERMCounts", >+ name: $__("Counts"), >+ icon: "fas fa-chart-bar", >+ description: $__( >+ "Shows the number of ERM related resources such as agreements, licenses, local packages, local titles, documents, etc." >+ ), >+ ...props, >+ }, >+ emit >+ ); >+ const countDefinitions = reactive([ >+ { >+ page: "AgreementsList", >+ name: "agreements_count", >+ labelSingular: __("agreement"), >+ labelPlural: __("agreements"), >+ count: 0, >+ }, >+ { >+ page: "LicensesList", >+ name: "licenses_count", >+ labelSingular: __("license"), >+ labelPlural: __("licenses"), >+ count: 0, >+ }, >+ { >+ name: "documents_count", >+ labelSingular: __("document"), >+ labelPlural: __("documents"), >+ count: 0, >+ }, >+ { >+ page: "EHoldingsLocalPackagesList", >+ name: "eholdings_packages_count", >+ labelSingular: __("local package"), >+ labelPlural: __("local packages"), >+ count: 0, >+ }, >+ { >+ page: "EHoldingsLocalTitlesList", >+ name: "eholdings_titles_count", >+ labelSingular: __("local title"), >+ labelPlural: __("local titles"), >+ count: 0, >+ }, >+ { >+ page: "UsageStatisticsDataProvidersList", >+ name: "usage_data_providers_count", >+ labelSingular: __("usage data provider"), >+ labelPlural: __("usage data providers"), >+ count: 0, >+ }, >+ ]); >+ >+ async function getCounts() { >+ try { >+ const response = await APIClient.erm.counts.get(); >+ >+ Object.keys(response.counts).forEach(key => { >+ const item = countDefinitions.find(i => i.name === key); >+ if (item) { >+ item.count = response.counts[key]; >+ } >+ }); >+ >+ baseWidget.loading.value = false; >+ } catch (error) { >+ console.error(error); >+ } >+ } >+ >+ baseWidget.onDashboardMounted(() => { >+ getCounts(); >+ }); >+ >+ function goToPage(page) { >+ router.push({ name: page }); >+ } >+ >+ return { >+ ...baseWidget, >+ countDefinitions, >+ goToPage, >+ }; >+ }, >+}; >+</script> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ModuleDashboard/Widgets/ERMLatestSUSHIJobs.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ModuleDashboard/Widgets/ERMLatestSUSHIJobs.vue >new file mode 100644 >index 00000000000..5b6c7e08147 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ModuleDashboard/Widgets/ERMLatestSUSHIJobs.vue >@@ -0,0 +1,147 @@ >+<template> >+ <WidgetWrapper v-bind="widgetWrapperProps"> >+ <template #default> >+ <KohaTable >+ ref="table" >+ v-bind="tableOptions" >+ :key="JSON.stringify(tableOptions)" >+ @view="viewJob" >+ /> >+ </template> >+ </WidgetWrapper> >+</template> >+ >+<script> >+import { ref, computed } from "vue"; >+import WidgetWrapper from "../WidgetWrapper.vue"; >+import KohaTable from "../../KohaTable.vue"; >+import { useBaseWidget } from "../../../composables/base-widget.js"; >+import { $__ } from "@koha-vue/i18n"; >+ >+export default { >+ name: "ERMLatestSUSHIJobs", >+ components: { WidgetWrapper, KohaTable }, >+ props: { >+ display: String, >+ }, >+ emits: ["removed", "added", "moveWidget"], >+ setup(props, { emit }) { >+ const baseWidget = useBaseWidget( >+ { >+ id: "ERMLatestSUSHIJobs", >+ name: $__("Latest SUSHI Counter jobs"), >+ icon: "fa-solid fa-gears", >+ description: $__("Show latest SUSHI Counter background jobs."), >+ loading: false, >+ ...props, >+ }, >+ emit >+ ); >+ >+ const table = ref(); >+ >+ const job_statuses = [ >+ { _id: "new", _str: $__("New") }, >+ { _id: "cancelled", _str: $__("Cancelled") }, >+ { _id: "finished", _str: $__("Finished") }, >+ { _id: "started", _str: $__("Started") }, >+ { _id: "running", _str: $__("Running") }, >+ { _id: "failed", _str: $__("Failed") }, >+ ]; >+ >+ function get_job_status(status) { >+ const status_lib = job_statuses.find(s => s._id === status); >+ return status_lib ? status_lib._str : status; >+ } >+ >+ function getTableColumns() { >+ return [ >+ { >+ title: $__("Status"), >+ data: "status", >+ searchable: true, >+ orderable: true, >+ render(data, type, row) { >+ return get_job_status(row.status).escapeHtml(); >+ }, >+ }, >+ { >+ title: $__("Data Provider"), >+ data: "data", >+ searchable: true, >+ orderable: true, >+ render(data) { >+ return ( >+ '<a href="/cgi-bin/koha/erm/eusage/usage_data_providers/' + >+ data.ud_provider_id + >+ '" class="show">' + >+ escape_str(data.ud_provider_name) + >+ "</a>" >+ ); >+ }, >+ }, >+ { >+ title: $__("Started"), >+ data: "started_date", >+ searchable: true, >+ orderable: true, >+ render(data, type, row) { >+ return $datetime(row.started_date); >+ }, >+ }, >+ { >+ title: $__("Ended"), >+ data: "ended_date", >+ searchable: true, >+ orderable: true, >+ render(data, type, row) { >+ return $datetime(row.ended_date); >+ }, >+ }, >+ ]; >+ } >+ >+ const tableOptions = computed(() => ({ >+ columns: getTableColumns(), >+ options: { >+ dom: "t", >+ pageLength: 5, >+ order: [2, "desc"], >+ }, >+ url: "/api/v1/jobs", >+ default_filters: { >+ only_current: 0, >+ type: "erm_sushi_harvester", >+ }, >+ actions: { >+ 0: ["show"], >+ "-1": [ >+ { >+ view: { >+ text: $__("View"), >+ icon: "fa fa-eye", >+ }, >+ }, >+ ], >+ }, >+ })); >+ >+ function viewJob(job, dt, event) { >+ event.preventDefault(); >+ window.open( >+ "/cgi-bin/koha/admin/background_jobs.pl?op=view&id=" + >+ encodeURIComponent(job.job_id), >+ "_blank" >+ ); >+ } >+ >+ return { >+ ...baseWidget, >+ table, >+ tableOptions, >+ viewJob, >+ getTableColumns, >+ }; >+ }, >+}; >+</script> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ModuleDashboard/Widgets/ERMLicensesNeedingAction.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ModuleDashboard/Widgets/ERMLicensesNeedingAction.vue >new file mode 100644 >index 00000000000..af520cc76bd >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ModuleDashboard/Widgets/ERMLicensesNeedingAction.vue >@@ -0,0 +1,199 @@ >+<template> >+ <WidgetWrapper v-bind="widgetWrapperProps"> >+ <template #default> >+ <KohaTable >+ ref="table" >+ v-bind="tableOptions" >+ :key="JSON.stringify(tableOptions)" >+ /> >+ </template> >+ </WidgetWrapper> >+</template> >+ >+<script> >+import { inject, ref, computed } from "vue"; >+import { storeToRefs } from "pinia"; >+import WidgetWrapper from "../WidgetWrapper.vue"; >+import KohaTable from "../../KohaTable.vue"; >+import { useBaseWidget } from "../../../composables/base-widget.js"; >+import { $__ } from "@koha-vue/i18n"; >+ >+export default { >+ name: "ERMLicensesNeedingAction", >+ components: { WidgetWrapper, KohaTable }, >+ props: { >+ display: String, >+ }, >+ emits: ["removed", "added", "moveWidget"], >+ setup(props, { emit }) { >+ const ERMStore = inject("ERMStore"); >+ const { get_lib_from_av } = ERMStore; >+ const { authorisedValues } = storeToRefs(ERMStore); >+ const av_license_statuses = authorisedValues.value.av_license_statuses; >+ >+ const table = ref(); >+ const default_settings = { >+ status: ["in_negotiation", "not_yet_active", "rejected"], >+ per_page: 5, >+ }; >+ const settings = ref(default_settings); >+ const settings_definitions = ref([ >+ { >+ name: "status", >+ type: "select", >+ label: __("Status"), >+ showInTable: true, >+ options: av_license_statuses.map(status => ({ >+ value: status.value, >+ description: status.description, >+ })), >+ allowMultipleChoices: true, >+ requiredKey: "value", >+ selectLabel: "description", >+ }, >+ { >+ name: "ended_on", >+ type: "select", >+ label: __("Ends in the next"), >+ showInTable: true, >+ options: [ >+ { value: "week", description: __("Week") }, >+ { value: "two_weeks", description: __("Two weeks") }, >+ { value: "month", description: __("Month") }, >+ { value: "two_months", description: __("Two months") }, >+ ], >+ requiredKey: "value", >+ selectLabel: "description", >+ }, >+ { >+ name: "per_page", >+ type: "select", >+ label: __("Show"), >+ showInTable: true, >+ options: [ >+ { value: 5, description: "5" }, >+ { value: 10, description: "10" }, >+ { value: 20, description: "20" }, >+ ], >+ requiredKey: "value", >+ selectLabel: "description", >+ }, >+ ]); >+ >+ function settingsToQueryParams(settings) { >+ const params = {}; >+ >+ if (settings.status && settings.status.length > 0) { >+ params["me.status"] = { >+ "-in": settings.status, >+ }; >+ } >+ >+ if (settings.ended_on) { >+ const now = new Date(); >+ let limitDate; >+ >+ switch (settings.ended_on) { >+ case "week": >+ limitDate = new Date(now.setDate(now.getDate() + 7)); >+ break; >+ case "two_weeks": >+ limitDate = new Date(now.setDate(now.getDate() + 14)); >+ break; >+ case "month": >+ limitDate = new Date(now.setMonth(now.getMonth() + 1)); >+ break; >+ case "two_months": >+ limitDate = new Date(now.setMonth(now.getMonth() + 2)); >+ break; >+ } >+ >+ if (limitDate) { >+ params["me.ended_on"] = { "<=": limitDate }; >+ } >+ } >+ >+ return params; >+ } >+ >+ const tableOptions = computed(() => ({ >+ columns: [ >+ { >+ title: __("Name"), >+ data: "name", >+ searchable: true, >+ orderable: true, >+ render(data, type, row) { >+ const name = escape_str(row.name); >+ const shortName = >+ name.length > 25 >+ ? name.substring(0, 22) + "..." >+ : name; >+ return `<a href="/cgi-bin/koha/erm/licenses/${row.license_id}" class="show">${shortName}</a>`; >+ }, >+ }, >+ { >+ title: __("Type"), >+ data: "type", >+ searchable: true, >+ orderable: true, >+ render(data, type, row) { >+ return escape_str( >+ get_lib_from_av("av_license_types", row.type) >+ ); >+ }, >+ }, >+ { >+ title: __("Status"), >+ data: "status", >+ searchable: true, >+ orderable: true, >+ render(data, type, row) { >+ return escape_str( >+ get_lib_from_av("av_license_statuses", row.status) >+ ); >+ }, >+ }, >+ { >+ title: __("Ends on"), >+ data: "ended_on", >+ searchable: true, >+ orderable: true, >+ render(data, type, row) { >+ return $date(row.ended_on); >+ }, >+ }, >+ ], >+ options: { >+ dom: "t", >+ embed: "vendor,extended_attributes,+strings", >+ pageLength: settings.value.per_page || 5, >+ }, >+ url: "/api/v1/erm/licenses", >+ default_filters: settingsToQueryParams(settings.value), >+ })); >+ >+ const baseWidget = useBaseWidget( >+ { >+ id: "ERMLicensesNeedingAction", >+ name: $__("Licenses needing action"), >+ icon: "fa fa-gavel", >+ description: $__( >+ "Show licenses that need action. It filters licenses by status and end date. This widget is configurable." >+ ), >+ loading: false, >+ settings: settings, >+ settings_definitions: settings_definitions, >+ ...props, >+ }, >+ emit >+ ); >+ >+ return { >+ ...baseWidget, >+ table, >+ tableOptions, >+ }; >+ }, >+}; >+</script> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ModuleDashboard/Widgets/ERMRunUsageReport.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ModuleDashboard/Widgets/ERMRunUsageReport.vue >new file mode 100644 >index 00000000000..0ae20b5c377 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ModuleDashboard/Widgets/ERMRunUsageReport.vue >@@ -0,0 +1,113 @@ >+<template> >+ <WidgetWrapper v-bind="widgetWrapperProps"> >+ <template #default> >+ <div v-if="!default_usage_reports.length"> >+ <div class="d-flex align-items-center alert alert-info"> >+ {{ noReportsText }} >+ </div> >+ <router-link :to="{ name: 'UsageStatisticsReportsHome' }"> >+ {{ createReportText }} >+ </router-link> >+ </div> >+ <div v-else class="d-flex align-items-center"> >+ <div class="flex-grow-1 me-2"> >+ <label for="filter" class="visually-hidden">{{ >+ pickReportText >+ }}</label> >+ <v-select >+ label="report_name" >+ :options="default_usage_reports" >+ style="min-width: 200px" >+ v-model="selected_report" >+ ></v-select> >+ </div> >+ <div> >+ <button >+ class="btn btn-primary" >+ type="button" >+ :disabled="!selected_report" >+ @click="runReport" >+ > >+ {{ runText }} >+ </button> >+ </div> >+ </div> >+ </template> >+ </WidgetWrapper> >+</template> >+ >+<script> >+import { ref } from "vue"; >+import { useRouter } from "vue-router"; >+import WidgetWrapper from "../WidgetWrapper.vue"; >+import { APIClient } from "../../../fetch/api-client.js"; >+import { useBaseWidget } from "../../../composables/base-widget.js"; >+import { $__ } from "@koha-vue/i18n"; >+ >+export default { >+ name: "ERMRunUsageReport", >+ components: { WidgetWrapper }, >+ props: { >+ display: String, >+ }, >+ emits: ["removed", "added", "moveWidget"], >+ setup(props, { emit }) { >+ const router = useRouter(); >+ const baseWidget = useBaseWidget( >+ { >+ id: "ERMRunUsageReport", >+ name: $__("Run eUsage report"), >+ icon: "fa-solid fa-file", >+ description: $__("Select a saved eUsage report to run."), >+ ...props, >+ }, >+ emit >+ ); >+ >+ const default_usage_reports = ref([]); >+ const selected_report = ref(null); >+ >+ const noReportsText = $__( >+ "No saved eUsage reports are available to run." >+ ); >+ const createReportText = $__("Create a report"); >+ const pickReportText = $__("Pick a report to run"); >+ const runText = $__("Run"); >+ >+ async function getReports() { >+ try { >+ const response = >+ await APIClient.erm.default_usage_reports.getAll(); >+ default_usage_reports.value = response; >+ } catch (error) { >+ console.error("Error getting default usage reports", error); >+ } finally { >+ baseWidget.loading.value = false; >+ } >+ } >+ >+ baseWidget.onDashboardMounted(() => { >+ getReports(); >+ }); >+ >+ function runReport() { >+ if (!selected_report.value) return; >+ router.push({ >+ name: "UsageStatisticsReportsViewer", >+ query: { data: selected_report.value.report_url_params }, >+ }); >+ } >+ >+ return { >+ ...baseWidget, >+ default_usage_reports, >+ selected_report, >+ runReport, >+ noReportsText, >+ createReportText, >+ pickReportText, >+ runText, >+ }; >+ }, >+}; >+</script> >-- >2.39.5
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 39320
:
187513
|
187514
|
187515
|
187516
|
187517
|
187518
|
187519
|
187520
|
187521
|
187522
|
187563
|
187568
|
187635
|
187636
|
187637
|
187638
|
187639
|
187640
|
187641
|
187642
|
187643
|
187644
|
187645
|
188094
|
188095
|
188096
|
188097
|
188098
|
188099
|
188100
|
188101
|
188102
|
188103
|
188104
|
188151
|
188279
|
188280
|
188281
|
188282
|
188283
|
188284
|
188285
|
188312
|
188315
|
188372
|
188375
|
188376
|
188637
|
188638
|
188639
|
188640
|
188641
|
188830
|
188831
|
188832
|
188833
|
188834
|
188835
|
188836
|
188837
|
188838
|
188839
|
188840
|
188841
|
188842
|
188843
|
188844
|
188845
|
188846
|
188847
|
188848
|
188849
|
188850
|
188851
|
188852
|
188853
|
188854
|
188855
|
188856
|
188857
|
188858
|
188859
|
188860
|
188861
|
188862
|
188863
|
188864