Merge tag '4.0' into develop

Finish Release 4.0 4.0
This commit is contained in:
Lorenz Hilpert
2025-07-23 17:02:32 +02:00
10 changed files with 269 additions and 230 deletions

View File

@@ -0,0 +1,8 @@
export * from './errors';
export * from './guards';
export * from './models';
export * from './operators';
export * from './questions';
export * from './schemas';
export * from './services';
export * from './stores';

View File

@@ -116,7 +116,7 @@ export class ReturnDetailsService {
* Validates that the email parameter is a properly formatted email address.
*/
static FetchReceiptsEmailParamsSchema = z.object({
email: z.string().email(),
email: z.string(),
});
/**

View File

@@ -22,7 +22,7 @@
></oms-feature-return-details-static>
@if (customerReceiptsResource.isLoading()) {
<ui-progress-bar class="w-full" mode="indeterminate"></ui-progress-bar>
} @else {
} @else if (!customerReceiptsResource.error()) {
@for (receipt of customerReceiptsResource.value(); track receipt.id) {
@if (r.id !== receipt.id) {
<oms-feature-return-details-lazy

View File

@@ -1,151 +1,157 @@
import {
ChangeDetectionStrategy,
Component,
computed,
inject,
resource,
} from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
import { z } from 'zod';
import { NgIconComponent, provideIcons } from '@ng-icons/core';
import { isaActionChevronLeft } from '@isa/icons';
import { ButtonComponent } from '@isa/ui/buttons';
import { injectActivatedTabId } from '@isa/core/tabs';
import { Location } from '@angular/common';
import { ExpandableDirectives } from '@isa/ui/expandable';
import { ProgressBarComponent } from '@isa/ui/progress-bar';
import {
ReturnDetailsService,
ReturnProcessStore,
ReturnDetailsStore,
} from '@isa/oms/data-access';
import { ReturnDetailsStaticComponent } from './return-details-static/return-details-static.component';
import { ReturnDetailsLazyComponent } from './return-details-lazy/return-details-lazy.component';
import { logger } from '@isa/core/logging';
import { groupBy } from 'lodash';
@Component({
selector: 'oms-feature-return-details',
templateUrl: './return-details.component.html',
styleUrls: ['./return-details.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
ReturnDetailsStaticComponent,
ReturnDetailsLazyComponent,
NgIconComponent,
ButtonComponent,
ExpandableDirectives,
ProgressBarComponent,
],
providers: [provideIcons({ isaActionChevronLeft }), ReturnDetailsStore],
})
export class ReturnDetailsComponent {
#logger = logger(() => ({
component: 'ReturnDetailsComponent',
itemId: this.receiptId(),
processId: this.processId(),
params: this.params(),
}));
#store = inject(ReturnDetailsStore);
#returnDetailsService = inject(ReturnDetailsService);
#returnProcessStore = inject(ReturnProcessStore);
private processId = injectActivatedTabId();
private _router = inject(Router);
private _activatedRoute = inject(ActivatedRoute);
location = inject(Location);
params = toSignal(this._activatedRoute.params);
receiptId = computed<number>(() => {
const params = this.params();
if (params) {
return z.coerce.number().parse(params['receiptId']);
}
throw new Error('No receiptId found in route params');
});
receiptResource = this.#store.receiptResource(this.receiptId);
customerReceiptsResource = resource({
params: this.receiptResource.value,
loader: async ({ params, abortSignal }) => {
const email = params.buyer?.communicationDetails?.email;
if (!email) {
return [];
}
return await this.#returnDetailsService.fetchReceiptsByEmail(
{ email },
abortSignal,
);
},
});
canStartProcess = computed(() => {
return (
this.#store.selectedItemIds().length > 0 && this.processId() !== undefined
);
});
startProcess() {
if (!this.canStartProcess()) {
this.#logger.warn(
'Cannot start process: No items selected or no process ID',
);
return;
}
const processId = this.processId();
const selectedItems = this.#store.selectedItems();
const selectedQuantites = this.#store.selectedQuantityMap();
const selectedProductCategories = this.#store.itemCategoryMap();
this.#logger.info('Starting return process', () => ({
processId: processId,
selectedItems: selectedItems.map((item) => item.id),
}));
if (!selectedItems.length || !processId) {
return;
}
const itemsGrouptByReceiptId = groupBy(
selectedItems,
(item) => item.receipt?.id,
);
const receipts = this.#store.receiptsEntityMap();
const returns = Object.entries(itemsGrouptByReceiptId).map(
([receiptId, items]) => ({
receipt: receipts[Number(receiptId)],
items: items.map((item) => {
const receiptItem = item;
return {
receiptItem,
quantity: selectedQuantites[receiptItem.id],
category: selectedProductCategories[receiptItem.id],
};
}),
}),
);
this.#logger.info('Starting return process with returns', () => ({
processId,
returns,
}));
this.#returnProcessStore.startProcess({
processId,
returns,
});
this._router.navigate(['../../', 'process'], {
relativeTo: this._activatedRoute,
});
}
}
import {
ChangeDetectionStrategy,
Component,
computed,
inject,
resource,
} from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
import { z } from 'zod';
import { NgIconComponent, provideIcons } from '@ng-icons/core';
import { isaActionChevronLeft } from '@isa/icons';
import { ButtonComponent } from '@isa/ui/buttons';
import { injectActivatedTabId } from '@isa/core/tabs';
import { Location } from '@angular/common';
import { ExpandableDirectives } from '@isa/ui/expandable';
import { ProgressBarComponent } from '@isa/ui/progress-bar';
import {
ReturnDetailsService,
ReturnProcessStore,
ReturnDetailsStore,
} from '@isa/oms/data-access';
import { ReturnDetailsStaticComponent } from './return-details-static/return-details-static.component';
import { ReturnDetailsLazyComponent } from './return-details-lazy/return-details-lazy.component';
import { logger } from '@isa/core/logging';
import { groupBy } from 'lodash';
@Component({
selector: 'oms-feature-return-details',
templateUrl: './return-details.component.html',
styleUrls: ['./return-details.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
ReturnDetailsStaticComponent,
ReturnDetailsLazyComponent,
NgIconComponent,
ButtonComponent,
ExpandableDirectives,
ProgressBarComponent,
],
providers: [provideIcons({ isaActionChevronLeft }), ReturnDetailsStore],
})
export class ReturnDetailsComponent {
#logger = logger(() => ({
component: 'ReturnDetailsComponent',
itemId: this.receiptId(),
processId: this.processId(),
params: this.params(),
}));
#store = inject(ReturnDetailsStore);
#returnDetailsService = inject(ReturnDetailsService);
#returnProcessStore = inject(ReturnProcessStore);
private processId = injectActivatedTabId();
private _router = inject(Router);
private _activatedRoute = inject(ActivatedRoute);
location = inject(Location);
params = toSignal(this._activatedRoute.params);
receiptId = computed<number>(() => {
const params = this.params();
if (params) {
return z.coerce.number().parse(params['receiptId']);
}
throw new Error('No receiptId found in route params');
});
receiptResource = this.#store.receiptResource(this.receiptId);
customerReceiptsResource = resource({
params: this.receiptResource.value,
loader: async ({ params, abortSignal }) => {
const email = params.buyer?.communicationDetails?.email;
if (!email) {
return [];
}
try {
return await this.#returnDetailsService.fetchReceiptsByEmail(
{ email },
abortSignal,
);
} catch (error) {
this.#logger.error('Failed to fetch customer receipts', error);
return [];
}
},
});
canStartProcess = computed(() => {
return (
this.#store.selectedItemIds().length > 0 && this.processId() !== undefined
);
});
startProcess() {
if (!this.canStartProcess()) {
this.#logger.warn(
'Cannot start process: No items selected or no process ID',
);
return;
}
const processId = this.processId();
const selectedItems = this.#store.selectedItems();
const selectedQuantites = this.#store.selectedQuantityMap();
const selectedProductCategories = this.#store.itemCategoryMap();
this.#logger.info('Starting return process', () => ({
processId: processId,
selectedItems: selectedItems.map((item) => item.id),
}));
if (!selectedItems.length || !processId) {
return;
}
const itemsGrouptByReceiptId = groupBy(
selectedItems,
(item) => item.receipt?.id,
);
const receipts = this.#store.receiptsEntityMap();
const returns = Object.entries(itemsGrouptByReceiptId).map(
([receiptId, items]) => ({
receipt: receipts[Number(receiptId)],
items: items.map((item) => {
const receiptItem = item;
return {
receiptItem,
quantity: selectedQuantites[receiptItem.id],
category: selectedProductCategories[receiptItem.id],
};
}),
}),
);
this.#logger.info('Starting return process with returns', () => ({
processId,
returns,
}));
this.#returnProcessStore.startProcess({
processId,
returns,
});
this._router.navigate(['../../', 'process'], {
relativeTo: this._activatedRoute,
});
}
}