Merged PR 1910: feat(remission-list, ui-tooltip): add info tooltip with performance optimization

feat(remission-list, ui-tooltip): add info tooltip with performance optimization

Add tooltip to department capacity info button with enhanced trigger management.
Optimize department list fetching to only load when search input or department
filter is active, improving initial load performance.

- Add tooltip directive to info button showing capacity details
- Implement conditional department list fetching based on input/filter presence
- Enhance tooltip directive with improved trigger management and positioning
- Update tooltip component to use modern Angular control flow syntax
- Add proper show/hide logic with trigger-specific behavior

Refs: #5255
This commit is contained in:
Nino Righi
2025-08-06 16:02:27 +00:00
committed by Andreas Schickinger
parent a0f24aac17
commit d22e320294
5 changed files with 114 additions and 54 deletions

View File

@@ -5,16 +5,16 @@
>
</filter-input-menu-button>
@if (displayCapacityValues()) {
@if (selectedDepartments()) {
<ui-toolbar class="ui-toolbar-rounded">
<span class="isa-text-body-2-regular"
><span class="isa-text-body-2-bold"
<span class="flex gap-1 isa-text-body-2-regular"
><span *uiSkeletonLoader="capacityFetching()" class="isa-text-body-2-bold"
>{{ leistung() }}/{{ maxLeistung() }}</span
>
Leistung</span
>
<span class="isa-text-body-2-regular"
><span class="isa-text-body-2-bold"
<span class="flex gap-1 isa-text-body-2-regular"
><span *uiSkeletonLoader="capacityFetching()" class="isa-text-body-2-bold"
>{{ stapel() }}/{{ maxStapel() }}</span
>
Stapel</span
@@ -23,7 +23,6 @@
class="w-6 h-6 flex items-center justify-center text-isa-accent-blue"
uiTooltip
[title]="'Stapel/Leistungsplätze'"
[content]="''"
[triggerOn]="['click', 'hover']"
>
<ng-icon size="1.5rem" name="isaOtherInfo"></ng-icon>

View File

@@ -18,6 +18,7 @@ import { ToolbarComponent } from '@isa/ui/toolbar';
import { TooltipDirective } from '@isa/ui/tooltip';
import { NgIconComponent, provideIcons } from '@ng-icons/core';
import { createRemissionCapacityResource } from '../resources';
import { SkeletonLoaderDirective } from '@isa/ui/skeleton-loader';
@Component({
selector: 'remi-feature-remission-list-department-elements',
@@ -30,6 +31,7 @@ import { createRemissionCapacityResource } from '../resources';
ToolbarComponent,
TooltipDirective,
NgIconComponent,
SkeletonLoaderDirective,
],
})
export class RemissionListDepartmentElementsComponent {
@@ -75,12 +77,19 @@ export class RemissionListDepartmentElementsComponent {
};
});
/**
* Computed signal to get the current value of the capacity resource.
* @returns {Array} The current capacity values or an empty array if not available.
*/
capacityResourceValue = computed(() => this.capacityResource.value());
displayCapacityValues = computed(() => {
const value = this.capacityResourceValue();
return !!value && value?.length > 0;
});
/**
* Computed signal to check if the capacity resource is currently fetching data.
* @returns {boolean} True if the resource is loading, false otherwise.
*/
capacityFetching = computed(
() => this.capacityResource.status() === 'loading',
);
leistungValues = computed(() => {
const value = this.capacityResourceValue();

View File

@@ -73,18 +73,15 @@ export const createRemissionListResource = (
let res: ListResponseArgs<RemissionItem> | undefined = undefined;
const queryToken = { ...params.queryToken };
const exactSearch = isExactSearch(queryToken, params.searchTrigger);
// #5128 #5234 Bei Exact Search soll er über Alle Listen nur mit dem Input ohne aktive Filter / orderBy suchen
const isExactSearch =
params.searchTrigger === 'scan' || isEan(queryToken?.input?.['qs']);
if (isExactSearch) {
if (exactSearch) {
queryToken.filter = {};
queryToken.orderBy = [];
}
if (
isExactSearch ||
exactSearch ||
params.remissionListType === RemissionListType.Pflicht
) {
const fetchListResponse = await remissionSearchService.fetchList(
@@ -99,8 +96,8 @@ export const createRemissionListResource = (
}
if (
isExactSearch ||
params.remissionListType === RemissionListType.Abteilung
exactSearch ||
canFetchDepartmentList(queryToken, params.remissionListType)
) {
const fetchDepartmentListResponse =
await remissionSearchService.fetchDepartmentList(
@@ -129,36 +126,88 @@ export const createRemissionListResource = (
throw new ResponseArgsError(res);
}
// Sort items: manually-added items first, then by created date (latest first)
if (res && res.result && Array.isArray(res.result)) {
res.result.sort((a, b) => {
const aIsManuallyAdded = a.source === 'manually-added';
const bIsManuallyAdded = b.source === 'manually-added';
// First priority: manually-added items come first
if (aIsManuallyAdded && !bIsManuallyAdded) {
return -1;
}
if (!aIsManuallyAdded && bIsManuallyAdded) {
return 1;
}
// Second priority: sort by created date (latest first)
if (a.created && b.created) {
const dateA = parseISO(a.created);
const dateB = parseISO(b.created);
return compareDesc(dateA, dateB); // Descending order (latest first)
}
// Handle cases where created date might be missing
if (a.created && !b.created) return -1;
if (!a.created && b.created) return 1;
return 0;
});
sortResponseResult(res);
}
return res;
},
});
};
/**
* Sorts the response result of remission items.
* - Manually added items come first.
* - Then sorted by created date in descending order (latest first).
*
* @param {ListResponseArgs<RemissionItem>} resopnse - The response containing remission items to sort
*/
const sortResponseResult = (
resopnse: ListResponseArgs<RemissionItem>,
): void => {
resopnse.result.sort((a, b) => {
const aIsManuallyAdded = a.source === 'manually-added';
const bIsManuallyAdded = b.source === 'manually-added';
// First priority: manually-added items come first
if (aIsManuallyAdded && !bIsManuallyAdded) {
return -1;
}
if (!aIsManuallyAdded && bIsManuallyAdded) {
return 1;
}
// Second priority: sort by created date (latest first)
if (a.created && b.created) {
const dateA = parseISO(a.created);
const dateB = parseISO(b.created);
return compareDesc(dateA, dateB); // Descending order (latest first)
}
// Handle cases where created date might be missing
if (a.created && !b.created) return -1;
if (!a.created && b.created) return 1;
return 0;
});
};
// #5128 #5234 Bei Exact Search soll er über Alle Listen nur mit dem Input ohne aktive Filter / orderBy suchen
/**
* Checks if the query token is an exact search based on the search trigger.
* An exact search is defined as:
* - Triggered by 'scan'
* - Or if the query token input contains a valid EAN (barcode) in 'qs'
* @param {QueryTokenInput} queryToken - The query token containing input parameters
* @param {SearchTrigger} searchTrigger - The trigger that initiated the search
* @returns {boolean} True if the search is exact, false otherwise
*/
const isExactSearch = (
queryToken: QueryTokenInput,
searchTrigger: SearchTrigger | 'reload' | 'initial',
): boolean => {
return searchTrigger === 'scan' || isEan(queryToken?.input?.['qs']);
};
// #5255 Performance optimization for initial department list fetch
/**
* Checks if the query token allows fetching the department list.
* This is true if the remission list type is 'Abteilung' and either:
* - There is a search input (queryToken.input['qs'])
* - There is an active filter for 'abteilungen'
*
* @param {QueryTokenInput} queryToken - The query token containing input and filter
* @param {RemissionListType} remissionListType - The type of remission list being queried
* @returns {boolean} True if the department list can be fetched, false otherwise
*/
const canFetchDepartmentList = (
queryToken: QueryTokenInput,
remissionListType: RemissionListType,
): boolean => {
const hasInput = queryToken?.input?.['qs'];
const hasAbteilungFilter = queryToken?.filter?.['abteilungen'];
return (
remissionListType === RemissionListType.Abteilung &&
(hasInput || hasAbteilungFilter)
);
};

View File

@@ -3,11 +3,14 @@
{{ title() }}
</div>
}
<div class="ui-tooltip-content">
@let t = template();
@if (t) {
<ng-container *ngTemplateOutlet="t"></ng-container>
} @else {
{{ text() }}
}
</div>
@if (text() || template()) {
<div class="ui-tooltip-content">
@let t = template();
@if (t) {
<ng-container *ngTemplateOutlet="t"></ng-container>
} @else {
{{ text() }}
}
</div>
}

View File

@@ -82,8 +82,8 @@ export class TooltipDirective implements OnDestroy {
/** Optional title for the tooltip. */
title = input<string>();
/** Content for the tooltip. Can be a string or a TemplateRef. This input is required. */
content = input.required<string | TemplateRef<unknown>>();
/** Content for the tooltip. Can be a string or a TemplateRef. */
content = input<string | TemplateRef<unknown>>();
/**
* Array of triggers that will cause the tooltip to show.