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:
parent
e7ad97908b
commit
6bbe1794f8
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue