Details Page Für Kundendetails

This commit is contained in:
Lorenz Hilpert
2020-11-10 15:38:50 +01:00
parent 702cdaad4f
commit c2dc51b9ed
37 changed files with 747 additions and 19 deletions

View File

@@ -1296,6 +1296,46 @@
}
}
}
},
"@ui/input": {
"projectType": "library",
"root": "apps/ui/input",
"sourceRoot": "apps/ui/input/src",
"prefix": "ui",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"tsConfig": "apps/ui/input/tsconfig.lib.json",
"project": "apps/ui/input/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/ui/input/tsconfig.lib.prod.json"
}
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "apps/ui/input/src/test.ts",
"tsConfig": "apps/ui/input/tsconfig.spec.json",
"karmaConfig": "apps/ui/input/karma.conf.js"
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"apps/ui/input/tsconfig.lib.json",
"apps/ui/input/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
}
},
"defaultProject": "sales"

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { StringDictionary } from '@cmf/core';
import { AutocompleteDTO, CustomerInfoDTO, CustomerService, InputDTO } from '@swagger/crm';
import { AutocompleteDTO, CustomerDTO, CustomerInfoDTO, CustomerService, InputDTO } from '@swagger/crm';
import { PagedResult, Result } from 'apps/domain/defs/src/public-api';
import { Observable } from 'rxjs';
@@ -28,7 +28,15 @@ export class CrmCustomerService {
});
}
getCustomer(customerId: number, eagerLoading?: number): Observable<Result<CustomerDTO>> {
return this.customerService.CustomerGetCustomer({ customerId, eagerLoading });
}
getFilters(): Observable<Result<InputDTO[]>> {
return this.customerService.CustomerQueryCustomerFilter();
}
patchCustomer(customerId: number, customer: CustomerDTO): Observable<Result<CustomerDTO>> {
return this.customerService.CustomerPatchCustomer({ customerId, customer });
}
}

View File

@@ -0,0 +1,98 @@
<a class="card-customer-orders">
<span class="title">Bestellungen</span>
</a>
<ng-container *ngIf="customer$ | async; let customer">
<div class="card-customer-details">
<div class="header-container">
<h1 class="title">Kundendetails</h1>
<a class="button-customer-history">
Historie
</a>
</div>
<p class="info">
Sind Ihre Kundendaten korrekt?
</p>
<div class="tags">
<div class="tag" *ngFor="let feature of customer.features">
<ui-icon icon="check" size="15px"></ui-icon>
{{ feature.description }}
</div>
</div>
<div class="details">
<div class="detail">
<span class="name">Erstellungsdatum</span>
<span class="value">{{ customer.created | date: 'dd.MM.yy | HH:mm' }} Uhr</span>
</div>
<div class="detail">
<span class="name">Kundennummer</span>
<span class="value">{{ customer.customerNumber }}</span>
</div>
<!-- <div class="detail">
<span class="name">Kundennummer-Filiale</span>
<span class="value">{{ customer.customerNumber }}</span>
</div>
<div class="detail">
<span class="name">Kundennummer-beeline</span>
<span class="value">??</span>
</div> -->
</div>
</div>
<ui-inline-input label="Anrede">
<input uiInput type="text" [disabled]="!(storeCustoemr$ | async)" [value]="customer?.gender" />
</ui-inline-input>
<ui-inline-input label="Titel" (save)="patchCustomer('title', $event)">
<input uiInput type="text" [disabled]="!(storeCustoemr$ | async)" [value]="customer?.title || ''" />
</ui-inline-input>
<ui-inline-input label="Vorname" (save)="patchCustomer('firstName', $event)">
<input uiInput type="text" [disabled]="!(storeCustoemr$ | async)" [value]="customer?.firstName || ''" />
</ui-inline-input>
<ui-inline-input label="Nachname" (save)="patchCustomer('lastName', $event)">
<input uiInput type="text" [disabled]="!(storeCustoemr$ | async)" [value]="customer?.lastName || ''" />
</ui-inline-input>
<ui-inline-input label="E-Mail" (save)="patchCommunicationDetails('email', $event)">
<input uiInput type="text" [disabled]="!(storeCustoemr$ | async)" [value]="customer?.communicationDetails?.email || ''" />
</ui-inline-input>
<ui-inline-input label="Festnetznummer" (save)="patchCommunicationDetails('phone', $event)">
<input uiInput type="text" [disabled]="!(storeCustoemr$ | async)" [value]="customer?.communicationDetails?.phone || ''" />
</ui-inline-input>
<ui-inline-input label="Mobilnummer" (save)="patchCommunicationDetails('mobile', $event)">
<input uiInput type="text" [disabled]="!(storeCustoemr$ | async)" [value]="customer?.communicationDetails?.mobile || ''" />
</ui-inline-input>
<ui-inline-input label="Geburtsdatum">
<input uiInput type="text" [disabled]="!(storeCustoemr$ | async)" [value]="(customer?.dateOfBirth | date) || ''" />
</ui-inline-input>
<ui-inline-input label="Firmenname" (save)="patchOrganisation('name', $event)">
<input uiInput type="text" [disabled]="!(storeCustoemr$ | async)" [value]="customer?.organisation?.name || ''" />
</ui-inline-input>
<ui-inline-input label="Abteilung" (save)="patchOrganisation('department', $event)">
<input uiInput type="text" [disabled]="!(storeCustoemr$ | async)" [value]="customer?.organisation?.department || ''" />
</ui-inline-input>
<ui-inline-input label="USt-ID" (save)="patchOrganisation('vatId', $event)">
<input uiInput type="text" [disabled]="!(storeCustoemr$ | async)" [value]="customer?.organisation?.vatId || ''" />
</ui-inline-input>
<div class="card-section-title">
<h3>Rechnungsadresse</h3>
<button class="button-add-address">Hinzufügen</button>
</div>
<ui-inline-input [label]="payerToAddressString(payer)" *ngFor="let payer of customer.payers">
<input uiInput name="payer" type="radio" />
</ui-inline-input>
<div class="card-section-title">
<h3>Lieferadresse</h3>
<button class="button-add-address">Hinzufügen</button>
</div>
<ui-inline-input [label]="shippingAddressToAddressString(address?.data)" *ngFor="let address of customer.shippingAddresses">
<input uiInput name="shipping" type="radio" />
</ui-inline-input>
<div class="card-customer-footer">
<button class="cta-to-cart">
Weiter zum Warenkorb
</button>
</div>
</ng-container>

View File

@@ -0,0 +1,78 @@
:host {
@apply flex flex-col box-border overflow-scroll;
height: calc(100vh - 293px);
}
.title {
@apply text-page-heading font-bold;
}
.info {
@apply text-2xl mt-1 mb-px-40;
}
.card-customer-orders,
.card-customer-details {
@apply bg-white rounded-t-card p-4 text-center;
box-shadow: 0 -2px 24px 0 #dce2e9;
}
.card-customer-orders .title {
@apply text-inactive-customer text-xl;
}
.tags {
@apply flex flex-row justify-center items-center text-ucla-blue font-bold text-regular mb-px-40;
}
.tag {
@apply flex items-center gap-2;
}
.details {
@apply flex flex-col text-regular gap-4;
.detail {
@apply flex flex-row gap-8;
.name {
}
.value {
@apply font-bold;
}
}
}
ui-inline-input {
@apply my-px;
}
.card-section-title {
@apply bg-white flex flex-row justify-between p-4 items-center pt-px-40 pb-px-20;
h3 {
@apply m-0 text-card-sub;
}
button.button-add-address {
@apply p-0 text-card-sub bg-transparent text-brand border-none font-bold;
}
}
.card-customer-footer {
@apply p-4 py-px-25 bg-white text-center rounded-b-card;
button.cta-to-cart {
@apply px-px-20 py-px-15 text-card-sub bg-brand text-white border-none font-bold rounded-full;
}
}
.header-container {
@apply relative;
}
a.button-customer-history {
@apply text-card-sub bg-transparent text-brand border-none font-bold rounded-full absolute right-0 top-0 py-px-5;
}

View File

@@ -0,0 +1,61 @@
import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { CrmCustomerService } from '@domain/crm';
import { AssignedPayerDTO, CommunicationDetailsDTO, CustomerDTO, OrganisationDTO, ShippingAddressDTO } from '@swagger/crm';
import { Observable } from 'rxjs';
import { first, map, shareReplay, switchMap, take } from 'rxjs/operators';
@Component({
selector: 'page-customer-details',
templateUrl: 'customer-details.component.html',
styleUrls: ['customer-details.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomerDetailsComponent implements OnInit {
customerId$: Observable<number>;
customer$: Observable<CustomerDTO>;
storeCustoemr$: Observable<boolean>;
onlineCustomer$: Observable<boolean>;
b2bCustomer$: Observable<boolean>;
payerToAddressString(payer: AssignedPayerDTO) {
const payerDto = payer?.payer?.data;
return `${payerDto?.firstName} ${payerDto?.lastName} | ${payerDto?.address?.street} ${payerDto?.address?.streetNumber}, ${payerDto?.address?.zipCode} ${payerDto?.address?.city}, ${payerDto?.address?.country}`;
}
shippingAddressToAddressString(shippingAddress: ShippingAddressDTO) {
return `${shippingAddress?.firstName} ${shippingAddress?.lastName} | ${shippingAddress?.address?.street} ${shippingAddress?.address?.streetNumber}, ${shippingAddress?.address?.zipCode} ${shippingAddress?.address?.city}, ${shippingAddress?.address?.country}`;
}
constructor(private activatedRoute: ActivatedRoute, private customerDetailsService: CrmCustomerService) {}
ngOnInit() {
this.customerId$ = this.activatedRoute.params.pipe(map((params) => Number(params['customerId'])));
this.customer$ = this.customerId$.pipe(
switchMap((customerId) => this.customerDetailsService.getCustomer(customerId, 2)),
map(({ result }) => result),
shareReplay()
);
this.storeCustoemr$ = this.customer$.pipe(map((customer) => !!customer?.features?.some((s) => s.key === 'store')));
this.onlineCustomer$ = this.customer$.pipe(map((customer) => !!customer?.features?.some((s) => s.key === 'onlineshop')));
this.b2bCustomer$ = this.customer$.pipe(map((customer) => !!customer?.features?.some((s) => s.key === 'b2b')));
}
async patchCustomer(proeprty: keyof CustomerDTO, value: any) {
const customer = await this.customer$.pipe(first()).toPromise();
await this.customerDetailsService.patchCustomer(customer.id, { [proeprty]: value }).toPromise();
}
async patchCommunicationDetails(proeprty: keyof CommunicationDetailsDTO, value: any) {
const customer = await this.customer$.pipe(first()).toPromise();
await this.customerDetailsService.patchCustomer(customer.id, { communicationDetails: { [proeprty]: value } }).toPromise();
}
async patchOrganisation(proeprty: keyof OrganisationDTO, value: any) {
const customer = await this.customer$.pipe(first()).toPromise();
await this.customerDetailsService.patchCustomer(customer.id, { organisation: { [proeprty]: value } }).toPromise();
}
}

View File

@@ -0,0 +1,13 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CustomerDetailsComponent } from './customer-details.component';
import { UiIconModule } from '@ui/icon';
import { UiInputModule } from '@ui/input';
@NgModule({
imports: [CommonModule, UiIconModule, UiInputModule],
exports: [CustomerDetailsComponent],
declarations: [CustomerDetailsComponent],
})
export class CustomerDetailsModule {}

View File

@@ -1,20 +1,13 @@
<div
class="scroll-container"
*ngIf="search.searchState$ | async as searchState"
>
<div class="scroll-container" *ngIf="search.searchState$ | async as searchState">
<a
[class.last]="last"
[class.load]="searchState === 'fetching'"
href="#"
*ngFor="
let customer of (search.searchResult$ | async)?.result;
let last = last
"
[routerLink]="['/customer', customer.id]"
*ngFor="let customer of (search.searchResult$ | async)?.result; let last = last"
uiIsInViewport
[options]="viewportEnterOptions"
(viewportEntered)="checkIfReload($event)"
>
<page-customer-result-card [customer]="customer">
</page-customer-result-card>
<page-customer-result-card [customer]="customer"> </page-customer-result-card>
</a>
</div>

View File

@@ -16,6 +16,7 @@ export class CustomerSearchResultComponent implements OnInit, OnDestroy {
protected readonly viewportEnterOptions: IntersectionObserverInit = {
threshold: 0.75,
};
constructor(private application: ApplicationService, private breadcrumb: BreadcrumbService, public search: CustomerSearch) {}
ngOnInit() {

View File

@@ -1,5 +1,6 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CustomerDetailsComponent } from './customer-details/customer-details.component';
import { CustomerSearchComponent } from './customer-search/customer-search.component';
import { CustomerSearchMainComponent } from './customer-search/search-main/search-main.component';
import { CustomerSearchResultComponent } from './customer-search/search-results/search-results.component';
@@ -19,6 +20,10 @@ const routes: Routes = [
{ path: ':customerId/details', component: null },
],
},
{
path: ':customerId',
component: CustomerDetailsComponent,
},
],
},
];

