Bug 39320: Add ERM specific dashboard widgets

Sponsored-by: UKHSA (UK Health Security Agency)

Signed-off-by: Jeremy Evans <Jeremy.Evans@ukhsa.gov.uk>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
This commit is contained in:
Pedro Amorim 2025-02-25 16:06:24 +00:00 committed by Lucas Gass
parent e7ad97908b
commit 6bbe1794f8
No known key found for this signature in database
GPG Key ID: 60B447398B0D36CA
4 changed files with 603 additions and 0 deletions

View File

@ -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"
>,&nbsp;</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>

View File

@ -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>

View File

@ -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>

View File

@ -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>