Merged PR 2055: feature(ui-label, ahf, warenausgabe, customer-orders): Added and Updated Labe...

feature(ui-label, ahf, warenausgabe, customer-orders): Added and Updated Label Library and Label to the Views, Updated Positioning

Ref: #5479
This commit is contained in:
Nino Righi
2025-11-28 12:37:11 +00:00
committed by Lorenz Hilpert
parent a5bb8b2895
commit 41630d5d7c
47 changed files with 1830 additions and 1549 deletions

View File

@@ -29,12 +29,33 @@ Create well-formatted commit: $ARGUMENTS
6. If multiple distinct changes are detected, suggests breaking the commit into multiple smaller commits
7. For each commit (or the single commit if not split), creates a commit message using emoji conventional commit format
## Determining the Scope
The scope in commit messages MUST be the `name` field from the affected library's `project.json`:
1. **Check the file path**: `libs/ui/label/src/...` → Look at `libs/ui/label/project.json`
2. **Read the project name**: The `"name"` field (e.g., `"name": "ui-label"`)
3. **Use that as scope**: `feat(ui-label): ...`
**Examples:**
- File: `libs/remission/feature/remission-list/src/...` → Scope: `remission-feature-remission-list`
- File: `libs/ui/notice/src/...` → Scope: `ui-notice`
- File: `apps/isa-app/src/...` → Scope: `isa-app`
**Multi-project changes:**
- If changes span 2-3 related projects, use the primary one or list them: `feat(ui-label, ui-notice): ...`
- If changes span many unrelated projects, split into separate commits
## Best Practices for Commits
- **Verify before committing**: Ensure code is linted, builds correctly, and documentation is updated
- **Atomic commits**: Each commit should contain related changes that serve a single purpose
- **Split large changes**: If changes touch multiple concerns, split them into separate commits
- **Conventional commit format**: Use the format `<type>: <description>` where type is one of:
- **Conventional commit format**: Use the format `<type>(<scope>): <description>` where:
- **scope**: The project name from `project.json` of the affected library (e.g., `ui-label`, `crm-feature-checkout`)
- Determine the scope by checking which library/project the changes belong to
- If changes span multiple projects, use the primary affected project or split into multiple commits
- type is one of:
- `feat`: A new feature
- `fix`: A bug fix
- `docs`: Documentation changes
@@ -122,33 +143,33 @@ When analyzing the diff, consider splitting commits based on these criteria:
## Examples
Good commit messages:
- ✨ feat: add user authentication system
- 🐛 fix: resolve memory leak in rendering process
- 📝 docs: update API documentation with new endpoints
- ♻️ refactor: simplify error handling logic in parser
- 🚨 fix: resolve linter warnings in component files
- 🧑‍💻 chore: improve developer tooling setup process
- 👔 feat: implement business logic for transaction validation
- 🩹 fix: address minor styling inconsistency in header
- 🚑️ fix: patch critical security vulnerability in auth flow
- 🎨 style: reorganize component structure for better readability
- 🔥 fix: remove deprecated legacy code
- 🦺 feat: add input validation for user registration form
- 💚 fix: resolve failing CI pipeline tests
- 📈 feat: implement analytics tracking for user engagement
- 🔒️ fix: strengthen authentication password requirements
- ♿️ feat: improve form accessibility for screen readers
Good commit messages (scope = project name from project.json):
- ✨ feat(auth-feature-login): add user authentication system
- 🐛 fix(ui-renderer): resolve memory leak in rendering process
- 📝 docs(crm-api): update API documentation with new endpoints
- ♻️ refactor(shared-utils): simplify error handling logic in parser
- 🚨 fix(ui-label): resolve linter warnings in component files
- 🧑‍💻 chore(dev-tools): improve developer tooling setup process
- 👔 feat(checkout-feature): implement business logic for transaction validation
- 🩹 fix(ui-header): address minor styling inconsistency in header
- 🚑️ fix(auth-core): patch critical security vulnerability in auth flow
- 🎨 style(ui-components): reorganize component structure for better readability
- 🔥 fix(legacy-module): remove deprecated legacy code
- 🦺 feat(user-registration): add input validation for user registration form
- 💚 fix(ci-config): resolve failing CI pipeline tests
- 📈 feat(analytics-feature): implement analytics tracking for user engagement
- 🔒️ fix(auth-password): strengthen authentication password requirements
- ♿️ feat(ui-forms): improve form accessibility for screen readers
Example of splitting commits:
- First commit: ✨ feat: add new solc version type definitions
- Second commit: 📝 docs: update documentation for new solc versions
- Third commit: 🔧 chore: update package.json dependencies
- Fourth commit: 🏷️ feat: add type definitions for new API endpoints
- Fifth commit: 🧵 feat: improve concurrency handling in worker threads
- Sixth commit: 🚨 fix: resolve linting issues in new code
- Seventh commit: ✅ test: add unit tests for new solc version features
- Eighth commit: 🔒️ fix: update dependencies with security vulnerabilities
- First commit: ✨ feat(ui-label): add prio-label component with variant styles
- Second commit: 📝 docs(ui-label): update README with usage examples
- Third commit: 🔧 chore(ui-notice): scaffold new notice library
- Fourth commit: 🏷️ feat(shared-types): add type definitions for new API endpoints
- Fifth commit: ♻️ refactor(pickup-shelf): update components to use new label
- Sixth commit: 🚨 fix(remission-list): resolve linting issues in new code
- Seventh commit: ✅ test(ui-label): add unit tests for prio-label component
- Eighth commit: 🔒️ fix(deps): update dependencies with security vulnerabilities
## Command Options

View File