View File

@@ -5,14 +5,10 @@ import { PageCustomerComponent } from './page-customer.component';
import { PageCustomerRoutingModule } from './page-customer-routing.module';
import { ShellBreadcrumbModule } from '@shell/breadcrumb';
import { CustomerSearchModule } from './customer-search/customer-search.module';
import { CustomerDetailsModule } from './customer-details/customer-details.module';
@NgModule({
imports: [
CommonModule,
PageCustomerRoutingModule,
ShellBreadcrumbModule,
CustomerSearchModule,
],
imports: [CommonModule, PageCustomerRoutingModule, ShellBreadcrumbModule, CustomerSearchModule, CustomerDetailsModule],
exports: [PageCustomerComponent],
declarations: [PageCustomerComponent],
})

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g id="checkbox" transform="matrix(1.45282,0,0,1.45282,-525.904,-518.64)">
<path d="M384,360C384,358.344 382.656,357 381,357L365,357C363.344,357 362,358.344 362,360L362,376C362,377.656 363.344,379 365,379L381,379C382.656,379 384,377.656 384,376L384,360ZM382,360L382,376C382,376.552 381.552,377 381,377C381,377 365,377 365,377C364.448,377 364,376.552 364,376L364,360C364,359.448 364.448,359 365,359L381,359C381.552,359 382,359.448 382,360Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 913 B

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path id="checkbox_checked" d="M31.981,4.377C31.981,1.972 30.028,0.019 27.623,0.019L4.377,0.019C1.972,0.019 0.019,1.972 0.019,4.377L0.019,27.623C0.019,30.028 1.972,31.981 4.377,31.981L27.623,31.981C30.028,31.981 31.981,30.028 31.981,27.623L31.981,4.377ZM29.075,4.377L29.075,27.623C29.075,28.424 28.424,29.075 27.623,29.075C27.623,29.075 4.377,29.075 4.377,29.075C3.576,29.075 2.925,28.424 2.925,27.623L2.925,4.377C2.925,3.576 3.576,2.925 4.377,2.925L27.623,2.925C28.424,2.925 29.075,3.576 29.075,4.377ZM25.151,8.013C24.854,8.055 24.58,8.197 24.374,8.415C19.929,12.869 16.602,16.545 12.361,20.844L7.533,16.766C7.169,16.457 6.667,16.366 6.218,16.527C5.768,16.689 5.44,17.079 5.356,17.549C5.272,18.019 5.447,18.498 5.813,18.805L11.584,23.688C12.115,24.134 12.899,24.098 13.387,23.605C18.15,18.832 21.553,15.005 26.26,10.288C26.674,9.887 26.782,9.265 26.53,8.748C26.277,8.231 25.721,7.934 25.151,8.013Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -31,6 +31,7 @@
<g id="checkbox" transform="matrix(1.45282,0,0,1.45282,-525.904,-518.64)">
<path d="M384,360C384,358.344 382.656,357 381,357L365,357C363.344,357 362,358.344 362,360L362,376C362,377.656 363.344,379 365,379L381,379C382.656,379 384,377.656 384,376L384,360ZM382,360L382,376C382,376.552 381.552,377 381,377C381,377 365,377 365,377C364.448,377 364,376.552 364,376L364,360C364,359.448 364.448,359 365,359L381,359C381.552,359 382,359.448 382,360Z"/>
</g>
<path id="checkbox_checked" d="M31.981,4.377C31.981,1.972 30.028,0.019 27.623,0.019L4.377,0.019C1.972,0.019 0.019,1.972 0.019,4.377L0.019,27.623C0.019,30.028 1.972,31.981 4.377,31.981L27.623,31.981C30.028,31.981 31.981,30.028 31.981,27.623L31.981,4.377ZM29.075,4.377L29.075,27.623C29.075,28.424 28.424,29.075 27.623,29.075C27.623,29.075 4.377,29.075 4.377,29.075C3.576,29.075 2.925,28.424 2.925,27.623L2.925,4.377C2.925,3.576 3.576,2.925 4.377,2.925L27.623,2.925C28.424,2.925 29.075,3.576 29.075,4.377ZM25.151,8.013C24.854,8.055 24.58,8.197 24.374,8.415C19.929,12.869 16.602,16.545 12.361,20.844L7.533,16.766C7.169,16.457 6.667,16.366 6.218,16.527C5.768,16.689 5.44,17.079 5.356,17.549C5.272,18.019 5.447,18.498 5.813,18.805L11.584,23.688C12.115,24.134 12.899,24.098 13.387,23.605C18.15,18.832 21.553,15.005 26.26,10.288C26.674,9.887 26.782,9.265 26.53,8.748C26.277,8.231 25.721,7.934 25.151,8.013Z"/>
<path id="circle_check" d="M16,0C24.831,0 32,7.169 32,16C32,24.831 24.831,32 16,32C7.169,32 0,24.831 0,16C0,7.169 7.169,0 16,0ZM22.864,10.009C22.642,10.042 22.436,10.148 22.282,10.311C18.947,13.652 16.452,16.409 13.27,19.633L9.649,16.574C9.375,16.342 8.999,16.274 8.662,16.395C8.325,16.517 8.078,16.809 8.015,17.162C7.953,17.514 8.083,17.874 8.358,18.104L12.687,21.766C13.086,22.101 13.674,22.073 14.04,21.704C17.612,18.124 20.165,15.254 23.697,11.716C24.007,11.415 24.088,10.949 23.899,10.561C23.709,10.173 23.292,9.95 22.864,10.009Z"/>
<g id="circle" transform="matrix(0.842105,0,0,0.842105,-265.263,-67.3684)">
<circle cx="334" cy="99" r="19"/>
@@ -156,4 +157,6 @@
<g id="scan" transform="matrix(0.985423,0,0,0.985423,0.233193,0.233244)">
<path d="M29.951,26.27L29.952,23.268L32.237,23.268L32.237,28.544L26.933,28.544L26.933,26.27L29.951,26.27ZM2.049,26.27L5.067,26.27L5.067,28.544L-0.237,28.544L-0.237,23.268L2.048,23.268L2.049,26.27ZM6.274,8.259L3.99,8.259L3.99,23.741L6.274,23.741L6.274,8.259ZM14.727,20.867L12.443,20.867L12.443,23.741L14.727,23.741L14.727,20.867ZM10.501,20.867L8.216,20.867L8.216,23.741L10.501,23.741L10.501,20.867ZM27.407,8.259L25.122,8.259L25.122,23.741L27.407,23.741L27.407,8.259ZM23.18,20.867L20.895,20.867L20.895,23.741L23.18,23.741L23.18,20.867ZM18.954,8.259L16.669,8.259L16.669,23.741L18.954,23.741L18.954,8.259ZM10.501,8.259L8.216,8.259L8.216,18.338L10.501,18.338L10.501,8.259ZM23.18,8.259L20.895,8.259L20.895,18.338L23.18,18.338L23.18,8.259ZM14.727,8.259L12.443,8.259L12.443,18.338L14.727,18.338L14.727,8.259ZM29.951,5.73L26.933,5.73L26.933,3.456L32.237,3.456L32.237,8.732L29.952,8.732L29.951,5.73ZM2.049,5.73L2.048,8.732L-0.237,8.732L-0.237,3.456L5.067,3.456L5.067,5.73L2.049,5.73Z"/>
</g>
<path id="radio_checked" d="M16,0.006L16.455,0.012C25.078,0.253 31.994,7.319 31.994,16C31.994,24.833 24.833,31.994 16,31.994C7.167,31.994 0.006,24.833 0.006,16C0.006,7.167 7.167,0.006 16,0.006ZM16.024,2.77L15.499,2.782C12.183,2.929 9.037,4.325 6.701,6.701C4.201,9.16 2.785,12.516 2.77,16.024C2.755,19.53 4.142,22.899 6.622,25.378C9.101,27.858 12.47,29.245 15.976,29.23L16.501,29.217C19.817,29.071 22.963,27.674 25.299,25.299C27.799,22.84 29.215,19.483 29.23,15.976C29.245,12.47 27.858,9.101 25.378,6.622C22.899,4.142 19.53,2.755 16.024,2.77ZM16,10C19.311,10 22,12.688 22,16C22,19.311 19.311,22 16,22C12.688,22 10,19.311 10,16C10,12.688 12.688,10 16,10Z"/>
<path id="radio" d="M16,0.006L16.455,0.012C25.078,0.253 31.994,7.319 31.994,16C31.994,24.833 24.833,31.994 16,31.994C7.167,31.994 0.006,24.833 0.006,16C0.006,7.167 7.167,0.006 16,0.006ZM16.024,2.77L15.499,2.782C12.183,2.929 9.037,4.325 6.701,6.701C4.201,9.16 2.785,12.516 2.77,16.024C2.755,19.53 4.142,22.899 6.622,25.378C9.101,27.858 12.47,29.245 15.976,29.23L16.501,29.217C19.817,29.071 22.963,27.674 25.299,25.299C27.799,22.84 29.215,19.483 29.23,15.976C29.245,12.47 27.858,9.101 25.378,6.622C22.899,4.142 19.53,2.755 16.024,2.77Z" style="fill-rule:nonzero;"/>
</svg>

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g id="radio" transform="matrix(1.45455,0,0,1.45455,0,0)">
<path d="M11,0C17.071,0 22,4.929 22,11C22,17.071 17.071,22 11,22C4.929,22 0,17.071 0,11C0,4.929 4.929,0 11,0ZM11,2C15.967,2 20,6.033 20,11C20,15.967 15.967,20 11,20C6.033,20 2,15.967 2,11C2,6.033 6.033,2 11,2Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 743 B

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path id="radio_checked" d="M16,0C24.831,0 32,7.169 32,16C32,24.831 24.831,32 16,32C7.169,32 0,24.831 0,16C0,7.169 7.169,0 16,0ZM16,2.909C23.225,2.909 29.091,8.775 29.091,16C29.091,23.225 23.225,29.091 16,29.091C8.775,29.091 2.909,23.225 2.909,16C2.909,8.775 8.775,2.909 16,2.909ZM16,9.398C19.644,9.398 22.602,12.356 22.602,16C22.602,19.644 19.644,22.602 16,22.602C12.356,22.602 9.398,19.644 9.398,16C9.398,12.356 12.356,9.398 16,9.398Z"/>
</svg>

