diff --git a/libs/oms/data-access/src/lib/models/return-process-question-key.ts b/libs/oms/data-access/src/lib/models/return-process-question-key.ts
index c95cdb3e7..1110c0d16 100644
--- a/libs/oms/data-access/src/lib/models/return-process-question-key.ts
+++ b/libs/oms/data-access/src/lib/models/return-process-question-key.ts
@@ -6,6 +6,7 @@ export const ReturnProcessQuestionKey = {
DisplayCondition: 'display_condition',
DevicePower: 'device_power',
PackageComplete: 'package_complete',
+ PackageIncomplete: 'package_incomplete',
CaseCondition: 'case_condition',
UsbPort: 'usb_port',
} as const;
diff --git a/libs/oms/data-access/src/lib/models/return-process-question-type.ts b/libs/oms/data-access/src/lib/models/return-process-question-type.ts
index c740cd90e..5342cc971 100644
--- a/libs/oms/data-access/src/lib/models/return-process-question-type.ts
+++ b/libs/oms/data-access/src/lib/models/return-process-question-type.ts
@@ -1,6 +1,7 @@
export const ReturnProcessQuestionType = {
Select: 'select',
Product: 'product',
+ Checklist: 'checklist',
} as const;
export type ReturnProcessQuestionType =
diff --git a/libs/oms/data-access/src/lib/models/return-process-question.ts b/libs/oms/data-access/src/lib/models/return-process-question.ts
index 6ce49169e..b4603c27f 100644
--- a/libs/oms/data-access/src/lib/models/return-process-question.ts
+++ b/libs/oms/data-access/src/lib/models/return-process-question.ts
@@ -7,20 +7,44 @@ export interface ReturnProcessQuestionBase {
type: ReturnProcessQuestionType;
}
+export interface ReturnProcessQuestionOptions {
+ label: string;
+ value: string;
+}
+
+export interface ReturnProcessChecklistQuestion
+ extends ReturnProcessQuestionBase {
+ type: typeof ReturnProcessQuestionType.Checklist;
+ options: ReturnProcessChecklistQuestionOptions[];
+ other?: {
+ label: string;
+ value?: string;
+ };
+ nextQuestion?: ReturnProcessQuestionKey;
+}
+
+export type ReturnProcessChecklistQuestionOptions =
+ ReturnProcessQuestionOptions;
+
export interface ReturnProcessSelectQuestion extends ReturnProcessQuestionBase {
type: typeof ReturnProcessQuestionType.Select;
options: ReturnProcessQuestionSelectOptions[];
}
-export interface ReturnProcessQuestionSelectOptions {
+export interface ReturnProcessQuestionSelectOptions
+ extends ReturnProcessQuestionOptions {
label: string;
value: string;
nextQuestion?: ReturnProcessQuestionKey;
}
-export interface ReturnProcessProductQuestion extends ReturnProcessQuestionBase {
+export interface ReturnProcessProductQuestion
+ extends ReturnProcessQuestionBase {
type: typeof ReturnProcessQuestionType.Product;
nextQuestion?: ReturnProcessQuestionKey;
}
-export type ReturnProcessQuestion = ReturnProcessSelectQuestion | ReturnProcessProductQuestion;
+export type ReturnProcessQuestion =
+ | ReturnProcessSelectQuestion
+ | ReturnProcessProductQuestion
+ | ReturnProcessChecklistQuestion;
diff --git a/libs/oms/data-access/src/lib/questions/constants.ts b/libs/oms/data-access/src/lib/questions/constants.ts
index a95565290..226efd640 100644
--- a/libs/oms/data-access/src/lib/questions/constants.ts
+++ b/libs/oms/data-access/src/lib/questions/constants.ts
@@ -33,3 +33,12 @@ export const enum ItemDefectiveValue {
Yes = 'yes',
No = 'no',
}
+
+export const enum PackageIncompleteValue {
+ // Karton/Umverpackung
+ Ovp = 'ovp',
+ // Ladekabel
+ CharchingCable = 'charching_cable',
+ // Quickstart Guide
+ QuickstartGuide = 'quickstart_guide',
+}
diff --git a/libs/oms/data-access/src/lib/questions/tolino.spec.ts b/libs/oms/data-access/src/lib/questions/tolino.spec.ts
index 3cdd638b0..075f1a22f 100644
--- a/libs/oms/data-access/src/lib/questions/tolino.spec.ts
+++ b/libs/oms/data-access/src/lib/questions/tolino.spec.ts
@@ -12,39 +12,6 @@ describe('Tolino Return Process', () => {
const result = validateTolinoQuestions(answers);
expect(result).toEqual({ state: EligibleForReturnState.Eligible });
});
-
- it('should return Eligible when damaged and has significant defects', () => {
- const answers = {
- [ReturnProcessQuestionKey.ItemCondition]: 'damaged',
- [ReturnProcessQuestionKey.DevicePower]: 'yes',
- };
- const result = validateTolinoQuestions(answers);
- expect(result).toEqual({ state: EligibleForReturnState.Eligible });
- });
-
- it('should return NotEligible when damaged but no significant defects', () => {
- const answers = {
- [ReturnProcessQuestionKey.ItemCondition]: 'damaged',
- [ReturnProcessQuestionKey.DevicePower]: 'no',
- [ReturnProcessQuestionKey.PackageComplete]: 'no',
- [ReturnProcessQuestionKey.CaseCondition]: 'no',
- [ReturnProcessQuestionKey.UsbPort]: 'no',
- };
- const result = validateTolinoQuestions(answers);
- expect(result).toEqual({
- state: EligibleForReturnState.NotEligible,
- reason: 'No significant defects found',
- });
- });
-
- it('should return NotEligible for invalid answers', () => {
- const answers = {};
- const result = validateTolinoQuestions(answers);
- expect(result).toEqual({
- state: EligibleForReturnState.NotEligible,
- reason: 'Invalid answers',
- });
- });
});
describe('Registry mappings', () => {
diff --git a/libs/oms/data-access/src/lib/questions/tolino.ts b/libs/oms/data-access/src/lib/questions/tolino.ts
index 036de7982..7175acab8 100644
--- a/libs/oms/data-access/src/lib/questions/tolino.ts
+++ b/libs/oms/data-access/src/lib/questions/tolino.ts
@@ -9,6 +9,7 @@ import {
import {
ItemConditionValue,
ItemDefectiveValue,
+ PackageIncompleteValue,
ReturnReasonValue,
} from './constants';
@@ -60,7 +61,7 @@ export const tolinoQuestions: ReturnProcessQuestion[] = [
{
label: 'Ja',
value: ItemDefectiveValue.No,
- nextQuestion: ReturnProcessQuestionKey.ReturnReason,
+ nextQuestion: ReturnProcessQuestionKey.PackageIncomplete,
},
{
label: 'Nein',
@@ -69,6 +70,29 @@ export const tolinoQuestions: ReturnProcessQuestion[] = [
},
],
},
+ {
+ key: ReturnProcessQuestionKey.PackageIncomplete,
+ description: 'Was fehlt?',
+ type: ReturnProcessQuestionType.Checklist,
+ options: [
+ {
+ label: 'Karton/Umverpackung',
+ value: PackageIncompleteValue.Ovp,
+ },
+ {
+ label: 'Ladekabel',
+ value: PackageIncompleteValue.CharchingCable,
+ },
+ {
+ label: 'Quickstart Guide',
+ value: PackageIncompleteValue.QuickstartGuide,
+ },
+ ],
+ other: {
+ label: 'Sonstiges',
+ },
+ nextQuestion: ReturnProcessQuestionKey.CaseCondition,
+ },
{
key: ReturnProcessQuestionKey.CaseCondition,
description: 'Hat das Gehäuse Mängel oder ist zerkratzt?',
@@ -137,45 +161,11 @@ export const tolinoQuestions: ReturnProcessQuestion[] = [
/**
* Validates the answers for the Tolino return process and determines return eligibility.
- *
- * The validation logic follows these rules:
- * 1. If item is in original packaging, it's always eligible for return
- * 2. If item is damaged, it requires at least one defect to be eligible
- * 3. If no valid condition is provided, the item is not eligible
- *
* @param {ReturnProcessAnswers} answers - A record of answers keyed by question keys
* @returns {EligibleForReturn} Object containing eligibility state and reason if applicable
*/
export function validateTolinoQuestions(
answers: ReturnProcessAnswers,
): EligibleForReturn {
- if (
- answers[ReturnProcessQuestionKey.ItemCondition] ===
- ItemConditionValue.OriginalPackaging
- ) {
- return { state: EligibleForReturnState.Eligible };
- } else if (
- answers[ReturnProcessQuestionKey.ItemCondition] ===
- ItemConditionValue.Damaged
- ) {
- if (
- answers[ReturnProcessQuestionKey.DevicePower] ===
- ItemDefectiveValue.Yes ||
- answers[ReturnProcessQuestionKey.PackageComplete] ===
- ItemDefectiveValue.Yes ||
- answers[ReturnProcessQuestionKey.CaseCondition] ===
- ItemDefectiveValue.Yes ||
- answers[ReturnProcessQuestionKey.UsbPort] === ItemDefectiveValue.Yes
- ) {
- return { state: EligibleForReturnState.Eligible };
- }
- return {
- state: EligibleForReturnState.NotEligible,
- reason: 'No significant defects found',
- };
- }
- return {
- state: EligibleForReturnState.NotEligible,
- reason: 'Invalid answers',
- };
+ return { state: EligibleForReturnState.Eligible };
}
diff --git a/libs/oms/data-access/src/lib/return-process.service.ts b/libs/oms/data-access/src/lib/return-process.service.ts
index 0b2412e39..0908b5e08 100644
--- a/libs/oms/data-access/src/lib/return-process.service.ts
+++ b/libs/oms/data-access/src/lib/return-process.service.ts
@@ -8,6 +8,7 @@ import {
} from './models';
import { CategoryQuestions, CategoryQuestionValidators } from './questions';
import { KeyValue } from '@angular/common';
+import { ReturnProcessChecklistAnswerSchema } from './schemas';
/**
* Service responsible for managing the return process workflow.
@@ -57,7 +58,9 @@ export class ReturnProcessService {
process.productCategory || process.receiptItem.features?.['category'];
if (category) {
- return CategoryQuestionValidators[category];
+ return CategoryQuestionValidators[
+ category as keyof typeof CategoryQuestionValidators
+ ];
}
return undefined;
}
@@ -102,6 +105,16 @@ export class ReturnProcessService {
questionKey = process.answers[question.key]
? question.nextQuestion
: undefined;
+ } else if (question.type === ReturnProcessQuestionType.Checklist) {
+ const answer = ReturnProcessChecklistAnswerSchema.parse(
+ process.answers[question.key],
+ );
+
+ if (answer.options?.length || answer.other) {
+ questionKey = question.nextQuestion;
+ } else {
+ questionKey = undefined;
+ }
} else {
console.error('Unknown question type', question);
break;
diff --git a/libs/oms/data-access/src/lib/schemas/fetch-return-details.ts b/libs/oms/data-access/src/lib/schemas/fetch-return-details.schema.ts
similarity index 100%
rename from libs/oms/data-access/src/lib/schemas/fetch-return-details.ts
rename to libs/oms/data-access/src/lib/schemas/fetch-return-details.schema.ts
diff --git a/libs/oms/data-access/src/lib/schemas/index.ts b/libs/oms/data-access/src/lib/schemas/index.ts
index 31b442477..cac1ac094 100644
--- a/libs/oms/data-access/src/lib/schemas/index.ts
+++ b/libs/oms/data-access/src/lib/schemas/index.ts
@@ -1,2 +1,3 @@
-export * from './fetch-return-details';
+export * from './fetch-return-details.schema';
export * from './query-token.schema';
+export * from './return-process-question-answer.schema';
diff --git a/libs/oms/data-access/src/lib/schemas/return-process-question-answer.schema.ts b/libs/oms/data-access/src/lib/schemas/return-process-question-answer.schema.ts
new file mode 100644
index 000000000..c64ef0e33
--- /dev/null
+++ b/libs/oms/data-access/src/lib/schemas/return-process-question-answer.schema.ts
@@ -0,0 +1,10 @@
+import { z } from 'zod';
+
+export const ReturnProcessChecklistAnswerSchema = z.object({
+ options: z.array(z.string()).default([]),
+ other: z.string().optional(),
+});
+
+export type ReturnProcessChecklistAnswer = z.infer<
+ typeof ReturnProcessChecklistAnswerSchema
+>;
diff --git a/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-checklist-question/return-process-checklist-question.component.html b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-checklist-question/return-process-checklist-question.component.html
new file mode 100644
index 000000000..d30db92e5
--- /dev/null
+++ b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-checklist-question/return-process-checklist-question.component.html
@@ -0,0 +1,24 @@
+@let q = question();
+@if (q) {
+
+ {{ q.description }}
+
+
+
+ @for (option of q.options; track option.label) {
+
+ }
+
+
+ @if (q.other) {
+
+
+
+
+ }
+}
diff --git a/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-checklist-question/return-process-checklist-question.component.scss b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-checklist-question/return-process-checklist-question.component.scss
new file mode 100644
index 000000000..461bfa7d0
--- /dev/null
+++ b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-checklist-question/return-process-checklist-question.component.scss
@@ -0,0 +1,3 @@
+:host {
+ @apply grid grid-cols-2 gap-6;
+}
diff --git a/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-checklist-question/return-process-checklist-question.component.ts b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-checklist-question/return-process-checklist-question.component.ts
new file mode 100644
index 000000000..083a0f03b
--- /dev/null
+++ b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-checklist-question/return-process-checklist-question.component.ts
@@ -0,0 +1,88 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ computed,
+ effect,
+ inject,
+ input,
+ linkedSignal,
+ untracked,
+} from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import {
+ ReturnProcessChecklistAnswer,
+ ReturnProcessChecklistAnswerSchema,
+ ReturnProcessChecklistQuestion,
+ ReturnProcessStore,
+} from '@isa/oms/data-access';
+import {
+ ChipOptionComponent,
+ ChipsComponent,
+ CheckboxComponent,
+ ChecklistComponent,
+ CheckboxLabelDirective,
+ ChecklistValueDirective,
+ TextareaComponent,
+} from '@isa/ui/input-controls';
+import { isEqual } from 'lodash';
+import { z } from 'zod';
+
+@Component({
+ selector: 'oms-feature-return-process-checklist-question',
+ templateUrl: './return-process-checklist-question.component.html',
+ styleUrls: ['./return-process-checklist-question.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ standalone: true,
+ imports: [
+ FormsModule,
+ ChipsComponent,
+ ChipOptionComponent,
+ CheckboxComponent,
+ ChecklistComponent,
+ ChecklistValueDirective,
+ CheckboxLabelDirective,
+ TextareaComponent,
+ ],
+})
+export class ReturnProcessChecklistQuestionComponent {
+ #returnProcessStore = inject(ReturnProcessStore);
+
+ processId = input.required();
+
+ question = input.required();
+
+ currentAnswer = computed(() => {
+ const currentAnswersRaw =
+ this.#returnProcessStore.entityMap()[this.processId()]?.answers[
+ this.question().key
+ ];
+
+ if (!currentAnswersRaw) {
+ return ReturnProcessChecklistAnswerSchema.parse({});
+ }
+
+ return ReturnProcessChecklistAnswerSchema.parse(currentAnswersRaw);
+ });
+
+ options = linkedSignal(() => this.currentAnswer().options, {
+ equal: isEqual,
+ });
+
+ other = linkedSignal(() => this.currentAnswer().other, { equal: isEqual });
+
+ valueChangesEffect = effect(() => {
+ const options = this.options();
+ const other = this.other();
+ untracked(() => {
+ this.updateAnswer({ options, other });
+ });
+ });
+
+ updateAnswer(answer: ReturnProcessChecklistAnswer) {
+ this.#returnProcessStore.setAnswer(
+ this.processId(),
+ this.question().key,
+ answer,
+ );
+ }
+}
diff --git a/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-questions.component.html b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-questions.component.html
index dbbcd2f65..81dfd39e3 100644
--- a/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-questions.component.html
+++ b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-questions.component.html
@@ -20,18 +20,24 @@
@for (question of questions(); track question.key; let last = $last) {
@switch (question.type) {
- @case ('select') {
+ @case (ReturnProcessQuestionType.Select) {
}
- @case ('product') {
+ @case (ReturnProcessQuestionType.Product) {
}
+ @case (ReturnProcessQuestionType.Checklist) {
+
+ }
@default {
{{ question | json }}
}
diff --git a/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-questions.component.ts b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-questions.component.ts
index e3ef7cf7b..bce52b5fc 100644
--- a/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-questions.component.ts
+++ b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-questions.component.ts
@@ -9,11 +9,13 @@ import {
import {
ReturnProcess,
ReturnProcessQuestion,
+ ReturnProcessQuestionType,
ReturnProcessService,
ReturnProcessStore,
} from '@isa/oms/data-access';
import { ReturnProcessSelectQuestionComponent } from './return-process-select-question/return-process-select-question.component';
import { ReturnProcessProductQuestionComponent } from './return-process-product-question/return-process-product-question.component';
+import { ReturnProcessChecklistQuestionComponent } from './return-process-checklist-question/return-process-checklist-question.component';
import {
DropdownButtonComponent,
DropdownOptionComponent,
@@ -28,6 +30,7 @@ import {
JsonPipe,
ReturnProcessSelectQuestionComponent,
ReturnProcessProductQuestionComponent,
+ ReturnProcessChecklistQuestionComponent,
DropdownButtonComponent,
DropdownOptionComponent,
],
@@ -37,6 +40,8 @@ export class ReturnProcessQuestionsComponent {
// Renamed service to match naming conventions
#returnProcessService = inject(ReturnProcessService);
+ ReturnProcessQuestionType = ReturnProcessQuestionType;
+
returnProcessId = input.required
();
returnProcess = computed(() => {
diff --git a/libs/ui/input-controls/src/index.ts b/libs/ui/input-controls/src/index.ts
index 0c4c28456..40b22105d 100644
--- a/libs/ui/input-controls/src/index.ts
+++ b/libs/ui/input-controls/src/index.ts
@@ -1,7 +1,11 @@
+export * from './lib/checkbox/checkbox-label.directive';
export * from './lib/checkbox/checkbox.component';
+export * from './lib/checkbox/checklist-value.directive';
+export * from './lib/checkbox/checklist.component';
export * from './lib/core/input-control.directive';
export * from './lib/dropdown/dropdown.component';
export * from './lib/dropdown/dropdown.types';
export * from './lib/text-field/text-field.component';
+export * from './lib/text-field/textarea.component';
export * from './lib/chips/chips.component';
export * from './lib/chips/chip-option.component';
diff --git a/libs/ui/input-controls/src/input-controls.scss b/libs/ui/input-controls/src/input-controls.scss
index eed9852c2..10f11077d 100644
--- a/libs/ui/input-controls/src/input-controls.scss
+++ b/libs/ui/input-controls/src/input-controls.scss
@@ -1,4 +1,6 @@
-@use './lib/checkbox/checkbox';
-@use './lib/chips/chips';
-@use './lib/dropdown/dropdown';
-@use './lib/text-field/text-field.scss';
+@use "./lib/checkbox/checkbox";
+@use "./lib/checkbox/checklist";
+@use "./lib/chips/chips";
+@use "./lib/dropdown/dropdown";
+@use "./lib/text-field/text-field";
+@use "./lib/text-field/textarea";
diff --git a/libs/ui/input-controls/src/lib/checkbox/_checkbox.scss b/libs/ui/input-controls/src/lib/checkbox/_checkbox.scss
index ed78d464d..7b898a096 100644
--- a/libs/ui/input-controls/src/lib/checkbox/_checkbox.scss
+++ b/libs/ui/input-controls/src/lib/checkbox/_checkbox.scss
@@ -1,3 +1,11 @@
+.ui-checkbox-label {
+ @apply inline-flex items-center gap-4 text-isa-neutral-900 isa-text-body-2-regular;
+}
+
+.ui-checkbox-label:has(:checked) {
+ @apply isa-text-body-2-bold;
+}
+
.ui-checkbox.ui-checkbox__checkbox {
@apply relative inline-flex p-3 items-center justify-center rounded-lg bg-isa-white size-6 border border-solid border-isa-neutral-900;
font-size: 1.5rem;
diff --git a/libs/ui/input-controls/src/lib/checkbox/_checklist.scss b/libs/ui/input-controls/src/lib/checkbox/_checklist.scss
new file mode 100644
index 000000000..3a8940208
--- /dev/null
+++ b/libs/ui/input-controls/src/lib/checkbox/_checklist.scss
@@ -0,0 +1,3 @@
+.ui-checklist {
+ @apply inline-grid grid-flow-row gap-6;
+}
diff --git a/libs/ui/input-controls/src/lib/checkbox/checkbox-label.directive.ts b/libs/ui/input-controls/src/lib/checkbox/checkbox-label.directive.ts
new file mode 100644
index 000000000..37cc0e03a
--- /dev/null
+++ b/libs/ui/input-controls/src/lib/checkbox/checkbox-label.directive.ts
@@ -0,0 +1,9 @@
+import { Directive } from '@angular/core';
+
+@Directive({
+ selector: 'label[uiCheckboxLabel]',
+ host: {
+ class: 'ui-checkbox-label',
+ },
+})
+export class CheckboxLabelDirective {}
diff --git a/libs/ui/input-controls/src/lib/checkbox/checkbox.component.ts b/libs/ui/input-controls/src/lib/checkbox/checkbox.component.ts
index e8c03ee79..f5f408ea6 100644
--- a/libs/ui/input-controls/src/lib/checkbox/checkbox.component.ts
+++ b/libs/ui/input-controls/src/lib/checkbox/checkbox.component.ts
@@ -13,7 +13,8 @@ export const CheckboxAppearance = {
Checkbox: 'checkbox',
} as const;
-export type CheckboxAppearance = (typeof CheckboxAppearance)[keyof typeof CheckboxAppearance];
+export type CheckboxAppearance =
+ (typeof CheckboxAppearance)[keyof typeof CheckboxAppearance];
@Component({
selector: 'ui-checkbox',
diff --git a/libs/ui/input-controls/src/lib/checkbox/checklist-value.directive.ts b/libs/ui/input-controls/src/lib/checkbox/checklist-value.directive.ts
new file mode 100644
index 000000000..2425a7dca
--- /dev/null
+++ b/libs/ui/input-controls/src/lib/checkbox/checklist-value.directive.ts
@@ -0,0 +1,21 @@
+import { Directive, input, model } from '@angular/core';
+
+@Directive({
+ selector: '[uiChecklistValue]',
+ host: {
+ 'class': 'ui-checklist-value',
+ '[checked]': 'checked()',
+ '(change)': 'onChange($event)',
+ },
+})
+export class ChecklistValueDirective {
+ uiChecklistValue = input(null);
+
+ checked = model(false);
+
+ onChange(event: Event) {
+ if (event.target instanceof HTMLInputElement) {
+ this.checked.set(event.target.checked);
+ }
+ }
+}
diff --git a/libs/ui/input-controls/src/lib/checkbox/checklist.component.ts b/libs/ui/input-controls/src/lib/checkbox/checklist.component.ts
new file mode 100644
index 000000000..c15085072
--- /dev/null
+++ b/libs/ui/input-controls/src/lib/checkbox/checklist.component.ts
@@ -0,0 +1,90 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ contentChildren,
+ model,
+ effect,
+} from '@angular/core';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+import { CdkObserveContent } from '@angular/cdk/observers';
+import { ChecklistValueDirective } from './checklist-value.directive';
+import { isEqual } from 'lodash';
+
+@Component({
+ selector: 'ui-checklist',
+ template: '',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [CdkObserveContent],
+ host: {
+ '[class]': '["ui-checklist"]',
+ },
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: ChecklistComponent,
+ multi: true,
+ },
+ ],
+})
+export class ChecklistComponent implements ControlValueAccessor {
+ value = model([]);
+
+ onChange?: (value: unknown[]) => void;
+ onTouched?: () => void;
+
+ valueDirectives = contentChildren(ChecklistValueDirective, {
+ descendants: true,
+ });
+
+ valueDirectivesEffect = effect(() => {
+ const valueDirectives = this.valueDirectives();
+
+ const currentValue = this.value() ?? [];
+ const nextValue = structuredClone(currentValue);
+
+ for (const directive of valueDirectives) {
+ const value = directive.uiChecklistValue();
+ const checked = directive.checked();
+
+ if (checked) {
+ if (!nextValue.includes(value)) {
+ nextValue.push(value);
+ }
+ } else {
+ const index = nextValue.indexOf(value);
+ if (index !== -1) {
+ nextValue.splice(index, 1);
+ }
+ }
+ }
+
+ if (isEqual(currentValue, nextValue)) {
+ return;
+ }
+
+ this.value.set(nextValue);
+ this.onChange?.(nextValue);
+ this.onTouched?.();
+ });
+
+ writeValue(obj: unknown): void {
+ if (Array.isArray(obj)) {
+ this.value.set(obj);
+ for (const directive of this.valueDirectives()) {
+ const value = directive.uiChecklistValue();
+ const checked = obj.includes(value);
+ directive.checked.set(checked);
+ }
+ } else {
+ this.value.set([]);
+ }
+ }
+
+ registerOnChange(fn: (value: unknown[]) => void): void {
+ this.onChange = fn;
+ }
+
+ registerOnTouched(fn: () => void): void {
+ this.onTouched = fn;
+ }
+}
diff --git a/libs/ui/input-controls/src/lib/text-field/_textarea.scss b/libs/ui/input-controls/src/lib/text-field/_textarea.scss
new file mode 100644
index 000000000..6beb414b1
--- /dev/null
+++ b/libs/ui/input-controls/src/lib/text-field/_textarea.scss
@@ -0,0 +1,24 @@
+.ui-textarea {
+ @apply px-2 py-1;
+ @apply bg-white text-isa-neutral-900 isa-text-body-2-regular;
+ @apply inline-flex flex-col gap-1;
+ @apply border border-solid rounded-lg;
+
+ textarea {
+ &:focus {
+ @apply outline-none;
+ }
+
+ &:placeholder {
+ @apply text-isa-neutral-400;
+ }
+ }
+}
+
+.ui-textarea__empty {
+ @apply border-isa-neutral-600;
+}
+
+.ui-textarea__not-empty {
+ @apply border-isa-neutral-900;
+}
diff --git a/libs/ui/input-controls/src/lib/text-field/textarea.component.html b/libs/ui/input-controls/src/lib/text-field/textarea.component.html
new file mode 100644
index 000000000..ccedd768a
--- /dev/null
+++ b/libs/ui/input-controls/src/lib/text-field/textarea.component.html
@@ -0,0 +1,4 @@
+@if (label()) {
+ {{ label() }}
+}
+
diff --git a/libs/ui/input-controls/src/lib/text-field/textarea.component.ts b/libs/ui/input-controls/src/lib/text-field/textarea.component.ts
new file mode 100644
index 000000000..d90fcaa05
--- /dev/null
+++ b/libs/ui/input-controls/src/lib/text-field/textarea.component.ts
@@ -0,0 +1,19 @@
+import { Component, computed, contentChild, input } from '@angular/core';
+import { NgControl } from '@angular/forms';
+
+@Component({
+ selector: 'ui-textarea',
+ templateUrl: './textarea.component.html',
+ host: { '[class]': '["ui-textarea", controlEmptyClass()]' },
+})
+export class TextareaComponent {
+ label = input();
+
+ ngControl = contentChild(NgControl);
+
+ controlEmptyClass = computed(() => {
+ return this.ngControl()?.value
+ ? 'ui-textarea__not-empty'
+ : 'ui-textarea__empty';
+ });
+}