@@ -1,29 +1,66 @@
@if (orderItem$ | async; as orderItem) {
<div #features class="page-customer-order-details-item__features">
@if (orderItem?.features?.prebooked) {
<img [uiOverlayTrigger]="prebookedTooltip" src="/assets/images/tag_icon_preorder.svg" [alt]="orderItem?.features?.prebooked" />
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #prebookedTooltip [closeable]="true">
<img
[uiOverlayTrigger]="prebookedTooltip"
src="/assets/images/tag_icon_preorder.svg"
[alt]="orderItem?.features?.prebooked"
/>
<ui-tooltip
yPosition="above"
xPosition="after"
[yOffset]="-11"
[xOffset]="-8"
#prebookedTooltip
[closeable]="true"
>
Artikel wird für Sie vorgemerkt.
</ui-tooltip>
}
@if (notificationsSent$ | async; as notificationsSent) {
@if (notificationsSent?.NOTIFICATION_EMAIL) {
<img [uiOverlayTrigger]="emailTooltip" src="/assets/images/email_bookmark.svg" />
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #emailTooltip [closeable]="true">
<img
[uiOverlayTrigger]="emailTooltip"
src="/assets/images/email_bookmark.svg"
/>
<ui-tooltip
yPosition="above"
xPosition="after"
[yOffset]="-11"
[xOffset]="-8"
#emailTooltip
[closeable]="true"
>
Per E-Mail benachrichtigt
<br />
@for (notification of notificationsSent?.NOTIFICATION_EMAIL; track notification) {
@for (
notification of notificationsSent?.NOTIFICATION_EMAIL;
track notification
) {
{{ notification | date: 'dd.MM.yyyy | HH:mm' }} Uhr
<br />
}
</ui-tooltip>
}
@if (notificationsSent?.NOTIFICATION_SMS) {
<img [uiOverlayTrigger]="smsTooltip" src="/assets/images/sms_bookmark.svg" />
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #smsTooltip [closeable]="true">
<img
[uiOverlayTrigger]="smsTooltip"
src="/assets/images/sms_bookmark.svg"
/>
<ui-tooltip
yPosition="above"
xPosition="after"
[yOffset]="-11"
[xOffset]="-8"
#smsTooltip
[closeable]="true"
>
Per SMS benachrichtigt
<br />
@for (notification of notificationsSent?.NOTIFICATION_SMS; track notification) {
@for (
notification of notificationsSent?.NOTIFICATION_SMS;
track notification
) {
{{ notification | date: 'dd.MM.yyyy | HH:mm' }} Uhr
<br />
}
@@ -33,7 +70,10 @@
</div>
<div class="page-customer-order-details-item__item-container">
<div class="page-customer-order-details-item__thumbnail">
<img [src]="orderItem.product?.ean | productImage" [alt]="orderItem.product?.name" />
<img
[src]="orderItem.product?.ean | productImage"
[alt]="orderItem.product?.name"
/>
</div>
<div class="page-customer-order-details-item__details">
<div class="flex flex-row justify-between items-start mb-[1.3125rem]">
@@ -42,19 +82,29 @@
#elementDistance="uiElementDistance"
[style.max-width.px]="elementDistance.distanceChange | async"
class="flex flex-col"
>
<div class="font-normal mb-[0.375rem]">{{ orderItem.product?.contributors }}</div>
>
@if (hasRewardPoints$ | async) {
<ui-label class="w-10 mb-2">Prämie</ui-label>
}
<div class="font-normal mb-[0.375rem]">
{{ orderItem.product?.contributors }}
</div>
<div>{{ orderItem.product?.name }}</div>
</h3>
<div class="history-wrapper flex flex-col items-end justify-center">
<button class="cta-history text-p1" (click)="historyClick.emit(orderItem)">Historie</button>
<button
class="cta-history text-p1"
(click)="historyClick.emit(orderItem)"
>
Historie
</button>
@if (selectable$ | async) {
<input
[ngModel]="selected$ | async"
(ngModelChange)="setSelected($event)"
class="isa-select-bullet mt-4"
type="checkbox"
/>
/>
}
</div>
</div>
@@ -72,19 +122,26 @@
[showSpinner]="false"
></ui-quantity-dropdown>
}
<span class="overall-quantity">(von {{ orderItem?.overallQuantity }})</span>
<span class="overall-quantity"
>(von {{ orderItem?.overallQuantity }})</span
>
</div>
</div>
@if (!!orderItem.product?.formatDetail) {
<div class="detail">
<div class="label">Format</div>
<div class="value">
@if (orderItem?.product?.format && orderItem?.product?.format !== 'UNKNOWN') {
@if (
orderItem?.product?.format &&
orderItem?.product?.format !== 'UNKNOWN'
) {
<img
class="format-icon"
[src]="'/assets/images/Icon_' + orderItem.product?.format + '.svg'"
[src]="
'/assets/images/Icon_' + orderItem.product?.format + '.svg'
"
alt="format icon"
/>
/>
}
<span>{{ orderItem.product?.formatDetail }}</span>
</div>
@@ -96,10 +153,17 @@
<div class="value">{{ orderItem.product?.ean }}</div>
</div>
}
@if (orderItem.price !== undefined) {
@if (orderItem.price !== undefined || (hasRewardPoints$ | async)) {
<div class="detail">
<div class="label">Preis</div>
<div class="value">{{ orderItem.price | currency: 'EUR' }}</div>
@if (hasRewardPoints$ | async) {
<div class="label">Prämie</div>
<div class="value">
{{ rewardPoints$ | async | number: '1.0-0' }} Lesepunkte
</div>
} @else {
<div class="label">Preis</div>
<div class="value">{{ orderItem.price | currency: 'EUR' }}</div>
}
</div>
}
@if (!!orderItem.retailPrice?.vat?.inPercent) {
@@ -133,14 +197,23 @@
orderItemFeature(orderItem) === 'Versand' ||
orderItemFeature(orderItem) === 'B2B-Versand' ||
orderItemFeature(orderItem) === 'DIG-Versand'
) {
{{ orderItem?.estimatedDelivery ? 'Lieferung zwischen' : 'Lieferung ab' }}
) {
{{
orderItem?.estimatedDelivery
? 'Lieferung zwischen'
: 'Lieferung ab'
}}
}
@if (orderItemFeature(orderItem) === 'Abholung' || orderItemFeature(orderItem) === 'Rücklage') {
@if (
orderItemFeature(orderItem) === 'Abholung' ||
orderItemFeature(orderItem) === 'Rücklage'
) {
Abholung ab
}
</div>
@if (!!orderItem?.estimatedDelivery || !!orderItem?.estimatedShippingDate) {
@if (
!!orderItem?.estimatedDelivery || !!orderItem?.estimatedShippingDate
) {
<div class="value bg-[#D8DFE5] rounded w-max px-2">
@if (!!orderItem?.estimatedDelivery) {
{{ orderItem?.estimatedDelivery?.start | date: 'dd.MM.yy' }} und
@@ -155,14 +228,22 @@
</div>
@if (getOrderItemTrackingData(orderItem); as trackingData) {
<div class="page-customer-order-details-item__tracking-details">
<div class="label">{{ trackingData.length > 1 ? 'Sendungsnummern' : 'Sendungsnummer' }}</div>
<div class="label">
{{ trackingData.length > 1 ? 'Sendungsnummern' : 'Sendungsnummer' }}
</div>
@for (tracking of trackingData; track tracking) {
@if (tracking.trackingProvider === 'DHL' && !isNative) {
<a class="value text-[#0556B4]" [href]="getTrackingNumberLink(tracking.trackingNumber)" target="_blank">
<a
class="value text-[#0556B4]"
[href]="getTrackingNumberLink(tracking.trackingNumber)"
target="_blank"
>
{{ tracking.trackingProvider }}: {{ tracking.trackingNumber }}
</a>
} @else {
<p class="value">{{ tracking.trackingProvider }}: {{ tracking.trackingNumber }}</p>
<p class="value">
{{ tracking.trackingProvider }}: {{ tracking.trackingNumber }}
</p>
}
}
</div>
@@ -206,7 +287,9 @@
@if (!!receipt?.printedDate) {
<div class="detail">
<div class="label">Erstellt am</div>
<div class="value">{{ receipt?.printedDate | date: 'dd.MM.yy | HH:mm' }} Uhr</div>
<div class="value">
{{ receipt?.printedDate | date: 'dd.MM.yy | HH:mm' }} Uhr
</div>
</div>
}
@if (!!receipt?.receiptText) {
@@ -219,12 +302,20 @@
<div class="detail">
<div class="label">Belegart</div>
<div class="value">
{{ receipt?.receiptType === 1 ? 'Lieferschein' : receipt?.receiptType === 64 ? 'Zahlungsbeleg' : '-' }}
{{
receipt?.receiptType === 1
? 'Lieferschein'
: receipt?.receiptType === 64
? 'Zahlungsbeleg'
: '-'
}}
</div>
</div>
}
}
<div class="page-customer-order-details-item__comment flex flex-col items-start mt-[1.625rem]">
<div
class="page-customer-order-details-item__comment flex flex-col items-start mt-[1.625rem]"
>
<div class="label mb-[0.375rem]">Anmerkung</div>
<div class="flex flex-row w-full">
<textarea
@@ -248,17 +339,23 @@
<button
type="reset"
class="clear"
(click)="specialCommentControl.setValue(''); saveSpecialComment(); triggerResize()"
>
(click)="
specialCommentControl.setValue('');
saveSpecialComment();
triggerResize()
"
>
<shared-icon icon="close" [size]="24"></shared-icon>
</button>
}
@if (specialCommentControl?.enabled && specialCommentControl.dirty) {
@if (
specialCommentControl?.enabled && specialCommentControl.dirty
) {
<button
class="cta-save"
type="submit"
(click)="saveSpecialComment()"
>
>
Speichern
</button>
}

View File

@@ -15,12 +15,25 @@ import { DomainOmsService, DomainReceiptService } from '@domain/oms';
import { ComponentStore } from '@ngrx/component-store';
import { tapResponse } from '@ngrx/operators';
import { OrderDTO, OrderItemListItemDTO, ReceiptDTO, ReceiptType } from '@generated/swagger/oms-api';
import {
OrderDTO,
OrderItemListItemDTO,
ReceiptDTO,
ReceiptType,
} from '@generated/swagger/oms-api';
import { isEqual } from 'lodash';
import { combineLatest, NEVER, Subject, Observable } from 'rxjs';
import { catchError, filter, first, map, switchMap, withLatestFrom } from 'rxjs/operators';
import {
catchError,
filter,
first,
map,
switchMap,
withLatestFrom,
} from 'rxjs/operators';
import { CustomerOrderDetailsStore } from '../customer-order-details.store';
import { EnvironmentService } from '@core/environment';
import { getOrderItemRewardFeature } from '@isa/oms/data-access';
export interface CustomerOrderDetailsItemComponentState {
orderItem?: OrderItemListItemDTO;
@@ -59,7 +72,12 @@ export class CustomerOrderDetailsItemComponent
// Remove Prev OrderItem from selected list
this._store.selectOrderItem(this.orderItem, false);
this.patchState({ orderItem, quantity: orderItem?.quantity, receipts: [], more: false });
this.patchState({
orderItem,
quantity: orderItem?.quantity,
receipts: [],
more: false,
});
this.specialCommentControl.reset(orderItem?.specialComment);
// Add New OrderItem to selected list if selected was set to true by its input
@@ -94,8 +112,23 @@ export class CustomerOrderDetailsItemComponent
),
);
canChangeQuantity$ = combineLatest([this.orderItem$, this._store.fetchPartial$]).pipe(
map(([item, partialPickup]) => ([16, 8192].includes(item?.processingStatus) || partialPickup) && item.quantity > 1),
hasRewardPoints$ = this.orderItem$.pipe(
map((orderItem) => getOrderItemRewardFeature(orderItem) !== undefined),
);
rewardPoints$ = this.orderItem$.pipe(
map((orderItem) => getOrderItemRewardFeature(orderItem)),
);
canChangeQuantity$ = combineLatest([
this.orderItem$,
this._store.fetchPartial$,
]).pipe(
map(
([item, partialPickup]) =>
([16, 8192].includes(item?.processingStatus) || partialPickup) &&
item.quantity > 1,
),
);
get quantity() {
@@ -111,7 +144,9 @@ export class CustomerOrderDetailsItemComponent
@Input()
get selected() {
return this._store.selectedeOrderItemSubsetIds.includes(this.orderItem?.orderItemSubsetId);
return this._store.selectedeOrderItemSubsetIds.includes(
this.orderItem?.orderItemSubsetId,
);
}
set selected(selected: boolean) {
if (this.selected !== selected) {
@@ -120,22 +155,36 @@ export class CustomerOrderDetailsItemComponent
}
}
readonly selected$ = combineLatest([this.orderItem$, this._store.selectedeOrderItemSubsetIds$]).pipe(
map(([orderItem, selectedItems]) => selectedItems.includes(orderItem?.orderItemSubsetId)),
readonly selected$ = combineLatest([
this.orderItem$,
this._store.selectedeOrderItemSubsetIds$,
]).pipe(
map(([orderItem, selectedItems]) =>
selectedItems.includes(orderItem?.orderItemSubsetId),
),
);
@Output()
selectedChange = new EventEmitter<boolean>();
get selectable() {
return this._store.itemsSelectable && this._store.items.length > 1 && this._store.fetchPartial;
return (
this._store.itemsSelectable &&
this._store.items.length > 1 &&
this._store.fetchPartial
);
}
readonly selectable$ = combineLatest([
this._store.items$,
this._store.itemsSelectable$,
this._store.fetchPartial$,
]).pipe(map(([orderItems, selectable, fetchPartial]) => orderItems.length > 1 && selectable && fetchPartial));
]).pipe(
map(
([orderItems, selectable, fetchPartial]) =>
orderItems.length > 1 && selectable && fetchPartial,
),
);
get receipts() {
return this.get((s) => s.receipts);
@@ -173,6 +222,7 @@ export class CustomerOrderDetailsItemComponent
});
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
ngOnInit() {}
ngOnDestroy() {
@@ -182,37 +232,39 @@ export class CustomerOrderDetailsItemComponent
this._onDestroy$.complete();
}
loadReceipts = this.effect((done$: Observable<(receipts: ReceiptDTO[]) => void | undefined>) =>
done$.pipe(
withLatestFrom(this.orderItem$),
filter(([_, orderItem]) => !!orderItem),
switchMap(([done, orderItem]) =>
this._domainReceiptService
.getReceipts({
receiptType: 65 as ReceiptType,
ids: [orderItem.orderItemSubsetId],
eagerLoading: 1,
})
.pipe(
tapResponse(
(res) => {
const receipts = res.result.map((r) => r.item3?.data).filter((f) => !!f);
this.receipts = receipts;
loadReceipts = this.effect(
(done$: Observable<(receipts: ReceiptDTO[]) => void | undefined>) =>
done$.pipe(
withLatestFrom(this.orderItem$),
filter(([, orderItem]) => !!orderItem),
switchMap(([done, orderItem]) =>
this._domainReceiptService
.getReceipts({
receiptType: 65 as ReceiptType,
ids: [orderItem.orderItemSubsetId],
eagerLoading: 1,
})
.pipe(
tapResponse(
(res) => {
const receipts = res.result
.map((r) => r.item3?.data)
.filter((f) => !!f);
this.receipts = receipts;
if (typeof done === 'function') {
done?.(receipts);
}
},
(err) => {
if (typeof done === 'function') {
done?.([]);
}
},
() => {},
if (typeof done === 'function') {
done?.(receipts);
}
},
() => {
if (typeof done === 'function') {
done?.([]);
}
},
),
),
),
),
),
),
);
async saveSpecialComment() {
@@ -220,7 +272,7 @@ export class CustomerOrderDetailsItemComponent
try {
this.specialCommentControl.reset(this.specialCommentControl.value);
const res = await this._omsService
await this._omsService
.patchComment({
orderId,
orderItemId,
@@ -230,7 +282,10 @@ export class CustomerOrderDetailsItemComponent
.pipe(first())
.toPromise();
this.orderItem = { ...this.orderItem, specialComment: this.specialCommentControl.value ?? '' };
this.orderItem = {
...this.orderItem,
specialComment: this.specialCommentControl.value ?? '',
};
this._store.updateOrderItems([this.orderItem]);
this.specialCommentChanged.emit();
} catch (error) {
@@ -253,8 +308,9 @@ export class CustomerOrderDetailsItemComponent
orderItemFeature(orderItemListItem: OrderItemListItemDTO) {
const orderItems = this.order?.items;
return orderItems?.find((orderItem) => orderItem.data.id === orderItemListItem.orderItemId)?.data?.features
?.orderType;
return orderItems?.find(
(orderItem) => orderItem.data.id === orderItemListItem.orderItemId,
)?.data?.features?.orderType;
}
getOrderItemTrackingData(
@@ -263,15 +319,18 @@ export class CustomerOrderDetailsItemComponent
const orderItems = this.order?.items;
const completeTrackingInformation = orderItems
?.find((orderItem) => orderItem.data.id === orderItemListItem.orderItemId)
?.data?.subsetItems?.find((subsetItem) => subsetItem.id === orderItemListItem.orderItemSubsetId)
?.data?.trackingNumber;
?.data?.subsetItems?.find(
(subsetItem) => subsetItem.id === orderItemListItem.orderItemSubsetId,
)?.data?.trackingNumber;
if (!completeTrackingInformation) {
return;
}
// Beispielnummer: 'DHL: 124124' - Bei mehreren Tracking-Informationen muss noch ein Splitter eingebaut werden, je nach dem welcher Trenner verwendet wird
const trackingInformationPairs = completeTrackingInformation.split(':').map((obj) => obj.trim());
const trackingInformationPairs = completeTrackingInformation
.split(':')
.map((obj) => obj.trim());
return this._trackingTransformationHelper(trackingInformationPairs);
}
@@ -282,7 +341,10 @@ export class CustomerOrderDetailsItemComponent
return trackingInformationPairs.reduce(
(acc, current, index, array) => {
if (index % 2 === 0) {
acc.push({ trackingProvider: current, trackingNumber: array[index + 1] });
acc.push({
trackingProvider: current,
trackingNumber: array[index + 1],
});
}
return acc;
},

View File

@@ -17,6 +17,7 @@ import { ProductImageModule } from '@cdn/product-image';
import { CustomerOrderDetailsStore } from './customer-order-details.store';
import { UiDatepickerModule } from '@ui/datepicker';
import { UiDropdownModule } from '@ui/dropdown';
import { LabelComponent } from '@isa/ui/label';
@NgModule({
imports: [
@@ -34,6 +35,7 @@ import { UiDropdownModule } from '@ui/dropdown';
CustomerOrderPipesModule,
ProductImageModule,
CustomerOrderDetailsTagsComponent,
LabelComponent,
],
exports: [
CustomerOrderDetailsComponent,
@@ -41,7 +43,11 @@ import { UiDropdownModule } from '@ui/dropdown';
CustomerOrderDetailsHeaderComponent,
CustomerOrderDetailsTagsComponent,
],
declarations: [CustomerOrderDetailsComponent, CustomerOrderDetailsItemComponent, CustomerOrderDetailsHeaderComponent],
declarations: [
CustomerOrderDetailsComponent,
CustomerOrderDetailsItemComponent,
CustomerOrderDetailsHeaderComponent,
],
providers: [CustomerOrderDetailsStore],
})
export class CustomerOrderDetailsModule {}

View File

@@ -11,13 +11,11 @@
[alt]="name"
/>
}
@if (hasRewardPoints$ | async) {
<ui-label [type]="Labeltype.Tag" [priority]="LabelPriority.High">
Prämie
</ui-label>
}
</div>
<div class="grid grid-flow-row gap-2">
@if (hasRewardPoints$ | async) {
<ui-label class="w-10">Prämie</ui-label>
}
<div class="grid grid-flow-col justify-between items-end">
<span>{{ orderItem.product?.contributors }}</span>
@if (orderDetailsHistoryRoute$ | async; as orderDetailsHistoryRoute) {
@@ -25,7 +23,7 @@
[routerLink]="orderDetailsHistoryRoute.path"
[queryParams]="orderDetailsHistoryRoute.urlTree.queryParams"
[queryParamsHandling]="'merge'"
class="text-brand font-bold text-xl"
class="text-brand font-bold text-xl relative -top-8"
>
Historie
</a>
@@ -64,7 +62,9 @@
<div class="col-data">
@if (hasRewardPoints$ | async) {
<div class="col-label">Prämie</div>
<div class="col-value">{{ rewardPoints$ | async | number: '1.0-0' }} Lesepunkte</div>
<div class="col-value">
{{ rewardPoints$ | async | number: '1.0-0' }} Lesepunkte
</div>
} @else {
<div class="col-label">Preis</div>
<div class="col-value">

View File

@@ -21,7 +21,7 @@ import { map, takeUntil } from 'rxjs/operators';
import { CustomerSearchStore } from '../../store';
import { CustomerSearchNavigation } from '@shared/services/navigation';
import { PaymentTypePipe } from '@shared/pipes/customer';
import { LabelComponent, Labeltype, LabelPriority } from '@isa/ui/label';
import { LabelComponent } from '@isa/ui/label';
import { getOrderItemRewardFeature } from '@isa/oms/data-access';
import { IconComponent } from '@shared/components/icon';
@@ -113,9 +113,6 @@ export class CustomerOrderItemListItemComponent implements OnInit, OnDestroy {
map((orderItem) => getOrderItemRewardFeature(orderItem)),
);
Labeltype = Labeltype;
LabelPriority = LabelPriority;
ngOnInit() {
this.customerId$
.pipe(takeUntil(this._onDestroy))

View File

@@ -1,31 +1,75 @@
@if (orderItem) {
<div #features class="page-pickup-shelf-details-item__features">
@if (orderItem?.features?.prebooked) {
<img [uiOverlayTrigger]="prebookedTooltip" src="/assets/images/tag_icon_preorder.svg" [alt]="orderItem?.features?.prebooked" />
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #prebookedTooltip [closeable]="true">
<img
[uiOverlayTrigger]="prebookedTooltip"
src="/assets/images/tag_icon_preorder.svg"
[alt]="orderItem?.features?.prebooked"
/>
<ui-tooltip
yPosition="above"
xPosition="after"
[yOffset]="-11"
[xOffset]="-8"
#prebookedTooltip
[closeable]="true"
>
Artikel wird für Sie vorgemerkt.
</ui-tooltip>
}
@if (hasEmailNotification$ | async) {
<img [uiOverlayTrigger]="emailTooltip" src="/assets/images/email_bookmark.svg" />
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #emailTooltip [closeable]="true">
<img
[uiOverlayTrigger]="emailTooltip"
src="/assets/images/email_bookmark.svg"
/>
<ui-tooltip
yPosition="above"
xPosition="after"
[yOffset]="-11"
[xOffset]="-8"
#emailTooltip
[closeable]="true"
>
Per E-Mail benachrichtigt
<br />
@for (notifications of emailNotificationDates$ | async; track notifications) {
@for (notificationDate of notifications.dates; track notificationDate) {
{{ notifications.type | notificationType }} {{ notificationDate | date: 'dd.MM.yyyy | HH:mm' }} Uhr
@for (
notifications of emailNotificationDates$ | async;
track notifications
) {
@for (
notificationDate of notifications.dates;
track notificationDate
) {
{{ notifications.type | notificationType }}
{{ notificationDate | date: 'dd.MM.yyyy | HH:mm' }} Uhr
<br />
}
}
</ui-tooltip>
}
@if (hasSmsNotification$ | async) {
<img [uiOverlayTrigger]="smsTooltip" src="/assets/images/sms_bookmark.svg" />
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #smsTooltip [closeable]="true">
<img
[uiOverlayTrigger]="smsTooltip"
src="/assets/images/sms_bookmark.svg"
/>
<ui-tooltip
yPosition="above"
xPosition="after"
[yOffset]="-11"
[xOffset]="-8"
#smsTooltip
[closeable]="true"
>
Per SMS benachrichtigt
<br />
@for (notifications of smsNotificationDates$ | async; track notifications) {
@for (notificationDate of notifications.dates; track notificationDate) {
@for (
notifications of smsNotificationDates$ | async;
track notifications
) {
@for (
notificationDate of notifications.dates;
track notificationDate
) {
{{ notificationDate | date: 'dd.MM.yyyy | HH:mm' }} Uhr
<br />
}
@@ -39,280 +83,336 @@
[productImageNavigation]="orderItem?.product?.ean"
[src]="orderItem.product?.ean | productImage"
[alt]="orderItem.product?.name"
/>
@if (hasRewardPoints$ | async) {
<ui-label [type]="Labeltype.Tag" [priority]="LabelPriority.High">
Prämie
</ui-label>
}
/>
</div>
<div class="page-pickup-shelf-details-item__details">
<div class="flex flex-row justify-between items-start mb-[1.3125rem]">
<h3
[uiElementDistance]="features"
#elementDistance="uiElementDistance"
[style.max-width.px]="elementDistance.distanceChange | async"
class="flex flex-col"
>
@if (hasRewardPoints$ | async) {
<ui-label class="w-10 mb-2">Prämie</ui-label>
}
<div class="font-normal mb-[0.375rem]">
{{ orderItem.product?.contributors }}
</div>
<div>{{ orderItem.product?.name }}</div>
</h3>
<div class="history-wrapper flex flex-col items-end justify-center">
<button
class="cta-history text-p1"
(click)="historyClick.emit(orderItem)"
>
Historie
</button>
@if (selectable$ | async) {
<input
[ngModel]="selected$ | async"
(ngModelChange)="
setSelected($event);
tracker.trackEvent({
category: 'pickup-shelf-list-item',
action: 'select',
name: orderItem?.product?.name,
value: $event ? 1 : 0,
})
"
class="isa-select-bullet mt-4"
type="checkbox"
matomoTracker
#tracker="matomo"
/>
}
</div>
</div>
<div class="page-pickup-shelf-details-item__details">
<div class="flex flex-row justify-between items-start mb-[1.3125rem]">
<h3
[uiElementDistance]="features"
#elementDistance="uiElementDistance"
[style.max-width.px]="elementDistance.distanceChange | async"
class="flex flex-col"
>
<div class="font-normal mb-[0.375rem]">{{ orderItem.product?.contributors }}</div>
<div>{{ orderItem.product?.name }}</div>
</h3>
<div class="history-wrapper flex flex-col items-end justify-center">
<button class="cta-history text-p1" (click)="historyClick.emit(orderItem)">Historie</button>
@if (selectable$ | async) {
<input
[ngModel]="selected$ | async"
(ngModelChange)="
setSelected($event);
tracker.trackEvent({
category: 'pickup-shelf-list-item',
action: 'select',
name: orderItem?.product?.name,
value: $event ? 1 : 0,
})
"
class="isa-select-bullet mt-4"
type="checkbox"
matomoTracker
#tracker="matomo"
/>
}
</div>
<div class="detail">
<div class="label">Menge</div>
<div class="value">
@if (!(canChangeQuantity$ | async)) {
{{ orderItem?.quantity }}x
}
@if (canChangeQuantity$ | async) {
<ui-quantity-dropdown
[showTrash]="false"
[range]="orderItem?.quantity"
[(ngModel)]="quantity"
(ngModelChange)="
tracker.trackEvent({
category: 'pickup-shelf-list-item',
action: 'quantity',
name: orderItem?.product?.name,
value: $event,
})
"
[showSpinner]="false"
matomoTracker
#tracker="matomo"
></ui-quantity-dropdown>
}
<span class="overall-quantity"
>(von {{ orderItem?.overallQuantity }})</span
>
</div>
</div>
@if (!!orderItem.product?.formatDetail) {
<div class="detail">
<div class="label">Menge</div>
<div class="label">Format</div>
<div class="value">
@if (!(canChangeQuantity$ | async)) {
{{ orderItem?.quantity }}x
@if (
orderItem?.product?.format &&
orderItem?.product?.format !== 'UNKNOWN'
) {
<img
class="format-icon"
[src]="
'/assets/images/Icon_' + orderItem.product?.format + '.svg'
"
alt="format icon"
/>
}
@if (canChangeQuantity$ | async) {
<ui-quantity-dropdown
[showTrash]="false"
[range]="orderItem?.quantity"
[(ngModel)]="quantity"
(ngModelChange)="
tracker.trackEvent({ category: 'pickup-shelf-list-item', action: 'quantity', name: orderItem?.product?.name, value: $event })
"
[showSpinner]="false"
matomoTracker
#tracker="matomo"
></ui-quantity-dropdown>
}
<span class="overall-quantity">(von {{ orderItem?.overallQuantity }})</span>
<span>{{ orderItem.product?.formatDetail }}</span>
</div>
</div>
@if (!!orderItem.product?.formatDetail) {
<div class="detail">
<div class="label">Format</div>
}
@if (!!orderItem.product?.ean) {
<div class="detail">
<div class="label">ISBN/EAN</div>
<div class="value">{{ orderItem.product?.ean }}</div>
</div>
}
@if (orderItem.price !== undefined || (hasRewardPoints$ | async)) {
<div class="detail">
@if (hasRewardPoints$ | async) {
<div class="label">Prämie</div>
<div class="value">
@if (orderItem?.product?.format && orderItem?.product?.format !== 'UNKNOWN') {
<img
class="format-icon"
[src]="'/assets/images/Icon_' + orderItem.product?.format + '.svg'"
alt="format icon"
/>
}
<span>{{ orderItem.product?.formatDetail }}</span>
</div>
</div>
}
@if (!!orderItem.product?.ean) {
<div class="detail">
<div class="label">ISBN/EAN</div>
<div class="value">{{ orderItem.product?.ean }}</div>
</div>
}
@if (orderItem.price !== undefined || (hasRewardPoints$ | async)) {
<div class="detail">
@if (hasRewardPoints$ | async) {
<div class="label">Prämie</div>
<div class="value">{{ rewardPoints$ | async | number: '1.0-0' }} Lesepunkte</div>
} @else {
<div class="label">Preis</div>
<div class="value">{{ orderItem.price | currency: 'EUR' }}</div>
}
</div>
}
@if (!!orderItem.retailPrice?.vat?.inPercent) {
<div class="detail">
<div class="label">MwSt</div>
<div class="value">{{ orderItem.retailPrice?.vat?.inPercent }}%</div>
</div>
}
<hr class="border-[#EDEFF0] border-t-2 my-4" />
@if (orderItem.supplier) {
<div class="detail">
<div class="label">Lieferant</div>
<div class="value">{{ orderItem.supplier }}</div>
@if (!expanded) {
<button
(click)="expanded = !expanded"
type="button"
class="page-pickup-shelf-details-item__more text-[#0556B4] font-bold flex flex-row items-center justify-center"
[class.flex-row-reverse]="!expanded"
>
<shared-icon class="mr-1" icon="arrow-back" [size]="20" [class.ml-1]="!expanded" [class.rotate-180]="!expanded"></shared-icon>
{{ expanded ? 'Weniger' : 'Mehr' }}
</button>
}
</div>
}
@if (!!orderItem.ssc || !!orderItem.sscText) {
<div class="detail">
<div class="label">Meldenummer</div>
<div class="value">{{ orderItem.ssc }} - {{ orderItem.sscText }}</div>
</div>
}
@if (expanded) {
@if (!!orderItem.targetBranch) {
<div class="detail">
<div class="label">Zielfiliale</div>
<div class="value">{{ orderItem.targetBranch }}</div>
{{ rewardPoints$ | async | number: '1.0-0' }} Lesepunkte
</div>
} @else {
<div class="label">Preis</div>
<div class="value">{{ orderItem.price | currency: 'EUR' }}</div>
}
<div class="detail">
<div class="label">
@if (
orderItemFeature(orderItem) === 'Versand' ||
orderItemFeature(orderItem) === 'B2B-Versand' ||
orderItemFeature(orderItem) === 'DIG-Versand'
) {
{{ orderItem?.estimatedDelivery ? 'Lieferung zwischen' : 'Lieferung ab' }}
}
@if (orderItemFeature(orderItem) === 'Abholung' || orderItemFeature(orderItem) === 'Rücklage') {
Abholung ab
}
</div>
@if (!!orderItem?.estimatedDelivery || !!orderItem?.estimatedShippingDate) {
<div class="value bg-[#D8DFE5] rounded w-max px-2">
@if (!!orderItem?.estimatedDelivery) {
{{ orderItem?.estimatedDelivery?.start | date: 'dd.MM.yy' }} und
{{ orderItem?.estimatedDelivery?.stop | date: 'dd.MM.yy' }}
} @else {
@if (!!orderItem?.estimatedShippingDate) {
{{ orderItem?.estimatedShippingDate | date: 'dd.MM.yy' }}
}
}
</div>
}
</div>
<hr class="border-[#EDEFF0] border-t-2 my-4" />
@if (!!orderItem?.compartmentCode) {
<div class="detail">
<div class="label">Abholfachnr.</div>
<div class="value">{{ orderItem?.compartmentCode }}</div>
</div>
}
<div class="detail">
<div class="label">Vormerker</div>
<div class="value">{{ orderItem.isPrebooked ? 'Ja' : 'Nein' }}</div>
</div>
<hr class="border-[#EDEFF0] border-t-2 my-4" />
@if (!!orderItem.paymentProcessing) {
<div class="detail">
<div class="label">Zahlungsweg</div>
<div class="value">{{ orderItem.paymentProcessing || '-' }}</div>
</div>
}
@if (!!orderItem.paymentType) {
<div class="detail">
<div class="label">Zahlungsart</div>
<div class="value">{{ orderItem.paymentType | paymentType }}</div>
</div>
}
@if (receiptCount$ | async; as count) {
<h4 class="receipt-header">
{{ count > 1 ? 'Belege' : 'Beleg' }}
</h4>
}
@for (receipt of receipts$ | async; track receipt) {
@if (!!receipt?.receiptNumber) {
<div class="detail">
<div class="label">Belegnummer</div>
<div class="value">{{ receipt?.receiptNumber }}</div>
</div>
}
@if (!!receipt?.printedDate) {
<div class="detail">
<div class="label">Erstellt am</div>
<div class="value">{{ receipt?.printedDate | date: 'dd.MM.yy | HH:mm' }} Uhr</div>
</div>
}
@if (!!receipt?.receiptText) {
<div class="detail">
<div class="label">Rechnungstext</div>
<div class="value">{{ receipt?.receiptText || '-' }}</div>
</div>
}
@if (!!receipt?.receiptType) {
<div class="detail">
<div class="label">Belegart</div>
<div class="value">
{{ receipt?.receiptType === 1 ? 'Lieferschein' : receipt?.receiptType === 64 ? 'Zahlungsbeleg' : '-' }}
</div>
</div>
}
}
@if (!!orderItem.paymentProcessing || !!orderItem.paymentType || !!(receiptCount$ | async)) {
<hr
class="border-[#EDEFF0] border-t-2 my-4"
/>
}
@if (expanded) {
</div>
}
@if (!!orderItem.retailPrice?.vat?.inPercent) {
<div class="detail">
<div class="label">MwSt</div>
<div class="value">{{ orderItem.retailPrice?.vat?.inPercent }}%</div>
</div>
}
<hr class="border-[#EDEFF0] border-t-2 my-4" />
@if (orderItem.supplier) {
<div class="detail">
<div class="label">Lieferant</div>
<div class="value">{{ orderItem.supplier }}</div>
@if (!expanded) {
<button
(click)="expanded = !expanded"
type="button"
class="page-pickup-shelf-details-item__less text-[#0556B4] font-bold flex flex-row items-center justify-center"
>
<shared-icon class="mr-1" icon="arrow-back" [size]="20"></shared-icon>
Weniger
class="page-pickup-shelf-details-item__more text-[#0556B4] font-bold flex flex-row items-center justify-center"
[class.flex-row-reverse]="!expanded"
>
<shared-icon
class="mr-1"
icon="arrow-back"
[size]="20"
[class.ml-1]="!expanded"
[class.rotate-180]="!expanded"
></shared-icon>
{{ expanded ? 'Weniger' : 'Mehr' }}
</button>
}
</div>
}
@if (!!orderItem.ssc || !!orderItem.sscText) {
<div class="detail">
<div class="label">Meldenummer</div>
<div class="value">{{ orderItem.ssc }} - {{ orderItem.sscText }}</div>
</div>
}
@if (expanded) {
@if (!!orderItem.targetBranch) {
<div class="detail">
<div class="label">Zielfiliale</div>
<div class="value">{{ orderItem.targetBranch }}</div>
</div>
}
<div class="page-pickup-shelf-details-item__comment flex flex-col items-start mt-[1.625rem]">
<div class="label mb-[0.375rem]">Anmerkung</div>
<div class="flex flex-row w-full">
<textarea
matInput
cdkTextareaAutosize
#autosize="cdkTextareaAutosize"
cdkAutosizeMinRows="1"
cdkAutosizeMaxRows="5"
maxlength="200"
#specialCommentInput
(keydown.delete)="triggerResize()"
(keydown.backspace)="triggerResize()"
type="text"
name="comment"
placeholder="Eine Anmerkung hinzufügen"
[formControl]="specialCommentControl"
[class.inactive]="!specialCommentControl.dirty"
></textarea>
<div class="comment-actions">
@if (!!specialCommentControl.value?.length) {
<button
type="reset"
class="clear"
(click)="specialCommentControl.setValue(''); saveSpecialComment(); triggerResize()"
>
<shared-icon icon="close" [size]="24"></shared-icon>
</button>
}
@if (specialCommentControl?.enabled && specialCommentControl.dirty) {
<button
class="cta-save"
type="submit"
(click)="saveSpecialComment()"
matomoClickCategory="pickup-shelf-details-item"
matomoClickAction="save"
matomoClickName="special-comment"
>
Speichern
</button>
<div class="detail">
<div class="label">
@if (
orderItemFeature(orderItem) === 'Versand' ||
orderItemFeature(orderItem) === 'B2B-Versand' ||
orderItemFeature(orderItem) === 'DIG-Versand'
) {
{{
orderItem?.estimatedDelivery
? 'Lieferung zwischen'
: 'Lieferung ab'
}}
}
@if (
orderItemFeature(orderItem) === 'Abholung' ||
orderItemFeature(orderItem) === 'Rücklage'
) {
Abholung ab
}
</div>
@if (
!!orderItem?.estimatedDelivery || !!orderItem?.estimatedShippingDate
) {
<div class="value bg-[#D8DFE5] rounded w-max px-2">
@if (!!orderItem?.estimatedDelivery) {
{{ orderItem?.estimatedDelivery?.start | date: 'dd.MM.yy' }} und
{{ orderItem?.estimatedDelivery?.stop | date: 'dd.MM.yy' }}
} @else {
@if (!!orderItem?.estimatedShippingDate) {
{{ orderItem?.estimatedShippingDate | date: 'dd.MM.yy' }}
}
}
</div>
}
</div>
<hr class="border-[#EDEFF0] border-t-2 my-4" />
@if (!!orderItem?.compartmentCode) {
<div class="detail">
<div class="label">Abholfachnr.</div>
<div class="value">{{ orderItem?.compartmentCode }}</div>
</div>
}
<div class="detail">
<div class="label">Vormerker</div>
<div class="value">{{ orderItem.isPrebooked ? 'Ja' : 'Nein' }}</div>
</div>
<hr class="border-[#EDEFF0] border-t-2 my-4" />
@if (!!orderItem.paymentProcessing) {
<div class="detail">
<div class="label">Zahlungsweg</div>
<div class="value">{{ orderItem.paymentProcessing || '-' }}</div>
</div>
}
@if (!!orderItem.paymentType) {
<div class="detail">
<div class="label">Zahlungsart</div>
<div class="value">{{ orderItem.paymentType | paymentType }}</div>
</div>
}
@if (receiptCount$ | async; as count) {
<h4 class="receipt-header">
{{ count > 1 ? 'Belege' : 'Beleg' }}
</h4>
}
@for (receipt of receipts$ | async; track receipt) {
@if (!!receipt?.receiptNumber) {
<div class="detail">
<div class="label">Belegnummer</div>
<div class="value">{{ receipt?.receiptNumber }}</div>
</div>
}
@if (!!receipt?.printedDate) {
<div class="detail">
<div class="label">Erstellt am</div>
<div class="value">
{{ receipt?.printedDate | date: 'dd.MM.yy | HH:mm' }} Uhr
</div>
</div>
}
@if (!!receipt?.receiptText) {
<div class="detail">
<div class="label">Rechnungstext</div>
<div class="value">{{ receipt?.receiptText || '-' }}</div>
</div>
}
@if (!!receipt?.receiptType) {
<div class="detail">
<div class="label">Belegart</div>
<div class="value">
{{
receipt?.receiptType === 1
? 'Lieferschein'
: receipt?.receiptType === 64
? 'Zahlungsbeleg'
: '-'
}}
</div>
</div>
}
}
@if (
!!orderItem.paymentProcessing ||
!!orderItem.paymentType ||
!!(receiptCount$ | async)
) {
<hr class="border-[#EDEFF0] border-t-2 my-4" />
}
@if (expanded) {
<button
(click)="expanded = !expanded"
type="button"
class="page-pickup-shelf-details-item__less text-[#0556B4] font-bold flex flex-row items-center justify-center"
>
<shared-icon
class="mr-1"
icon="arrow-back"
[size]="20"
></shared-icon>
Weniger
</button>
}
}
<div
class="page-pickup-shelf-details-item__comment flex flex-col items-start mt-[1.625rem]"
>
<div class="label mb-[0.375rem]">Anmerkung</div>
<div class="flex flex-row w-full">
<textarea
matInput
cdkTextareaAutosize
#autosize="cdkTextareaAutosize"
cdkAutosizeMinRows="1"
cdkAutosizeMaxRows="5"
maxlength="200"
#specialCommentInput
(keydown.delete)="triggerResize()"
(keydown.backspace)="triggerResize()"
type="text"
name="comment"
placeholder="Eine Anmerkung hinzufügen"
[formControl]="specialCommentControl"
[class.inactive]="!specialCommentControl.dirty"
></textarea>
<div class="comment-actions">
@if (!!specialCommentControl.value?.length) {
<button
type="reset"
class="clear"
(click)="
specialCommentControl.setValue('');
saveSpecialComment();
triggerResize()
"
>
<shared-icon icon="close" [size]="24"></shared-icon>
</button>
}
@if (
specialCommentControl?.enabled && specialCommentControl.dirty
) {
<button
class="cta-save"
type="submit"
(click)="saveSpecialComment()"
matomoClickCategory="pickup-shelf-details-item"
matomoClickAction="save"
matomoClickName="special-comment"
>
Speichern
</button>
}
</div>
</div>
</div>
</div>
}
</div>
}

View File

@@ -1,5 +1,10 @@
import { CdkTextareaAutosize, TextFieldModule } from '@angular/cdk/text-field';
import { AsyncPipe, CurrencyPipe, DatePipe, DecimalPipe } from '@angular/common';
import {
AsyncPipe,
CurrencyPipe,
DatePipe,
DecimalPipe,
} from '@angular/common';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
@@ -11,12 +16,23 @@ import {
inject,
OnDestroy,
} from '@angular/core';
import { FormsModule, ReactiveFormsModule, UntypedFormControl } from '@angular/forms';
import { NavigateOnClickDirective, ProductImageModule } from '@cdn/product-image';
import { DBHOrderItemListItemDTO, OrderDTO, ReceiptDTO } from '@generated/swagger/oms-api';
import {
FormsModule,
ReactiveFormsModule,
UntypedFormControl,
} from '@angular/forms';
import {
NavigateOnClickDirective,
ProductImageModule,
} from '@cdn/product-image';
import {
DBHOrderItemListItemDTO,
OrderDTO,
ReceiptDTO,
} from '@generated/swagger/oms-api';
import { getOrderItemRewardFeature } from '@isa/oms/data-access';
import { UiCommonModule } from '@ui/common';
import { LabelComponent, Labeltype, LabelPriority } from '@isa/ui/label';
import { LabelComponent } from '@isa/ui/label';
import { UiTooltipModule } from '@ui/tooltip';
import { PickupShelfPaymentTypePipe } from '../pipes/payment-type.pipe';
import { IconModule } from '@shared/components/icon';
@@ -60,8 +76,8 @@ export interface PickUpShelfDetailsItemComponentState {
NotificationTypePipe,
NavigateOnClickDirective,
MatomoModule,
LabelComponent
],
LabelComponent,
],
})
export class PickUpShelfDetailsItemComponent
extends ComponentStore<PickUpShelfDetailsItemComponentState>
@@ -88,7 +104,11 @@ export class PickUpShelfDetailsItemComponent
this._store.selectOrderItem(this.orderItem, false);
}
this.patchState({ orderItem, quantity: orderItem?.quantity, more: false });
this.patchState({
orderItem,
quantity: orderItem?.quantity,
more: false,
});
this.specialCommentControl.reset(orderItem?.specialComment);
// Add New OrderItem to selected list if selected was set to true by its input
if (this.get((s) => s.selected)) {
@@ -110,16 +130,24 @@ export class PickUpShelfDetailsItemComponent
readonly orderItem$ = this.select((s) => s.orderItem);
emailNotificationDates$ = this.orderItem$.pipe(
switchMap((orderItem) => this._store.getEmailNotificationDate$(orderItem?.orderItemSubsetId)),
switchMap((orderItem) =>
this._store.getEmailNotificationDate$(orderItem?.orderItemSubsetId),
),
);
hasEmailNotification$ = this.emailNotificationDates$.pipe(map((dates) => dates?.length > 0));
hasEmailNotification$ = this.emailNotificationDates$.pipe(
map((dates) => dates?.length > 0),
);
smsNotificationDates$ = this.orderItem$.pipe(
switchMap((orderItem) => this._store.getSmsNotificationDate$(orderItem?.orderItemSubsetId)),
switchMap((orderItem) =>
this._store.getSmsNotificationDate$(orderItem?.orderItemSubsetId),
),
);
hasSmsNotification$ = this.smsNotificationDates$.pipe(map((dates) => dates?.length > 0));
hasSmsNotification$ = this.smsNotificationDates$.pipe(
map((dates) => dates?.length > 0),
);
/**
* Observable that indicates whether the order item has reward points (Lesepunkte).
@@ -137,8 +165,15 @@ export class PickUpShelfDetailsItemComponent
map((orderItem) => getOrderItemRewardFeature(orderItem)),
);
canChangeQuantity$ = combineLatest([this.orderItem$, this._store.fetchPartial$]).pipe(
map(([item, partialPickup]) => ([16, 8192].includes(item?.processingStatus) || partialPickup) && item.quantity > 1),
canChangeQuantity$ = combineLatest([
this.orderItem$,
this._store.fetchPartial$,
]).pipe(
map(
([item, partialPickup]) =>
([16, 8192].includes(item?.processingStatus) || partialPickup) &&
item.quantity > 1,
),
);
get quantity() {
@@ -147,7 +182,10 @@ export class PickUpShelfDetailsItemComponent
set quantity(quantity: number) {
if (this.quantity !== quantity) {
this.patchState({ quantity });
this._store.setSelectedOrderItemQuantity({ orderItemSubsetId: this.orderItem.orderItemSubsetId, quantity });
this._store.setSelectedOrderItemQuantity({
orderItemSubsetId: this.orderItem.orderItemSubsetId,
quantity,
});
}
}
@@ -155,7 +193,9 @@ export class PickUpShelfDetailsItemComponent
@Input()
get selected() {
return this._store.selectedOrderItemIds.includes(this.orderItem?.orderItemSubsetId);
return this._store.selectedOrderItemIds.includes(
this.orderItem?.orderItemSubsetId,
);
}
set selected(selected: boolean) {
if (this.selected !== selected) {
@@ -164,23 +204,40 @@ export class PickUpShelfDetailsItemComponent
}
}
readonly selected$ = combineLatest([this.orderItem$, this._store.selectedOrderItemIds$]).pipe(
map(([orderItem, selectedItems]) => selectedItems.includes(orderItem?.orderItemSubsetId)),
readonly selected$ = combineLatest([
this.orderItem$,
this._store.selectedOrderItemIds$,
]).pipe(
map(([orderItem, selectedItems]) =>
selectedItems.includes(orderItem?.orderItemSubsetId),
),
);
@Output()
selectedChange = new EventEmitter<boolean>();
get isItemSelectable() {
return this._store.orderItems?.some((item) => !!item?.actions && item?.actions?.length > 0);
return this._store.orderItems?.some(
(item) => !!item?.actions && item?.actions?.length > 0,
);
}
get selectable() {
return this.isItemSelectable && this._store.orderItems.length > 1 && this._store.fetchPartial;
return (
this.isItemSelectable &&
this._store.orderItems.length > 1 &&
this._store.fetchPartial
);
}
readonly selectable$ = combineLatest([this._store.orderItems$, this._store.fetchPartial$]).pipe(
map(([orderItems, fetchPartial]) => orderItems.length > 1 && this.isItemSelectable && fetchPartial),
readonly selectable$ = combineLatest([
this._store.orderItems$,
this._store.fetchPartial$,
]).pipe(
map(
([orderItems, fetchPartial]) =>
orderItems.length > 1 && this.isItemSelectable && fetchPartial,
),
);
get receipts() {
@@ -193,7 +250,9 @@ export class PickUpShelfDetailsItemComponent
readonly receipts$ = this._store.receipts$;
readonly receiptCount$ = this.receipts$.pipe(map((receipts) => receipts?.length));
readonly receiptCount$ = this.receipts$.pipe(
map((receipts) => receipts?.length),
);
specialCommentControl = new UntypedFormControl();
@@ -203,10 +262,6 @@ export class PickUpShelfDetailsItemComponent
expanded = false;
// Expose to template
Labeltype = Labeltype;
LabelPriority = LabelPriority;
constructor(private _cdr: ChangeDetectorRef) {
super({
more: false,
@@ -239,8 +294,9 @@ export class PickUpShelfDetailsItemComponent
orderItemFeature(orderItemListItem: DBHOrderItemListItemDTO) {
const orderItems = this.order?.items;
return orderItems?.find((orderItem) => orderItem.data.id === orderItemListItem.orderItemId)?.data?.features
?.orderType;
return orderItems?.find(
(orderItem) => orderItem.data.id === orderItemListItem.orderItemId,
)?.data?.features?.orderType;
}
triggerResize() {

View File

@@ -4,12 +4,16 @@
[routerLinkActive]="!isTablet && !primaryOutletActive ? 'active' : ''"
queryParamsHandling="preserve"
(click)="onDetailsClick()"
>
>
<div
class="page-pickup-shelf-list-item__item-grid-container"
[class.page-pickup-shelf-list-item__item-grid-container-main]="primaryOutletActive"
[class.page-pickup-shelf-list-item__item-grid-container-secondary]="primaryOutletActive && isItemSelectable === undefined"
>
[class.page-pickup-shelf-list-item__item-grid-container-main]="
primaryOutletActive
"
[class.page-pickup-shelf-list-item__item-grid-container-secondary]="
primaryOutletActive && isItemSelectable === undefined
"
>
<div class="page-pickup-shelf-list-item__item-thumbnail text-center">
@if (item?.product?.ean | productImage; as productImage) {
<img
@@ -18,88 +22,126 @@
[productImageNavigation]="item?.product?.ean"
[src]="productImage"
[alt]="item?.product?.name"
/>
}
@if (hasRewardPoints) {
<ui-label [type]="Labeltype.Tag" [priority]="LabelPriority.High">
Prämie
</ui-label>
/>
}
</div>
<div
class="page-pickup-shelf-list-item__item-title-contributors flex flex-col"
[class.mr-32]="showCompartmentCode && item.features?.paid && (isTablet || isDesktopSmall || primaryOutletActive)"
>
[class.mr-32]="
showCompartmentCode &&
item.features?.paid &&
(isTablet || isDesktopSmall || primaryOutletActive)
"
>
@if (hasRewardPoints) {
<ui-label class="w-10 mb-2">Prämie</ui-label>
}
<div
class="page-pickup-shelf-list-item__item-contributors text-p2 font-normal text-ellipsis overflow-hidden max-w-[24rem] whitespace-nowrap mb-[0.375rem]"
>
@for (contributor of contributors; track contributor; let last = $last) {
>
@for (
contributor of contributors;
track contributor;
let last = $last
) {
{{ contributor }}{{ last ? '' : ';' }}
}
</div>
<div
class="page-pickup-shelf-list-item__item-title font-bold text-p1 desktop-small:text-p2"
[class.page-pickup-shelf-list-item__item-title-bigger-text-size]="!primaryOutletActive"
>
[class.page-pickup-shelf-list-item__item-title-bigger-text-size]="
!primaryOutletActive
"
>
{{ item?.product?.name }}
</div>
</div>
<div class="page-pickup-shelf-list-item__item-ean-quantity-changed flex flex-col">
<div class="page-pickup-shelf-list-item__item-ean text-p2 flex flex-row mb-[0.375rem]" [attr.data-ean]="item?.product?.ean">
<div
class="page-pickup-shelf-list-item__item-ean-quantity-changed flex flex-col"
>
<div
class="page-pickup-shelf-list-item__item-ean text-p2 flex flex-row mb-[0.375rem]"
[attr.data-ean]="item?.product?.ean"
>
<div class="min-w-[7.5rem]">EAN</div>
<div class="font-bold">{{ item?.product?.ean }}</div>
</div>
<div class="page-pickup-shelf-list-item__item-quantity flex flex-row text-p2 mb-[0.375rem]" [attr.data-menge]="item.quantity">
<div
class="page-pickup-shelf-list-item__item-quantity flex flex-row text-p2 mb-[0.375rem]"
[attr.data-menge]="item.quantity"
>
<div class="min-w-[7.5rem]">Menge</div>
<div class="font-bold">{{ item.quantity }} x</div>
</div>
<div class="page-pickup-shelf-list-item__item-changed text-p2" [attr.data-geaendert]="item?.processingStatusDate">
<div
class="page-pickup-shelf-list-item__item-changed text-p2"
[attr.data-geaendert]="item?.processingStatusDate"
>
@if (showChangeDate) {
<div class="flex flex-row">
<div class="min-w-[7.5rem]">Geändert</div>
<div class="font-bold">{{ item?.processingStatusDate | date: 'dd.MM.yy | HH:mm' }} Uhr</div>
<div class="font-bold">
{{ item?.processingStatusDate | date: 'dd.MM.yy | HH:mm' }} Uhr
</div>
</div>
} @else {
<div class="flex flex-row" [attr.data-bestelldatum]="item?.orderDate">
<div class="min-w-[7.5rem]">Bestelldatum</div>
<div class="font-bold">{{ item?.orderDate | date: 'dd.MM.yy | HH:mm' }} Uhr</div>
<div class="font-bold">
{{ item?.orderDate | date: 'dd.MM.yy | HH:mm' }} Uhr
</div>
</div>
}
</div>
</div>
<div class="page-pickup-shelf-list-item__item-order-number-processing-status-paid flex flex-col">
<div
class="page-pickup-shelf-list-item__item-order-number-processing-status-paid flex flex-col"
>
@if (showCompartmentCode) {
<div
class="page-pickup-shelf-list-item__item-order-number text-h3 mb-[0.375rem] self-end font-bold break-all text-right"
[attr.data-compartment-code]="item?.compartmentCode"
[attr.data-compartment-info]="item?.compartmentInfo"
>
{{ item?.compartmentCode }}{{ item?.compartmentInfo && '_' + item?.compartmentInfo }}
>
{{ item?.compartmentCode
}}{{ item?.compartmentInfo && '_' + item?.compartmentInfo }}
</div>
}
<div class="page-pickup-shelf-list-item__item-processing-paid-status flex flex-col font-bold self-end text-p2 mb-[0.375rem]">
<div
class="page-pickup-shelf-list-item__item-processing-paid-status flex flex-col font-bold self-end text-p2 mb-[0.375rem]"
>
<div
class="page-pickup-shelf-list-item__item-processing-status flex flex-row mb-[0.375rem] rounded p-3 py-[0.125rem] text-white"
[style]="processingStatusColor"
[attr.data-processing-status]="item.processingStatus"
>
>
{{ item.processingStatus | processingStatus }}
</div>
<div class="page-pickup-shelf-list-item__item-paid self-end flex flex-row">
@if (item.features?.paid && (isTablet || isDesktopSmall || primaryOutletActive)) {
<div
class="page-pickup-shelf-list-item__item-paid self-end flex flex-row"
>
@if (
item.features?.paid &&
(isTablet || isDesktopSmall || primaryOutletActive)
) {
<div
class="font-bold flex flex-row items-center justify-center text-p2 text-[#26830C]"
[attr.data-paid]="item.features?.paid"
>
<shared-icon class="flex items-center justify-center mr-[0.375rem]" [size]="24" icon="credit-card"></shared-icon>
>
<shared-icon
class="flex items-center justify-center mr-[0.375rem]"
[size]="24"
icon="credit-card"
></shared-icon>
{{ item.features?.paid }}
</div>
}
@@ -110,28 +152,35 @@
@if (isItemSelectable) {
<div
class="page-pickup-shelf-list-item__item-select-bullet justify-self-end self-center mb-2"
[class.page-pickup-shelf-list-item__item-select-bullet-primary]="primaryOutletActive"
>
[class.page-pickup-shelf-list-item__item-select-bullet-primary]="
primaryOutletActive
"
>
<input
(click)="$event.stopPropagation()"
[ngModel]="selectedItem"
(ngModelChange)="
setSelected();
tracker.trackEvent({ category: 'pickup-shelf-list-item', action: 'select', name: item?.product?.name, value: $event ? 1 : 0 })
"
(ngModelChange)="
setSelected();
tracker.trackEvent({
category: 'pickup-shelf-list-item',
action: 'select',
name: item?.product?.name,
value: $event ? 1 : 0,
})
"
class="isa-select-bullet"
type="checkbox"
matomoTracker
#tracker="matomo"
/>
</div>
}
<div
[attr.data-special-comment]="item?.specialComment"
class="page-pickup-shelf-list-item__item-special-comment break-words font-bold text-p2 mt-[0.375rem] text-[#996900]"
>
{{ item?.specialComment }}
/>
</div>
}
<div
[attr.data-special-comment]="item?.specialComment"
class="page-pickup-shelf-list-item__item-special-comment break-words font-bold text-p2 mt-[0.375rem] text-[#996900]"
>
{{ item?.specialComment }}
</div>
</a>
</div>
</a>

View File

@@ -1,12 +1,21 @@
import { DatePipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, ElementRef, Input, inject } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
ElementRef,
Input,
inject,
} from '@angular/core';
import { RouterLink, RouterLinkActive } from '@angular/router';
import { NavigateOnClickDirective, ProductImageModule } from '@cdn/product-image';
import {
NavigateOnClickDirective,
ProductImageModule,
} from '@cdn/product-image';
import { EnvironmentService } from '@core/environment';
import { IconModule } from '@shared/components/icon';
import { DBHOrderItemListItemDTO } from '@generated/swagger/oms-api';
import { getOrderItemRewardFeature } from '@isa/oms/data-access';
import { LabelComponent, Labeltype, LabelPriority } from '@isa/ui/label';
import { LabelComponent } from '@isa/ui/label';
import { UiCommonModule } from '@ui/common';
import { PickupShelfProcessingStatusPipe } from '../pipes/processing-status.pipe';
import { FormsModule } from '@angular/forms';
@@ -32,8 +41,8 @@ import { MatomoModule } from 'ngx-matomo-client';
PickupShelfProcessingStatusPipe,
NavigateOnClickDirective,
MatomoModule,
LabelComponent
],
LabelComponent,
],
providers: [PickupShelfProcessingStatusPipe],
})
export class PickUpShelfListItemComponent {
@@ -45,6 +54,7 @@ export class PickUpShelfListItemComponent {
@Input() primaryOutletActive = false;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@Input() itemDetailsLink: any[] = [];
@Input() isItemSelectable?: boolean = undefined;
@@ -64,7 +74,9 @@ export class PickUpShelfListItemComponent {
}
get contributors() {
return this.item?.product?.contributors?.split(';').map((val) => val.trim());
return this.item?.product?.contributors
?.split(';')
.map((val) => val.trim());
}
get showChangeDate() {
@@ -73,11 +85,19 @@ export class PickUpShelfListItemComponent {
// Zeige nur CompartmentCode an wenn verfügbar
get showCompartmentCode() {
return !!this.item?.compartmentCode && (this.isTablet || this.isDesktopSmall || this.primaryOutletActive);
return (
!!this.item?.compartmentCode &&
(this.isTablet || this.isDesktopSmall || this.primaryOutletActive)
);
}
get processingStatusColor() {
return { 'background-color': this._processingStatusPipe.transform(this.item?.processingStatus, true) };
return {
'background-color': this._processingStatusPipe.transform(
this.item?.processingStatus,
true,
),
};
}
/**
@@ -90,14 +110,12 @@ export class PickUpShelfListItemComponent {
selected$ = this.store.selectedListItems$.pipe(
map((selectedListItems) =>
selectedListItems?.find((item) => item?.orderItemSubsetId === this.item?.orderItemSubsetId),
selectedListItems?.find(
(item) => item?.orderItemSubsetId === this.item?.orderItemSubsetId,
),
),
);
// Expose to template
Labeltype = Labeltype;
LabelPriority = LabelPriority;
constructor(
private _elRef: ElementRef,
private _environment: EnvironmentService,
@@ -125,7 +143,9 @@ export class PickUpShelfListItemComponent {
store: 'PickupShelfDetailsStore',
})) ?? [];
return items.some((i) => i.orderItemSubsetId === this.item.orderItemSubsetId);
return items.some(
(i) => i.orderItemSubsetId === this.item.orderItemSubsetId,
);
}
addOrderItemIntoCache() {
@@ -144,7 +164,10 @@ export class PickUpShelfListItemComponent {
}
scrollIntoView() {
this._elRef.nativeElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
this._elRef.nativeElement.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}
setSelected() {

View File

@@ -20,6 +20,7 @@
@import "../../../libs/ui/skeleton-loader/src/skeleton-loader.scss";
@import "../../../libs/ui/tooltip/src/tooltip.scss";
@import "../../../libs/ui/label/src/label.scss";
@import "../../../libs/ui/notice/src/notice.scss";
@import "../../../libs/ui/switch/src/switch.scss";
.input-control {

View File

@@ -1,33 +1,25 @@
import { argsToTemplate, type Meta, type StoryObj } from '@storybook/angular';
import { Labeltype, LabelPriority, LabelComponent } from '@isa/ui/label';
import { LabelComponent } from '@isa/ui/label';
type UiLabelInputs = {
type: Labeltype;
priority: LabelPriority;
type LabelInputs = {
active: boolean;
};
const meta: Meta<UiLabelInputs> = {
const meta: Meta<LabelInputs> = {
component: LabelComponent,
title: 'ui/label/Label',
argTypes: {
type: {
control: { type: 'select' },
options: Object.values(Labeltype),
description: 'Determines the label type',
},
priority: {
control: { type: 'select' },
options: Object.values(LabelPriority),
description: 'Determines the label priority',
active: {
control: { type: 'boolean' },
description: 'Determines if the label is active (hover/pressed state)',
},
},
args: {
type: 'tag',
priority: 'high',
active: false,
},
render: (args) => ({
props: args,
template: `<ui-label ${argsToTemplate(args)}>Prio 1</ui-label>`,
template: `<ui-label ${argsToTemplate(args)}>Prämie</ui-label>`,
}),
};
export default meta;
@@ -37,3 +29,21 @@ type Story = StoryObj<LabelComponent>;
export const Default: Story = {
args: {},
};
export const Active: Story = {
args: {
active: true,
},
};
export const RewardExample: Story = {
args: {},
render: () => ({
template: `
<div style="display: flex; gap: 1rem; align-items: center;">
<ui-label>Prämie</ui-label>
<ui-label [active]="true">Active</ui-label>
</div>
`,
}),
};

View File

@@ -0,0 +1,59 @@
import { argsToTemplate, type Meta, type StoryObj } from '@storybook/angular';
import { PrioLabelComponent } from '@isa/ui/label';
type PrioLabelInputs = {
priority: 1 | 2;
};
const meta: Meta<PrioLabelInputs> = {
component: PrioLabelComponent,
title: 'ui/label/PrioLabel',
argTypes: {
priority: {
control: { type: 'select' },
options: [1, 2],
description: 'The priority level (1 = high, 2 = low)',
},
},
args: {
priority: 1,
},
render: (args) => ({
props: args,
template: `<ui-prio-label ${argsToTemplate(args)}>Pflicht</ui-prio-label>`,
}),
};
export default meta;
type Story = StoryObj<PrioLabelComponent>;
export const Priority1: Story = {
args: {
priority: 1,
},
render: (args) => ({
props: args,
template: `<ui-prio-label [priority]="priority">Pflicht</ui-prio-label>`,
}),
};
export const Priority2: Story = {
args: {
priority: 2,
},
render: (args) => ({
props: args,
template: `<ui-prio-label [priority]="priority">Prio 2</ui-prio-label>`,
}),
};
export const AllPriorities: Story = {
render: () => ({
template: `
<div style="display: flex; gap: 1rem; align-items: center;">
<ui-prio-label [priority]="1">Pflicht</ui-prio-label>
<ui-prio-label [priority]="2">Prio 2</ui-prio-label>
</div>
`,
}),
};

View File

@@ -0,0 +1,70 @@
import { argsToTemplate, type Meta, type StoryObj } from '@storybook/angular';
import { NoticeComponent, NoticePriority } from '@isa/ui/notice';
type NoticeInputs = {
priority: NoticePriority;
};
const meta: Meta<NoticeInputs> = {
component: NoticeComponent,
title: 'ui/notice/Notice',
argTypes: {
priority: {
control: { type: 'select' },
options: Object.values(NoticePriority),
description: 'The priority level (high, medium, low)',
},
},
args: {
priority: NoticePriority.High,
},
render: (args) => ({
props: args,
template: `<ui-notice ${argsToTemplate(args)}>Important message</ui-notice>`,
}),
};
export default meta;
type Story = StoryObj<NoticeComponent>;
export const High: Story = {
args: {
priority: NoticePriority.High,
},
render: (args) => ({
props: args,
template: `<ui-notice [priority]="priority">Action Required</ui-notice>`,
}),
};
export const Medium: Story = {
args: {
priority: NoticePriority.Medium,
},
render: (args) => ({
props: args,
template: `<ui-notice [priority]="priority">Secondary Information</ui-notice>`,
}),
};
export const Low: Story = {
args: {
priority: NoticePriority.Low,
},
render: (args) => ({
props: args,
template: `<ui-notice [priority]="priority">Info Message</ui-notice>`,
}),
};
export const AllPriorities: Story = {
render: () => ({
template: `
<div style="display: flex; flex-direction: column; gap: 1rem;">
<ui-notice priority="high">High Priority</ui-notice>
<ui-notice priority="medium">Medium Priority</ui-notice>
<ui-notice priority="low">Low Priority</ui-notice>
</div>
`,
}),
};

View File

@@ -1,11 +1,11 @@
# Library Reference Guide
> **Last Updated:** 2025-11-27
> **Last Updated:** 2025-11-28
> **Angular Version:** 20.3.6
> **Nx Version:** 21.3.2
> **Total Libraries:** 73
> **Total Libraries:** 74
All 73 libraries in the monorepo have comprehensive README.md documentation located at `libs/[domain]/[layer]/[feature]/README.md`.
All 74 libraries in the monorepo have comprehensive README.md documentation located at `libs/[domain]/[layer]/[feature]/README.md`.
**IMPORTANT: Always use the `docs-researcher` subagent** to retrieve and analyze library documentation. This keeps the main context clean and prevents pollution.
@@ -297,12 +297,7 @@ Enterprise-grade barcode scanning library for ISA-Frontend using the Scandit SDK
---
## UI Component Libraries (18 libraries)
### `@isa/ui/label`
A flexible label component for displaying tags and notices with configurable priority levels across Angular applications.
**Location:** `libs/ui/label/`
## UI Component Libraries (19 libraries)
### `@isa/ui/bullet-list`
A lightweight bullet list component system for Angular applications supporting customizable icons and hierarchical content presentation.
@@ -349,6 +344,11 @@ A collection of reusable row components for displaying structured data with cons
**Location:** `libs/ui/item-rows/`
### `@isa/ui/label`
Label components for displaying tags, categories, and priority indicators.
**Location:** `libs/ui/label/`
### `@isa/ui/layout`
This library provides utilities and directives for responsive design and viewport behavior in Angular applications.
@@ -359,6 +359,11 @@ A lightweight Angular component library providing accessible menu components bui
**Location:** `libs/ui/menu/`
### `@isa/ui/notice`
A notice component for displaying prominent notifications and alerts with configurable priority levels.
**Location:** `libs/ui/notice/`
### `@isa/ui/progress-bar`
A lightweight Angular progress bar component supporting both determinate and indeterminate modes.

View File

@@ -38,7 +38,7 @@
class="w-fit"
[class.row-start-second]="desktopBreakpoint()"
>
<ui-label [type]="Labeltype.Notice">{{ impediment() }}</ui-label>
<ui-notice>{{ impediment() }}</ui-notice>
</ui-item-row-data>
}

View File

@@ -28,7 +28,7 @@ import { Breakpoint, breakpoint } from '@isa/ui/layout';
import { injectRemissionListType } from '../injects/inject-remission-list-type';
import { RemissionListItemSelectComponent } from './remission-list-item-select.component';
import { RemissionListItemActionsComponent } from './remission-list-item-actions.component';
import { LabelComponent, Labeltype } from '@isa/ui/label';
import { NoticeComponent } from '@isa/ui/notice';
/**
* Component representing a single item in the remission list.
@@ -58,16 +58,10 @@ import { LabelComponent, Labeltype } from '@isa/ui/label';
ItemRowDataImports,
RemissionListItemSelectComponent,
RemissionListItemActionsComponent,
LabelComponent,
NoticeComponent,
],
})
export class RemissionListItemComponent implements OnDestroy {
/**
* Type of label to display for the item.
* Defaults to 'tag', can be changed to 'notice' or other types as needed.
*/
Labeltype = Labeltype;
/**
* Store for managing selected remission quantities.
* @private
@@ -155,6 +149,7 @@ export class RemissionListItemComponent implements OnDestroy {
* Uses the store's selected quantity for the item's ID.
*/
selectedStockToRemit = computed(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
() => this.#store.selectedQuantity()?.[this.item().id!],
);

View File

@@ -13,13 +13,10 @@
/>
@if (tag) {
<ui-label
<ui-prio-label
data-what="remission-label"
[type]="Labeltype.Tag"
[priority]="
tag === RemissionItemTags.Prio2 ? LabelPriority.Low : LabelPriority.High
"
>{{ tag }}</ui-label
[priority]="tag === RemissionItemTags.Prio2 ? 2 : 1"
>{{ tag }}</ui-prio-label
>
}
</div>

View File

@@ -4,7 +4,7 @@ import { RemissionItem } from '@isa/remission/data-access';
import { ProductImageDirective } from '@isa/shared/product-image';
import { ProductRouterLinkDirective } from '@isa/shared/product-router-link';
import { ProductFormatComponent } from '@isa/shared/product-format';
import { LabelComponent, LabelPriority, Labeltype } from '@isa/ui/label';
import { PrioLabelComponent } from '@isa/ui/label';
export type ProductInfoItem = Pick<
RemissionItem,
@@ -28,7 +28,7 @@ export const RemissionItemTags = {
ProductRouterLinkDirective,
CurrencyPipe,
ProductFormatComponent,
LabelComponent,
PrioLabelComponent,
],
host: {
'[class]': 'classList',
@@ -38,8 +38,6 @@ export const RemissionItemTags = {
},
})
export class ProductInfoComponent {
Labeltype = Labeltype;
LabelPriority = LabelPriority;
RemissionItemTags = RemissionItemTags;
readonly classList: ReadonlyArray<string> = [
'grid',

View File

@@ -1,813 +1,88 @@
# @isa/ui/label
A flexible label component for displaying tags and notices with configurable priority levels across Angular applications.
Label components for displaying tags, categories, and priority indicators.
## Overview
The Label UI library provides a standalone Angular component for displaying visual labels with semantic meaning. It supports two distinct label types (Tag and Notice) and three priority levels (High, Medium, Low), enabling consistent visual communication of information status, categorization, and importance throughout the ISA application.
## Table of Contents
- [Features](#features)
- [Quick Start](#quick-start)
- [Core Concepts](#core-concepts)
- [API Reference](#api-reference)
- [Usage Examples](#usage-examples)
- [Styling and Customization](#styling-and-customization)
- [Testing](#testing)
- [Architecture Notes](#architecture-notes)
## Features
- **Two label types** - Tag and Notice for different semantic contexts
- **Three priority levels** - High, Medium, and Low for visual hierarchy
- **Standalone component** - Modern Angular architecture with explicit imports
- **Signal-based reactivity** - Uses Angular signals for efficient updates
- **Type-safe API** - TypeScript enums for label type and priority configuration
- **CSS class composition** - Dynamic class generation based on type and priority
- **OnPush change detection** - Optimized rendering performance
- **ViewEncapsulation.None** - Flexible styling with global CSS classes
- **Content projection** - Full control over label content via ng-content
- **Computed classes** - Reactive CSS class computation using Angular signals
## Quick Start
### 1. Import the Component
## Installation
```typescript
import { Component } from '@angular/core';
import { LabelComponent, Labeltype, LabelPriority } from '@isa/ui/label';
@Component({
selector: 'app-product-status',
template: `
<ui-label [type]="Labeltype.Tag" [priority]="LabelPriority.High">
In Stock
</ui-label>
`,
imports: [LabelComponent]
})
export class ProductStatusComponent {
Labeltype = Labeltype;
LabelPriority = LabelPriority;
}
import { LabelComponent, PrioLabelComponent } from '@isa/ui/label';
```
### 2. Basic Tag Label
```typescript
@Component({
template: `
<ui-label [type]="Labeltype.Tag" [priority]="LabelPriority.Medium">
New Arrival
</ui-label>
`,
imports: [LabelComponent]
})
export class ProductBadgeComponent {
Labeltype = Labeltype;
LabelPriority = LabelPriority;
}
```
### 3. Notice Label
```typescript
@Component({
template: `
<ui-label [type]="Labeltype.Notice" [priority]="LabelPriority.High">
Action Required
</ui-label>
`,
imports: [LabelComponent]
})
export class AlertComponent {
Labeltype = Labeltype;
LabelPriority = LabelPriority;
}
```
## Core Concepts
### Label Types
The component supports two distinct label types, each with specific semantic meaning:
#### 1. Tag (Default)
- **Purpose**: Categorization, status indication, metadata display
- **Use Cases**: Product categories, order status, item tags, filters
- **Visual Style**: Typically compact, pill-shaped design
- **CSS Class**: `ui-label__tag`
#### 2. Notice
- **Purpose**: Notifications, alerts, important messages
- **Use Cases**: Error messages, warnings, system notifications, user alerts
- **Visual Style**: Typically more prominent, attention-grabbing design
- **CSS Class**: `ui-label__notice`
### Priority Levels
Each label type can have one of three priority levels that control visual hierarchy:
#### 1. High Priority (Default)
- **Purpose**: Critical information, urgent status, primary actions
- **Visual Characteristics**: Most prominent styling, often with bold colors
- **Use Cases**: Error states, critical alerts, primary status
- **CSS Classes**:
- `ui-label__tag-priority-high` (for Tag type)
- `ui-label__notice-priority-high` (for Notice type)
#### 2. Medium Priority
- **Purpose**: Important but non-critical information
- **Visual Characteristics**: Moderate emphasis, balanced contrast
- **Use Cases**: Warnings, secondary status, informational notices
- **CSS Classes**:
- `ui-label__tag-priority-medium` (for Tag type)
- `ui-label__notice-priority-medium` (for Notice type)
#### 3. Low Priority
- **Purpose**: Supplementary information, subtle indicators
- **Visual Characteristics**: Minimal emphasis, subtle colors
- **Use Cases**: Hints, optional metadata, background information
- **CSS Classes**:
- `ui-label__tag-priority-low` (for Tag type)
- `ui-label__notice-priority-low` (for Notice type)
### Signal-Based Reactivity
The component uses Angular signals for reactive CSS class computation:
```typescript
// Signal inputs
type = input<Labeltype>(Labeltype.Tag);
priority = input<LabelPriority>(LabelPriority.High);
// Computed signal for type class
typeClass = computed(() => `ui-label__${this.type()}`);
// Computed signal for priority class (combines type and priority)
priorityClass = computed(() => `${this.typeClass()}-priority-${this.priority()}`);
```
This approach provides:
- **Automatic updates** - Classes update when inputs change
- **Efficient rendering** - Only recomputes when dependencies change
- **Type safety** - TypeScript ensures valid type/priority combinations
### CSS Class Structure
The component applies a hierarchical class structure:
```
ui-label (base class)
├── ui-label__tag (type class for Tag)
│ ├── ui-label__tag-priority-high (Tag + High priority)
│ ├── ui-label__tag-priority-medium (Tag + Medium priority)
│ └── ui-label__tag-priority-low (Tag + Low priority)
└── ui-label__notice (type class for Notice)
├── ui-label__notice-priority-high (Notice + High priority)
├── ui-label__notice-priority-medium (Notice + Medium priority)
└── ui-label__notice-priority-low (Notice + Low priority)
```
## API Reference
## Components
### LabelComponent
Standalone component for displaying labels with type and priority.
A badge-style label for tags, filters, and reward indicators.
#### Selector
```html
<ui-label>Content</ui-label>
```
**Figma:** [ISA Design System - Label](https://www.figma.com/design/bK0IW6akzSjHxmMwQfVPRW/ISA-DESIGN-SYSTEM?node-id=2806-8052&m=dev)
#### Inputs
##### `type`
- **Type**: `Labeltype`
- **Default**: `Labeltype.Tag`
- **Description**: The semantic type of the label
- **Values**:
- `Labeltype.Tag` - For categorization and status
- `Labeltype.Notice` - For notifications and alerts
**Example:**
```html
<ui-label [type]="Labeltype.Notice">Important Notice</ui-label>
```
##### `priority`
- **Type**: `LabelPriority`
- **Default**: `LabelPriority.High`
- **Description**: The visual priority level of the label
- **Values**:
- `LabelPriority.High` - Highest visual emphasis
- `LabelPriority.Medium` - Moderate visual emphasis
- `LabelPriority.Low` - Subtle visual emphasis
**Example:**
```html
<ui-label [priority]="LabelPriority.Medium">Optional Info</ui-label>
```
#### Computed Properties
##### `typeClass()`
- **Type**: `Signal<string>`
- **Returns**: CSS class string based on current type
- **Format**: `ui-label__${type}`
- **Example**: `"ui-label__tag"` or `"ui-label__notice"`
##### `priorityClass()`
- **Type**: `Signal<string>`
- **Returns**: CSS class string combining type and priority
- **Format**: `ui-label__${type}-priority-${priority}`
- **Example**: `"ui-label__tag-priority-high"`
#### Host Classes
The component automatically applies the following classes to its host element:
```typescript
['ui-label', typeClass(), priorityClass()]
```
Example rendered classes:
```html
<ui-label class="ui-label ui-label__tag ui-label__tag-priority-high">
Content
</ui-label>
```
### Type Definitions
#### Labeltype
TypeScript enum for label type values:
```typescript
export const Labeltype = {
Tag: 'tag',
Notice: 'notice',
} as const;
export type Labeltype = (typeof Labeltype)[keyof typeof Labeltype];
```
**Usage:**
```typescript
import { Labeltype } from '@isa/ui/label';
// In template
[type]="Labeltype.Tag"
[type]="Labeltype.Notice"
// In component class
readonly labelType = Labeltype.Tag;
```
#### LabelPriority
TypeScript enum for priority level values:
```typescript
export const LabelPriority = {
High: 'high',
Medium: 'medium',
Low: 'low',
} as const;
export type LabelPriority = (typeof LabelPriority)[keyof typeof LabelPriority];
```
**Usage:**
```typescript
import { LabelPriority } from '@isa/ui/label';
// In template
[priority]="LabelPriority.High"
[priority]="LabelPriority.Medium"
[priority]="LabelPriority.Low"
// In component class
readonly priority = LabelPriority.Medium;
```
## Usage Examples
### Product Status Tags
```typescript
import { Component } from '@angular/core';
import { LabelComponent, Labeltype, LabelPriority } from '@isa/ui/label';
@Component({
selector: 'app-product-card',
template: `
<div class="product-card">
<h3>{{ product.name }}</h3>
<!-- In Stock - High priority tag -->
<ui-label
[type]="Labeltype.Tag"
[priority]="LabelPriority.High">
In Stock
</ui-label>
<!-- Category - Medium priority tag -->
<ui-label
[type]="Labeltype.Tag"
[priority]="LabelPriority.Medium">
Electronics
</ui-label>
<!-- Condition - Low priority tag -->
<ui-label
[type]="Labeltype.Tag"
[priority]="LabelPriority.Low">
New
</ui-label>
</div>
`,
imports: [LabelComponent]
})
export class ProductCardComponent {
Labeltype = Labeltype;
LabelPriority = LabelPriority;
product = {
name: 'Wireless Headphones',
inStock: true,
category: 'Electronics'
};
}
```
### Order Status Indicators
```typescript
import { Component, input, computed } from '@angular/core';
import { LabelComponent, Labeltype, LabelPriority } from '@isa/ui/label';
@Component({
selector: 'app-order-status',
template: `
<ui-label
[type]="Labeltype.Tag"
[priority]="statusPriority()">
{{ statusText() }}
</ui-label>
`,
imports: [LabelComponent]
})
export class OrderStatusComponent {
Labeltype = Labeltype;
status = input.required<'pending' | 'processing' | 'shipped' | 'delivered'>();
statusText = computed(() => {
const statusMap = {
pending: 'Pending',
processing: 'Processing',
shipped: 'Shipped',
delivered: 'Delivered'
};
return statusMap[this.status()];
});
statusPriority = computed(() => {
const priorityMap = {
pending: LabelPriority.High,
processing: LabelPriority.High,
shipped: LabelPriority.Medium,
delivered: LabelPriority.Low
};
return priorityMap[this.status()];
});
}
```
### System Notifications
```typescript
import { Component, input } from '@angular/core';
import { LabelComponent, Labeltype, LabelPriority } from '@isa/ui/label';
interface Notification {
id: number;
message: string;
type: 'error' | 'warning' | 'info';
}
@Component({
selector: 'app-notification-item',
template: `
<div class="notification">
<ui-label
[type]="Labeltype.Notice"
[priority]="getNotificationPriority()">
{{ getNotificationLabel() }}
</ui-label>
<p>{{ notification().message }}</p>
</div>
`,
imports: [LabelComponent]
})
export class NotificationItemComponent {
Labeltype = Labeltype;
LabelPriority = LabelPriority;
notification = input.required<Notification>();
getNotificationPriority(): LabelPriority {
const type = this.notification().type;
switch (type) {
case 'error':
return LabelPriority.High;
case 'warning':
return LabelPriority.Medium;
case 'info':
return LabelPriority.Low;
}
}
getNotificationLabel(): string {
const type = this.notification().type;
return type.charAt(0).toUpperCase() + type.slice(1);
}
}
```
### Dynamic Labels with NgFor
```typescript
import { Component } from '@angular/core';
import { LabelComponent, Labeltype, LabelPriority } from '@isa/ui/label';
@Component({
selector: 'app-product-tags',
template: `
<div class="tags-container">
@for (tag of tags; track tag.id) {
<ui-label
[type]="Labeltype.Tag"
[priority]="tag.priority">
{{ tag.label }}
</ui-label>
}
</div>
`,
imports: [LabelComponent]
})
export class ProductTagsComponent {
Labeltype = Labeltype;
tags = [
{ id: 1, label: 'Sale', priority: LabelPriority.High },
{ id: 2, label: 'Free Shipping', priority: LabelPriority.Medium },
{ id: 3, label: 'Eco-Friendly', priority: LabelPriority.Low },
];
}
```
### Conditional Label Display
```typescript
import { Component, input, computed } from '@angular/core';
import { LabelComponent, Labeltype, LabelPriority } from '@isa/ui/label';
@Component({
selector: 'app-inventory-status',
template: `
@if (showStockLabel()) {
<ui-label
[type]="Labeltype.Tag"
[priority]="stockPriority()">
{{ stockLabel() }}
</ui-label>
}
`,
imports: [LabelComponent]
})
export class InventoryStatusComponent {
Labeltype = Labeltype;
LabelPriority = LabelPriority;
stockQuantity = input.required<number>();
showStockLabel = computed(() => this.stockQuantity() < 10);
stockLabel = computed(() => {
const qty = this.stockQuantity();
if (qty === 0) return 'Out of Stock';
if (qty < 5) return 'Low Stock';
return 'Limited Stock';
});
stockPriority = computed(() => {
const qty = this.stockQuantity();
if (qty === 0) return LabelPriority.High;
if (qty < 5) return LabelPriority.High;
return LabelPriority.Medium;
});
}
```
## Styling and Customization
### CSS Class Hierarchy
The component generates a predictable class structure for styling:
```css
/* Base class applied to all labels */
.ui-label {
display: inline-block;
font-family: var(--isa-font-family);
/* Base styles */
}
/* Type-specific base styles */
.ui-label__tag {
/* Tag-specific base styles */
}
.ui-label__notice {
/* Notice-specific base styles */
}
/* Priority-specific styles for Tags */
.ui-label__tag-priority-high {
background-color: var(--isa-accent-red);
color: white;
font-weight: 600;
}
.ui-label__tag-priority-medium {
background-color: var(--isa-accent-yellow);
color: var(--isa-text-primary);
font-weight: 500;
}
.ui-label__tag-priority-low {
background-color: var(--isa-neutral-200);
color: var(--isa-text-secondary);
font-weight: 400;
}
/* Priority-specific styles for Notices */
.ui-label__notice-priority-high {
border-left: 4px solid var(--isa-accent-red);
background-color: var(--isa-accent-red-light);
padding: 0.5rem 1rem;
}
.ui-label__notice-priority-medium {
border-left: 4px solid var(--isa-accent-yellow);
background-color: var(--isa-accent-yellow-light);
padding: 0.5rem 1rem;
}
.ui-label__notice-priority-low {
border-left: 4px solid var(--isa-neutral-300);
background-color: var(--isa-neutral-100);
padding: 0.5rem 1rem;
}
```
### Custom Styling Example
```scss
// Override specific label styles
.product-card {
ui-label {
border-radius: 4px;
padding: 0.25rem 0.75rem;
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
// Custom high-priority tag style
.ui-label__tag-priority-high {
animation: pulse 2s infinite;
}
// Custom notice styles
.ui-label__notice {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
}
```
### Tailwind CSS Integration
The component works seamlessly with Tailwind's design system:
#### Usage
```html
<!-- Using Tailwind utility classes alongside the component -->
<ui-label
[type]="Labeltype.Tag"
[priority]="LabelPriority.High"
class="rounded-full px-3 py-1 text-xs font-semibold">
Featured
</ui-label>
<!-- Basic label -->
<ui-label>Prämie</ui-label>
<!-- Active state (hover/pressed) -->
<ui-label [active]="true">Selected</ui-label>
```
## Testing
#### API
The library uses **Vitest** with **Angular Testing Utilities** for testing.
| Input | Type | Default | Description |
| -------- | --------- | ------- | ------------------------------------ |
| `active` | `boolean` | `false` | Active state (adds background color) |
### Running Tests
### PrioLabelComponent
```bash
# Run tests for this library
npx nx test label --skip-nx-cache
A priority indicator label with two priority levels.
# Run tests with coverage
npx nx test label --code-coverage --skip-nx-cache
**Figma:** [ISA Design System - Prio Label](https://www.figma.com/design/bK0IW6akzSjHxmMwQfVPRW/ISA-DESIGN-SYSTEM?node-id=682-2836&m=dev)
# Run tests in watch mode
npx nx test label --watch
#### Usage
```html
<!-- Priority 1 (high) - default -->
<ui-prio-label [priority]="1">Pflicht</ui-prio-label>
<!-- Priority 2 (low) -->
<ui-prio-label [priority]="2">Prio 2</ui-prio-label>
```
### Test Structure
#### API
The library includes comprehensive unit tests covering:
| Input | Type | Default | Description |
| ---------- | -------- | ------- | ----------------------------------- |
| `priority` | `1 \| 2` | `1` | Priority level (1 = high, 2 = low ) |
- **Type input** - Validates correct CSS class generation for each type
- **Priority input** - Validates correct CSS class generation for each priority
- **Default values** - Tests default type and priority behavior
- **Content projection** - Tests ng-content rendering
- **Signal reactivity** - Tests computed class updates when inputs change
- **Class composition** - Tests correct combination of base, type, and priority classes
## CSS Classes
### Example Test
### LabelComponent
- `.ui-label` - Base class
- `.ui-label--active` - Active state
### PrioLabelComponent
- `.ui-prio-label` - Base class
- `.ui-prio-label--1` - Priority 1 (dark background)
- `.ui-prio-label--2` - Priority 2 (light background)
## Accessibility
Both components include:
- `role="status"` - Indicates status information
- E2E testing attributes (`data-what`, `data-which`)
## E2E Testing
```typescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { describe, it, expect, beforeEach } from 'vitest';
import { LabelComponent, Labeltype, LabelPriority } from './label.component';
// Select all labels
page.locator('[data-what="label"]');
describe('LabelComponent', () => {
let component: LabelComponent;
let fixture: ComponentFixture<LabelComponent>;
// Select all priority labels
page.locator('[data-what="prio-label"]');
beforeEach(() => {
TestBed.configureTestingModule({
imports: [LabelComponent]
});
fixture = TestBed.createComponent(LabelComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have default type as Tag', () => {
expect(component.type()).toBe(Labeltype.Tag);
});
it('should have default priority as High', () => {
expect(component.priority()).toBe(LabelPriority.High);
});
it('should generate correct type class for Tag', () => {
fixture.componentRef.setInput('type', Labeltype.Tag);
expect(component.typeClass()).toBe('ui-label__tag');
});
it('should generate correct priority class', () => {
fixture.componentRef.setInput('type', Labeltype.Tag);
fixture.componentRef.setInput('priority', LabelPriority.Medium);
expect(component.priorityClass()).toBe('ui-label__tag-priority-medium');
});
it('should apply all classes to host element', () => {
fixture.componentRef.setInput('type', Labeltype.Notice);
fixture.componentRef.setInput('priority', LabelPriority.Low);
fixture.detectChanges();
const element = fixture.nativeElement as HTMLElement;
expect(element.classList.contains('ui-label')).toBe(true);
expect(element.classList.contains('ui-label__notice')).toBe(true);
expect(element.classList.contains('ui-label__notice-priority-low')).toBe(true);
});
});
// Select specific priority
page.locator('[data-what="prio-label"][data-which="priority-1"]');
```
## Architecture Notes
### Current Architecture
The library follows Angular standalone component architecture:
```
LabelComponent (Standalone)
├── Signal Inputs
│ ├── type: Signal<Labeltype>
│ └── priority: Signal<LabelPriority>
├── Computed Signals
│ ├── typeClass(): string
│ └── priorityClass(): string
└── Host Binding
└── [class]: computed classes array
```
### Design Decisions
#### 1. Standalone Component Architecture
**Decision**: Use standalone component instead of NgModule
**Rationale**:
- Aligns with Angular 20.1.2 best practices
- Reduces boilerplate and complexity
- Improves tree-shaking and bundle optimization
- Enables explicit, localized imports
**Impact**: Simpler consumption in feature modules
#### 2. Signal-Based Reactivity
**Decision**: Use `input()` and `computed()` signals instead of traditional inputs
**Rationale**:
- Modern Angular reactivity model
- Automatic dependency tracking
- Better performance with change detection
- Type-safe reactive transformations
**Impact**: Efficient CSS class updates without manual change detection
#### 3. ViewEncapsulation.None
**Decision**: Use ViewEncapsulation.None instead of Emulated or ShadowDOM
**Rationale**:
- Enables global styling through BEM-like class naming
- Consistent with ISA design system approach
- Easier integration with Tailwind CSS
- Flexible theming capabilities
**Impact**: Requires careful CSS class naming to avoid conflicts
#### 4. OnPush Change Detection
**Decision**: Use OnPush change detection strategy
**Rationale**:
- Optimal performance with signal-based inputs
- Reduces unnecessary change detection cycles
- Signals automatically trigger required updates
- Best practice for presentational components
**Impact**: Better performance, especially in large lists
#### 5. TypeScript Const Assertions for Enums
**Decision**: Use const objects with type inference instead of traditional enums
**Rationale**:
- Better type safety with literal types
- Improved autocomplete in IDEs
- Smaller JavaScript bundle size
- More flexible than traditional TypeScript enums
**Impact**: Type-safe API with minimal runtime overhead
### Performance Considerations
1. **Signal-based inputs** - Only recompute classes when inputs actually change
2. **OnPush change detection** - Minimal change detection overhead
3. **Computed signals** - Efficient memoization of class strings
4. **No template logic** - All computation in component class
5. **Lightweight DOM** - Simple ng-content projection with no wrapper elements
### Future Enhancements
Potential improvements identified:
1. **Icon Support** - Add optional icon input for visual enhancement
2. **Dismissible Labels** - Add optional close button with output event
3. **Animation Support** - Add entry/exit animations for dynamic labels
4. **Theme Variants** - Support custom color themes via injection tokens
5. **Size Variants** - Add size input (small, medium, large)
6. **Accessibility Improvements** - Add ARIA attributes for screen readers
7. **Tooltip Integration** - Built-in tooltip support for truncated content
## Dependencies
### Required Libraries
- `@angular/core` - Angular framework (20.1.2)
- `@angular/common` - CommonModule for basic directives
### Optional Libraries
None - this is a pure presentation component with no external dependencies.
### Path Alias
Import from: `@isa/ui/label`
## License
Internal ISA Frontend library - not for external distribution.

View File

@@ -12,7 +12,7 @@ module.exports = [
'error',
{
type: 'attribute',
prefix: 'lib',
prefix: 'ui',
style: 'camelCase',
},
],
@@ -20,7 +20,7 @@ module.exports = [
'error',
{
type: 'element',
prefix: 'lib',
prefix: 'ui',
style: 'kebab-case',
},
],

View File

@@ -1,30 +1,28 @@
{
"name": "label",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/ui/label/src",
"prefix": "lib",
"projectType": "library",
"tags": [],
"targets": {
"test": {
"executor": "@nx/vite:test",
"outputs": [
"{options.reportsDirectory}"
],
"options": {
"reportsDirectory": "../../../coverage/libs/ui/label"
},
"configurations": {
"ci": {
"mode": "run",
"coverage": {
"enabled": true
}
}
}
},
"lint": {
"executor": "@nx/eslint:lint"
}
}
}
{
"name": "ui-label",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/ui/label/src",
"prefix": "ui",
"projectType": "library",
"tags": [],
"targets": {
"test": {
"executor": "@nx/vite:test",
"outputs": ["{options.reportsDirectory}"],
"options": {
"reportsDirectory": "../../../coverage/libs/ui/label"
},
"configurations": {
"ci": {
"mode": "run",
"coverage": {
"enabled": true
}
}
}
},
"lint": {
"executor": "@nx/eslint:lint"
}
}
}

View File

@@ -1,2 +1,2 @@
export * from './lib/label.component';
export * from './lib/types';
export * from './lib/prio-label.component';

View File

@@ -1 +1,2 @@
@use "lib/label";
@use "lib/prio-label";

View File

@@ -1,31 +1,8 @@
.ui-label {
@apply flex items-center justify-center text-ellipsis whitespace-nowrap;
@apply bg-isa-white px-3 py-[0.125rem] min-w-16 rounded-[3.125rem] isa-text-caption-regular text-isa-neutral-900 border border-solid border-isa-neutral-900;
}
.ui-label__tag {
@apply px-3 py-[0.125rem] min-w-14 rounded-[3.125rem] isa-text-caption-regular;
}
.ui-label__tag-priority-high {
@apply bg-isa-neutral-700 text-isa-neutral-400;
}
.ui-label__tag-priority-low {
@apply bg-isa-neutral-300 text-isa-neutral-600;
}
.ui-label__notice {
@apply p-2 min-w-48 rounded-lg isa-text-body-2-bold text-isa-neutral-900;
}
.ui-label__notice-priority-high {
@apply bg-isa-secondary-100;
}
.ui-label__notice-priority-medium {
@apply bg-isa-neutral-100;
}
.ui-label__notice-priority-low {
@apply bg-transparent;
.ui-label--active {
@apply bg-isa-neutral-200;
}

View File

@@ -0,0 +1,12 @@
.ui-prio-label {
@apply flex items-center justify-center text-ellipsis whitespace-nowrap;
@apply px-3 py-[0.125rem] min-w-14 rounded-[3.125rem] isa-text-caption-regular;
}
.ui-prio-label--1 {
@apply bg-isa-neutral-700 text-isa-neutral-400;
}
.ui-prio-label--2 {
@apply bg-isa-neutral-300 text-isa-neutral-600;
}

View File

@@ -1,7 +1,6 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LabelComponent } from './label.component';
import { LabelPriority, Labeltype } from './types';
describe('LabelComponent', () => {
let component: LabelComponent;
@@ -22,127 +21,47 @@ describe('LabelComponent', () => {
expect(component).toBeTruthy();
});
it('should have default type as tag', () => {
expect(component.type()).toBe(Labeltype.Tag);
it('should have default active as false', () => {
expect(component.active()).toBe(false);
});
it('should have default priority as high', () => {
expect(component.priority()).toBe(LabelPriority.High);
});
it('should accept notice type', () => {
fixture.componentRef.setInput('type', Labeltype.Notice);
it('should accept active input', () => {
fixture.componentRef.setInput('active', true);
fixture.detectChanges();
expect(component.type()).toBe(Labeltype.Notice);
expect(component.active()).toBe(true);
});
it('should accept different priority levels', () => {
fixture.componentRef.setInput('priority', LabelPriority.Medium);
fixture.detectChanges();
expect(component.priority()).toBe(LabelPriority.Medium);
fixture.componentRef.setInput('priority', LabelPriority.Low);
fixture.detectChanges();
expect(component.priority()).toBe(LabelPriority.Low);
it('should return null for activeClass when not active', () => {
expect(component.activeClass()).toBeNull();
});
it('should have correct CSS classes for default type and priority', () => {
expect(component.typeClass()).toBe('ui-label__tag');
expect(component.priorityClass()).toBe('ui-label__tag-priority-high');
});
it('should have correct CSS classes for notice type', () => {
fixture.componentRef.setInput('type', Labeltype.Notice);
it('should return active class when active is true', () => {
fixture.componentRef.setInput('active', true);
fixture.detectChanges();
expect(component.typeClass()).toBe('ui-label__notice');
expect(component.priorityClass()).toBe('ui-label__notice-priority-high');
});
it('should have correct CSS classes for different priorities', () => {
fixture.componentRef.setInput('priority', LabelPriority.Medium);
fixture.detectChanges();
expect(component.priorityClass()).toBe('ui-label__tag-priority-medium');
fixture.componentRef.setInput('priority', LabelPriority.Low);
fixture.detectChanges();
expect(component.priorityClass()).toBe('ui-label__tag-priority-low');
});
it('should set host classes correctly', () => {
const hostElement = fixture.debugElement.nativeElement;
expect(hostElement.classList.contains('ui-label')).toBe(true);
expect(hostElement.classList.contains('ui-label__tag')).toBe(true);
expect(
hostElement.classList.contains('ui-label__tag-priority-high'),
).toBe(true);
expect(component.activeClass()).toBe('ui-label--active');
});
});
describe('Template Rendering', () => {
it('should display content with default type and priority classes', () => {
const labelElement = fixture.debugElement.nativeElement;
expect(labelElement.classList.contains('ui-label')).toBe(true);
expect(labelElement.classList.contains('ui-label__tag')).toBe(true);
expect(
labelElement.classList.contains('ui-label__tag-priority-high'),
).toBe(true);
it('should display content with default classes', () => {
const hostElement = fixture.debugElement.nativeElement;
expect(hostElement.classList.contains('ui-label')).toBe(true);
});
it('should display content with notice type', () => {
fixture.componentRef.setInput('type', Labeltype.Notice);
it('should display active class when active is true', () => {
fixture.componentRef.setInput('active', true);
fixture.detectChanges();
const labelElement = fixture.debugElement.nativeElement;
expect(labelElement.classList.contains('ui-label')).toBe(true);
expect(labelElement.classList.contains('ui-label__notice')).toBe(true);
expect(
labelElement.classList.contains('ui-label__notice-priority-high'),
).toBe(true);
const hostElement = fixture.debugElement.nativeElement;
expect(hostElement.classList.contains('ui-label--active')).toBe(true);
});
it('should display content with different priority levels', () => {
fixture.componentRef.setInput('priority', LabelPriority.Low);
it('should not display active class when active is false', () => {
fixture.componentRef.setInput('active', false);
fixture.detectChanges();
const labelElement = fixture.debugElement.nativeElement;
expect(labelElement.classList.contains('ui-label')).toBe(true);
expect(labelElement.classList.contains('ui-label__tag')).toBe(true);
expect(
labelElement.classList.contains('ui-label__tag-priority-low'),
).toBe(true);
});
});
describe('Input Validation', () => {
it('should handle type input changes', () => {
fixture.componentRef.setInput('type', Labeltype.Tag);
fixture.detectChanges();
expect(component.type()).toBe(Labeltype.Tag);
expect(component.typeClass()).toBe('ui-label__tag');
expect(component.priorityClass()).toBe('ui-label__tag-priority-high');
fixture.componentRef.setInput('type', Labeltype.Notice);
fixture.detectChanges();
expect(component.type()).toBe(Labeltype.Notice);
expect(component.typeClass()).toBe('ui-label__notice');
expect(component.priorityClass()).toBe('ui-label__notice-priority-high');
});
it('should handle priority input changes', () => {
fixture.componentRef.setInput('priority', LabelPriority.High);
fixture.detectChanges();
expect(component.priority()).toBe(LabelPriority.High);
expect(component.priorityClass()).toBe('ui-label__tag-priority-high');
fixture.componentRef.setInput('priority', LabelPriority.Medium);
fixture.detectChanges();
expect(component.priority()).toBe(LabelPriority.Medium);
expect(component.priorityClass()).toBe('ui-label__tag-priority-medium');
fixture.componentRef.setInput('priority', LabelPriority.Low);
fixture.detectChanges();
expect(component.priority()).toBe(LabelPriority.Low);
expect(component.priorityClass()).toBe('ui-label__tag-priority-low');
const hostElement = fixture.debugElement.nativeElement;
expect(hostElement.classList.contains('ui-label--active')).toBe(false);
});
});
@@ -150,72 +69,36 @@ describe('LabelComponent', () => {
it('should have proper host class binding', () => {
const hostElement = fixture.debugElement.nativeElement;
expect(hostElement.classList.contains('ui-label')).toBe(true);
expect(hostElement.classList.length).toBeGreaterThan(0);
});
it('should update classes when type changes', () => {
it('should have E2E testing attributes', () => {
const hostElement = fixture.debugElement.nativeElement;
// Initial state
expect(hostElement.classList.contains('ui-label__tag')).toBe(true);
expect(hostElement.classList.contains('ui-label__notice')).toBe(false);
expect(
hostElement.classList.contains('ui-label__tag-priority-high'),
).toBe(true);
// Change to notice
fixture.componentRef.setInput('type', Labeltype.Notice);
fixture.detectChanges();
expect(hostElement.classList.contains('ui-label__tag')).toBe(false);
expect(hostElement.classList.contains('ui-label__notice')).toBe(true);
expect(
hostElement.classList.contains('ui-label__tag-priority-high'),
).toBe(false);
expect(
hostElement.classList.contains('ui-label__notice-priority-high'),
).toBe(true);
expect(hostElement.getAttribute('data-what')).toBe('label');
expect(hostElement.getAttribute('data-which')).toBe('label');
});
it('should update classes when priority changes', () => {
it('should have accessibility role', () => {
const hostElement = fixture.debugElement.nativeElement;
// Initial state
expect(
hostElement.classList.contains('ui-label__tag-priority-high'),
).toBe(true);
expect(
hostElement.classList.contains('ui-label__tag-priority-medium'),
).toBe(false);
// Change to medium priority
fixture.componentRef.setInput('priority', LabelPriority.Medium);
fixture.detectChanges();
expect(
hostElement.classList.contains('ui-label__tag-priority-high'),
).toBe(false);
expect(
hostElement.classList.contains('ui-label__tag-priority-medium'),
).toBe(true);
expect(hostElement.getAttribute('role')).toBe('status');
});
it('should maintain both type and priority classes simultaneously', () => {
it('should update classes when active changes', () => {
const hostElement = fixture.debugElement.nativeElement;
fixture.componentRef.setInput('type', Labeltype.Notice);
fixture.componentRef.setInput('priority', LabelPriority.Low);
// Initial state (not active)
expect(hostElement.classList.contains('ui-label--active')).toBe(false);
// Change to active
fixture.componentRef.setInput('active', true);
fixture.detectChanges();
expect(hostElement.classList.contains('ui-label')).toBe(true);
expect(hostElement.classList.contains('ui-label__notice')).toBe(true);
expect(
hostElement.classList.contains('ui-label__notice-priority-low'),
).toBe(true);
expect(hostElement.classList.contains('ui-label__tag')).toBe(false);
expect(
hostElement.classList.contains('ui-label__tag-priority-high'),
).toBe(false);
expect(hostElement.classList.contains('ui-label--active')).toBe(true);
// Change back to not active
fixture.componentRef.setInput('active', false);
fixture.detectChanges();
expect(hostElement.classList.contains('ui-label--active')).toBe(false);
});
});
});

View File

@@ -5,35 +5,35 @@ import {
input,
ViewEncapsulation,
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { LabelPriority, Labeltype } from './types';
/**
* A component that displays a label with a specific type and priority.
* The label can be used to indicate tags or notices with different priorities.
* A component that displays a label badge.
* Used for tags, filters, and reward indicators.
*
* @example
* ```html
* <ui-label>Prämie</ui-label>
* <ui-label [active]="isSelected">Selected</ui-label>
* ```
*
* @see https://www.figma.com/design/bK0IW6akzSjHxmMwQfVPRW/ISA-DESIGN-SYSTEM?node-id=2806-8052&m=dev
*/
@Component({
selector: 'ui-label',
imports: [CommonModule],
templateUrl: './label.component.html',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'[class]': '["ui-label", typeClass(), priorityClass()]',
'[class]': '["ui-label", activeClass()]',
'data-what': 'label',
'data-which': 'label',
'role': 'status',
},
})
export class LabelComponent {
/** The type of the label. */
type = input<Labeltype>(Labeltype.Tag);
/** Whether the label is active (hover/pressed state). */
active = input<boolean>(false);
/** A computed CSS class based on the current type. */
typeClass = computed(() => `ui-label__${this.type()}`);
/** The priority of the label. */
priority = input<LabelPriority>(LabelPriority.High);
/** A computed CSS class based on the current priority and typeClass. */
priorityClass = computed(
() => `${this.typeClass()}-priority-${this.priority()}`,
);
/** A computed CSS class for the active state of the label. */
activeClass = computed(() => (this.active() ? 'ui-label--active' : null));
}

View File

@@ -0,0 +1,102 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PrioLabelComponent } from './prio-label.component';
describe('PrioLabelComponent', () => {
let component: PrioLabelComponent;
let fixture: ComponentFixture<PrioLabelComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [PrioLabelComponent],
}).compileComponents();
fixture = TestBed.createComponent(PrioLabelComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
describe('Component Setup and Initialization', () => {
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have default priority as 1', () => {
expect(component.priority()).toBe(1);
});
it('should accept priority 2', () => {
fixture.componentRef.setInput('priority', 2);
fixture.detectChanges();
expect(component.priority()).toBe(2);
});
it('should have correct CSS class for priority 1', () => {
expect(component.priorityClass()).toBe('ui-prio-label--1');
});
it('should have correct CSS class for priority 2', () => {
fixture.componentRef.setInput('priority', 2);
fixture.detectChanges();
expect(component.priorityClass()).toBe('ui-prio-label--2');
});
});
describe('Template Rendering', () => {
it('should display content with priority 1 classes', () => {
const hostElement = fixture.debugElement.nativeElement;
expect(hostElement.classList.contains('ui-prio-label')).toBe(true);
expect(hostElement.classList.contains('ui-prio-label--1')).toBe(true);
});
it('should display content with priority 2 classes', () => {
fixture.componentRef.setInput('priority', 2);
fixture.detectChanges();
const hostElement = fixture.debugElement.nativeElement;
expect(hostElement.classList.contains('ui-prio-label')).toBe(true);
expect(hostElement.classList.contains('ui-prio-label--2')).toBe(true);
});
it('should update classes when priority changes', () => {
const hostElement = fixture.debugElement.nativeElement;
expect(hostElement.classList.contains('ui-prio-label--1')).toBe(true);
expect(hostElement.classList.contains('ui-prio-label--2')).toBe(false);
fixture.componentRef.setInput('priority', 2);
fixture.detectChanges();
expect(hostElement.classList.contains('ui-prio-label--1')).toBe(false);
expect(hostElement.classList.contains('ui-prio-label--2')).toBe(true);
});
});
describe('Component Structure', () => {
it('should have proper host class binding', () => {
const hostElement = fixture.debugElement.nativeElement;
expect(hostElement.classList.contains('ui-prio-label')).toBe(true);
});
it('should have E2E testing attributes', () => {
const hostElement = fixture.debugElement.nativeElement;
expect(hostElement.getAttribute('data-what')).toBe('prio-label');
expect(hostElement.getAttribute('data-which')).toBe('priority-1');
});
it('should have correct data-which for different priorities', () => {
const hostElement = fixture.debugElement.nativeElement;
expect(hostElement.getAttribute('data-which')).toBe('priority-1');
fixture.componentRef.setInput('priority', 2);
fixture.detectChanges();
expect(hostElement.getAttribute('data-which')).toBe('priority-2');
});
it('should have accessibility role', () => {
const hostElement = fixture.debugElement.nativeElement;
expect(hostElement.getAttribute('role')).toBe('status');
});
});
});

View File

@@ -0,0 +1,39 @@
import {
ChangeDetectionStrategy,
Component,
computed,
input,
ViewEncapsulation,
} from '@angular/core';
/**
* Priority label component for displaying priority indicators.
* Supports priority levels 1 (high) and 2 (low) with custom text content.
*
* @example
* ```html
* <ui-prio-label [priority]="1">Pflicht</ui-prio-label>
* <ui-prio-label [priority]="2">Prio 2</ui-prio-label>
* ```
*
* @see https://www.figma.com/design/bK0IW6akzSjHxmMwQfVPRW/ISA-DESIGN-SYSTEM?node-id=682-2836&m=dev
*/
@Component({
selector: 'ui-prio-label',
template: '<ng-content></ng-content>',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'[class]': '["ui-prio-label", priorityClass()]',
'data-what': 'prio-label',
'[attr.data-which]': '"priority-" + priority()',
'role': 'status',
},
})
export class PrioLabelComponent {
/** The priority level of the label (1 = high, 2 = low). */
priority = input<1 | 2>(1);
/** A computed CSS class based on the current priority. */
priorityClass = computed(() => `ui-prio-label--${this.priority()}`);
}

View File

@@ -1,14 +0,0 @@
export const Labeltype = {
Tag: 'tag',
Notice: 'notice',
} as const;
export type Labeltype = (typeof Labeltype)[keyof typeof Labeltype];
export const LabelPriority = {
High: 'high',
Medium: 'medium',
Low: 'low',
} as const;
export type LabelPriority = (typeof LabelPriority)[keyof typeof LabelPriority];

83
libs/ui/notice/README.md Normal file
View File

@@ -0,0 +1,83 @@
# @isa/ui/notice
A notice component for displaying prominent notifications and alerts with configurable priority levels.
## Installation
```typescript
import { NoticeComponent, NoticePriority } from '@isa/ui/notice';
```
## Component
### NoticeComponent
**Figma:** [ISA Design System - Notice](https://www.figma.com/design/bK0IW6akzSjHxmMwQfVPRW/ISA-DESIGN-SYSTEM?node-id=2551-4407&m=dev)
## Priority Levels
| Priority | Description | Background |
| -------- | ---------------- | ---------------- |
| `high` | Most prominent | Secondary color |
| `medium` | Moderate | Neutral color |
| `low` | Subtle (no fill) | Transparent |
## Usage
```html
<!-- High priority (default) -->
<ui-notice>Action Required</ui-notice>
<!-- Medium priority -->
<ui-notice [priority]="NoticePriority.Medium">Secondary message</ui-notice>
<!-- Low priority -->
<ui-notice priority="low">Info message</ui-notice>
```
### Component Example
```typescript
import { Component } from '@angular/core';
import { NoticeComponent, NoticePriority } from '@isa/ui/notice';
@Component({
selector: 'app-alert',
template: `
<ui-notice [priority]="NoticePriority.High">Action Required</ui-notice>
<ui-notice [priority]="NoticePriority.Medium">Limited Stock</ui-notice>
`,
imports: [NoticeComponent],
})
export class AlertComponent {
NoticePriority = NoticePriority;
}
```
## API
| Input | Type | Default | Description |
| ---------- | ---------------- | -------------------- | --------------------------------- |
| `priority` | `NoticePriority` | `NoticePriority.High`| Visual priority level |
## CSS Classes
- `.ui-notice` - Base class
- `.ui-notice--high` - High priority (secondary background)
- `.ui-notice--medium` - Medium priority (neutral background)
- `.ui-notice--low` - Low priority (transparent)
## Accessibility
- `role="status"` - Indicates status information
- E2E testing attributes (`data-what`, `data-which`)
## E2E Testing
```typescript
// Select all notices
page.locator('[data-what="notice"]');
// Select specific priority
page.locator('[data-what="notice"][data-which="priority-high"]');
```

View File

@@ -0,0 +1,34 @@
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../../eslint.config.js');
module.exports = [
...baseConfig,
...nx.configs['flat/angular'],
...nx.configs['flat/angular-template'],
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'ui',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'ui',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {},
},
];

View File

@@ -0,0 +1,20 @@
{
"name": "ui-notice",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/ui/notice/src",
"prefix": "ui",
"projectType": "library",
"tags": ["scope:ui", "type:ui"],
"targets": {
"test": {
"executor": "@nx/vite:test",
"outputs": ["{options.reportsDirectory}"],
"options": {
"reportsDirectory": "../../../coverage/libs/ui/notice"
}
},
"lint": {
"executor": "@nx/eslint:lint"
}
}
}

View File

@@ -0,0 +1,2 @@
export * from './lib/notice/notice.component';
export * from './lib/notice/types';

View File

@@ -0,0 +1,16 @@
.ui-notice {
@apply inline-flex flex-col items-start;
@apply p-2 min-w-48 rounded-lg isa-text-body-2-bold text-isa-neutral-900;
}
.ui-notice--high {
@apply bg-isa-secondary-100;
}
.ui-notice--medium {
@apply bg-isa-neutral-100;
}
.ui-notice--low {
@apply bg-transparent;
}

View File

@@ -0,0 +1 @@
<ng-content></ng-content>

View File

@@ -0,0 +1,128 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NoticeComponent } from './notice.component';
import { NoticePriority } from './types';
describe('NoticeComponent', () => {
let component: NoticeComponent;
let fixture: ComponentFixture<NoticeComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NoticeComponent],
}).compileComponents();
fixture = TestBed.createComponent(NoticeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
describe('Component Setup and Initialization', () => {
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have default priority as high', () => {
expect(component.priority()).toBe(NoticePriority.High);
});
it('should accept medium priority', () => {
fixture.componentRef.setInput('priority', NoticePriority.Medium);
fixture.detectChanges();
expect(component.priority()).toBe(NoticePriority.Medium);
});
it('should accept low priority', () => {
fixture.componentRef.setInput('priority', NoticePriority.Low);
fixture.detectChanges();
expect(component.priority()).toBe(NoticePriority.Low);
});
it('should have correct CSS class for high priority', () => {
expect(component.priorityClass()).toBe('ui-notice--high');
});
it('should have correct CSS class for medium priority', () => {
fixture.componentRef.setInput('priority', NoticePriority.Medium);
fixture.detectChanges();
expect(component.priorityClass()).toBe('ui-notice--medium');
});
it('should have correct CSS class for low priority', () => {
fixture.componentRef.setInput('priority', NoticePriority.Low);
fixture.detectChanges();
expect(component.priorityClass()).toBe('ui-notice--low');
});
});
describe('Template Rendering', () => {
it('should display content with high priority classes', () => {
const hostElement = fixture.debugElement.nativeElement;
expect(hostElement.classList.contains('ui-notice')).toBe(true);
expect(hostElement.classList.contains('ui-notice--high')).toBe(true);
});
it('should display content with medium priority classes', () => {
fixture.componentRef.setInput('priority', NoticePriority.Medium);
fixture.detectChanges();
const hostElement = fixture.debugElement.nativeElement;
expect(hostElement.classList.contains('ui-notice')).toBe(true);
expect(hostElement.classList.contains('ui-notice--medium')).toBe(true);
});
it('should display content with low priority classes', () => {
fixture.componentRef.setInput('priority', NoticePriority.Low);
fixture.detectChanges();
const hostElement = fixture.debugElement.nativeElement;
expect(hostElement.classList.contains('ui-notice')).toBe(true);
expect(hostElement.classList.contains('ui-notice--low')).toBe(true);
});
it('should update classes when priority changes', () => {
const hostElement = fixture.debugElement.nativeElement;
expect(hostElement.classList.contains('ui-notice--high')).toBe(true);
expect(hostElement.classList.contains('ui-notice--medium')).toBe(false);
fixture.componentRef.setInput('priority', NoticePriority.Medium);
fixture.detectChanges();
expect(hostElement.classList.contains('ui-notice--high')).toBe(false);
expect(hostElement.classList.contains('ui-notice--medium')).toBe(true);
});
});
describe('Component Structure', () => {
it('should have proper host class binding', () => {
const hostElement = fixture.debugElement.nativeElement;
expect(hostElement.classList.contains('ui-notice')).toBe(true);
});
it('should have E2E testing attributes', () => {
const hostElement = fixture.debugElement.nativeElement;
expect(hostElement.getAttribute('data-what')).toBe('notice');
expect(hostElement.getAttribute('data-which')).toBe('priority-high');
});
it('should have correct data-which for different priorities', () => {
const hostElement = fixture.debugElement.nativeElement;
expect(hostElement.getAttribute('data-which')).toBe('priority-high');
fixture.componentRef.setInput('priority', NoticePriority.Medium);
fixture.detectChanges();
expect(hostElement.getAttribute('data-which')).toBe('priority-medium');
fixture.componentRef.setInput('priority', NoticePriority.Low);
fixture.detectChanges();
expect(hostElement.getAttribute('data-which')).toBe('priority-low');
});
it('should have accessibility role', () => {
const hostElement = fixture.debugElement.nativeElement;
expect(hostElement.getAttribute('role')).toBe('status');
});
});
});

View File

@@ -0,0 +1,41 @@
import {
ChangeDetectionStrategy,
Component,
computed,
input,
ViewEncapsulation,
} from '@angular/core';
import { NoticePriority } from './types';
/**
* Notice component for displaying prominent notifications and alerts.
* Supports high, medium, and low priority variants.
*
* @example
* ```html
* <ui-notice>Important message</ui-notice>
* <ui-notice [priority]="NoticePriority.Medium">Secondary message</ui-notice>
* <ui-notice [priority]="NoticePriority.Low">Info message</ui-notice>
* ```
*
* @see https://www.figma.com/design/bK0IW6akzSjHxmMwQfVPRW/ISA-DESIGN-SYSTEM?node-id=2551-4407&m=dev
*/
@Component({
selector: 'ui-notice',
template: '<ng-content></ng-content>',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'[class]': '["ui-notice", priorityClass()]',
'data-what': 'notice',
'[attr.data-which]': '"priority-" + priority()',
'role': 'status',
},
})
export class NoticeComponent {
/** The priority level of the notice (high, medium, low). */
priority = input<NoticePriority>(NoticePriority.High);
/** A computed CSS class based on the current priority. */
priorityClass = computed(() => `ui-notice--${this.priority()}`);
}

View File

@@ -0,0 +1,7 @@
export const NoticePriority = {
High: 'high',
Medium: 'medium',
Low: 'low',
} as const;
export type NoticePriority = (typeof NoticePriority)[keyof typeof NoticePriority];

View File

@@ -0,0 +1 @@
@use "lib/notice/notice";

View File

@@ -0,0 +1,13 @@
import '@angular/compiler';
import '@analogjs/vitest-angular/setup-zone';
import {
BrowserTestingModule,
platformBrowserTesting,
} from '@angular/platform-browser/testing';
import { getTestBed } from '@angular/core/testing';
getTestBed().initTestEnvironment(
BrowserTestingModule,
platformBrowserTesting(),
);

View File

@@ -0,0 +1,30 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"importHelpers": true,
"moduleResolution": "bundler",
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"module": "preserve"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"typeCheckHostBindings": true,
"strictTemplates": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@@ -0,0 +1,27 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": []
},
"exclude": [
"src/**/*.spec.ts",
"src/test-setup.ts",
"jest.config.ts",
"src/**/*.test.ts",
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx"
],
"include": ["src/**/*.ts"]
}

View File

@@ -0,0 +1,29 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest"
]
},
"include": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
],
"files": ["src/test-setup.ts"]
}

View File

@@ -0,0 +1,29 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import angular from '@analogjs/vite-plugin-angular';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
export default
// @ts-expect-error - Vitest reporter tuple types have complex inference issues
defineConfig(() => ({
root: __dirname,
cacheDir: '../../../node_modules/.vite/libs/ui/notice',
plugins: [angular(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
test: {
watch: false,
globals: true,
environment: 'jsdom',
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
setupFiles: ['src/test-setup.ts'],
reporters: [
'default',
['junit', { outputFile: '../../../testresults/junit-ui-notice.xml' }],
],
coverage: {
reportsDirectory: '../../../coverage/libs/ui/notice',
provider: 'v8' as const,
reporter: ['text', 'cobertura'],
},
},
}));

View File

@@ -153,6 +153,7 @@
"@isa/ui/label": ["libs/ui/label/src/index.ts"],
"@isa/ui/layout": ["libs/ui/layout/src/index.ts"],
"@isa/ui/menu": ["libs/ui/menu/src/index.ts"],
"@isa/ui/notice": ["libs/ui/notice/src/index.ts"],
"@isa/ui/progress-bar": ["libs/ui/progress-bar/src/index.ts"],
"@isa/ui/search-bar": ["libs/ui/search-bar/src/index.ts"],
"@isa/ui/skeleton-loader": ["libs/ui/skeleton-loader/src/index.ts"],