After

Width:  |  Height:  |  Size: 894 B

25
apps/ui/input/README.md Normal file
View File

@@ -0,0 +1,25 @@
# Input
This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 10.1.2.
## Code scaffolding
Run `ng generate component component-name --project input` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project input`.
> Note: Don't forget to add `--project input` or else it will be added to the default project in your `angular.json` file.
## Build
Run `ng build input` to build the project. The build artifacts will be stored in the `dist/` directory.
## Publishing
After building your library with `ng build input`, go to the dist folder `cd dist/input` and run `npm publish`.
## Running unit tests
Run `ng test input` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).

View File

@@ -0,0 +1,32 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma'),
],
client: {
clearContext: false, // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../../../coverage/ui/input'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true,
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true,
});
};

View File

@@ -0,0 +1,7 @@
{
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../../dist/ui/input",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View File

@@ -0,0 +1,11 @@
{
"name": "@ui/input",
"version": "0.0.1",
"peerDependencies": {
"@angular/common": "^10.1.2",
"@angular/core": "^10.1.2"
},
"dependencies": {
"tslib": "^2.0.0"
}
}

View File

@@ -0,0 +1,6 @@
// start:ng42.barrel
export * from './ui-inline-input.component';
export * from './ui-input.component';
export * from './ui-input.dircetive';
export * from './ui-input.module';
// end:ng42.barrel

View File

@@ -0,0 +1,34 @@
<label [class.checked]="input?.checked" (click)="input?.toggle()">{{ label }}</label>
<ng-container *ngIf="input?.type === 'radio'">
<ui-icon
[class.disabled]="input?.disabled"
[icon]="input?.checked ? 'radio_checked' : 'radio'"
(click)="input?.toggle()"
size="22px"
></ui-icon>
</ng-container>
<ng-container *ngIf="input?.type === 'checkbox'">
<ui-icon
[class.disabled]="input?.disabled"
[icon]="input?.checked ? 'checkbox_checked' : 'checkbox'"
(click)="input?.toggle()"
size="22px"
></ui-icon>
</ng-container>
<ng-content select="input[uiInput]"></ng-content>
<ng-container *ngIf="input?.type === 'text'">
<button class="clear-input" *ngIf="input?.active && input.value" (click)="input?.clear()">
<ui-icon icon="close" size="23px"></ui-icon>
</button>
<button class="edit-input" *ngIf="!input?.disabled && !input?.active" (click)="input?.activate(); input?.focus()">Ändern</button>
<button class="edit-input save" *ngIf="!input?.disabled && input?.active" (click)="input?.deactivate(); save.emit(input?.value)">
Speichern
</button>
<span class="disabled-label" *ngIf="input?.disabled">
Nicht änderbar
</span>
</ng-container>

View File

@@ -0,0 +1,57 @@
:host {
@apply flex flex-row gap-4 items-center p-4 bg-white text-regular;
}
:host ::ng-deep input {
@apply bg-transparent flex-grow text-regular border-none font-bold;
&:disabled {
@apply bg-transparent;
}
&:focus {
@apply outline-none;
}
}
button.clear-input {
@apply bg-transparent border-none text-ucla-blue p-0;
}
button.edit-input {
@apply bg-transparent border-none text-regular text-brand font-bold p-0;
}
.disabled-label {
@apply text-ucla-blue;
}
:host.input-checkbox,
:host.input-radio {
@apply flex-row-reverse;
& ::ng-deep input {
@apply flex-grow-0;
}
label {
@apply flex-grow;
}
}
:host ::ng-deep input[type='checkbox'],
:host ::ng-deep input[type='radio'] {
@apply hidden;
}
ui-icon {
@apply text-ucla-blue;
}
ui-icon.disabled {
@apply text-cool-grey;
}
label.checked {
@apply font-bold;
}

View File

@@ -0,0 +1,43 @@
import {
Component,
ChangeDetectionStrategy,
Input,
ContentChild,
EventEmitter,
HostBinding,
AfterContentInit,
ChangeDetectorRef,
Output,
} from '@angular/core';
import { UiInputDirective } from './ui-input.dircetive';
@Component({
selector: 'ui-inline-input',
templateUrl: 'ui-inline-input.component.html',
styleUrls: ['ui-inline-input.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UiInlineInputComponent implements AfterContentInit {
@Input()
label: string;
@ContentChild(UiInputDirective, { read: UiInputDirective })
input: UiInputDirective;
@Output()
save = new EventEmitter<any>();
@HostBinding('class')
get inputType() {
return `input-${this.input?.type}`;
}
constructor(private cdr: ChangeDetectorRef) {}
ngAfterContentInit() {
// TODO: Bessere Lösung benötigt
setInterval(() => {
this.cdr.detectChanges();
}, 100);
}
}

View File

View File

View File

@@ -0,0 +1,11 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'ui-input',
templateUrl: 'ui-input.component.html',
styleUrls: ['ui-input.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UiInputComponent {
constructor() {}
}

View File

@@ -0,0 +1,65 @@
import { Directive, ElementRef, EventEmitter, HostBinding, HostListener, Input, Output, Renderer2 } from '@angular/core';
@Directive({ selector: 'input[uiInput]' })
export class UiInputDirective {
@Input()
disabled: boolean;
active: boolean;
@HostBinding('disabled')
get inputDisabled() {
const isRadioOrCheckbox = this.type === 'checkbox' || this.type === 'radio';
return isRadioOrCheckbox ? this.disabled : this.disabled || !this.active;
}
get value() {
return this.elementRef?.nativeElement?.value;
}
get type() {
return this.elementRef?.nativeElement?.type;
}
get checked() {
return this.elementRef?.nativeElement?.checked;
}
@Output()
events = new EventEmitter<Event>();
constructor(private elementRef: ElementRef, private renderer: Renderer2) {}
activate() {
this.active = true;
}
deactivate() {
this.active = false;
}
focus() {
const native: HTMLInputElement = this.elementRef?.nativeElement;
setTimeout(() => {
native?.focus();
}, 1);
}
clear() {
if (this.elementRef?.nativeElement) {
this.renderer.setProperty(this.elementRef.nativeElement, 'value', '');
}
}
@HostListener('change', ['$event'])
change(event: Event) {
setTimeout(() => {
this.events.emit(event);
}, 1);
}
toggle() {
this.elementRef?.nativeElement?.click();
}
}

View File

@@ -0,0 +1,14 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { UiIconModule } from '@ui/icon';
import { UiInlineInputComponent } from './ui-inline-input.component';
import { UiInputComponent } from './ui-input.component';
import { UiInputDirective } from './ui-input.dircetive';
@NgModule({
imports: [CommonModule, UiIconModule],
exports: [UiInlineInputComponent, UiInputComponent, UiInputDirective],
declarations: [UiInlineInputComponent, UiInputComponent, UiInputDirective],
providers: [],
})
export class UiInputModule {}

View File

@@ -0,0 +1,5 @@
/*
* Public API Surface of input
*/
export * from './lib';

24
apps/ui/input/src/test.ts Normal file
View File

@@ -0,0 +1,24 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone';
import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
declare const require: {
context(
path: string,
deep?: boolean,
filter?: RegExp
): {
keys(): string[];
<T>(id: string): T;
};
};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

View File

@@ -0,0 +1,25 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"outDir": "../../../out-tsc/lib",
"target": "es2015",
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": [],
"lib": [
"dom",
"es2018"
]
},
"angularCompilerOptions": {
"skipTemplateCodegen": true,
"strictMetadataEmit": true,
"enableResourceInlining": true
},
"exclude": [
"src/test.ts",
"**/*.spec.ts"
]
}

View File

@@ -0,0 +1,10 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.lib.json",
"compilerOptions": {
"declarationMap": false
},
"angularCompilerOptions": {
"enableIvy": false
}
}

View File

@@ -0,0 +1,17 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"outDir": "../../../out-tsc/spec",
"types": [
"jasmine"
]
},
"files": [
"src/test.ts"
],
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
}

17
apps/ui/input/tslint.json Normal file
View File

@@ -0,0 +1,17 @@
{
"extends": "../../../tslint.json",
"rules": {
"directive-selector": [
true,
"attribute",
"ui",
"camelCase"
],
"component-selector": [
true,
"element",
"ui",
"kebab-case"
]
}
}

View File

Binary file not shown.

View File

@@ -24,6 +24,7 @@ module.exports = {
regular: '16px',
'cta-l': '18px',
'card-heading': '22px',
'card-sub': '20px',
'page-heading': '26px',
},
maxWidth: {
@@ -38,6 +39,7 @@ module.exports = {
'px-15': '15px',
'px-20': '20px',
'px-25': '25px',
'px-40': '40px',
'px-50': '50px',
'px-100': '100px',
'px-150': '150px',

View File

@@ -132,7 +132,10 @@
"apps/ui/filter/src/public-api.ts"
],
"@core/application": [
"apps/core/application/src/public-api.ts",
"apps/core/application/src/public-api.ts"
],
"@ui/input": [
"apps/ui/input/src/public-api.ts"
]
}
}