Skip to content

Commit adef212

Browse files
authored
Add compliance form to product specification (#232)
* Add support for requesting a compliance verification * Recover missing translations
1 parent 7c13472 commit adef212

10 files changed

Lines changed: 726 additions & 41 deletions

File tree

src/app/app.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ import { QuotesModule } from "src/app/features/quotes/quotes.module"
100100
import { MarkdownTextareaComponent } from "src/app/shared/forms/markdown-textarea/markdown-textarea.component"
101101
import { ProviderRevenueSharingComponent } from "src/app/pages/user-profile/profile-sections/provider-revenue-sharing/provider-revenue-sharing.component"
102102
import { OperatorRevenueSharingComponent } from "src/app/pages/admin/operator-revenue-sharing/operator-revenue-sharing.component"
103+
import { RequestValidationModalComponent } from './pages/seller-offerings/offerings/seller-product-spec/update-product-spec/request-validation-modal/request-validation-modal.component';
103104

104105
@NgModule({
105106
declarations: [
@@ -162,7 +163,8 @@ import { OperatorRevenueSharingComponent } from "src/app/pages/admin/operator-re
162163
OrganizationDetailsComponent,
163164
FaqComponent,
164165
NewPricePlanComponent,
165-
UpdatePricePlanComponent
166+
UpdatePricePlanComponent,
167+
RequestValidationModalComponent
166168
],
167169
imports: [
168170
BrowserModule,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<div class="fixed inset-0 bg-black bg-opacity-50 flex justify-end transition-opacity duration-500 z-50"
2+
style="opacity: 0; pointer-events: none;"
3+
[style.opacity]="showModal ? '1' : '0'"
4+
[style.pointer-events]="showModal ? 'auto' : 'none'"
5+
(click)="closeModal.emit()">
6+
<div class="w-full md:w-5/6 lg:w-3/4 xl:w-2/3 h-full bg-secondary-50 dark:bg-gray-800 p-6 overflow-y-auto transform transition-transform duration-500"
7+
style="transform: translateX(100%);"
8+
[style.transform]="showModal ? 'translateX(0)' : 'translateX(100%)'"
9+
(click)="$event.stopPropagation();">
10+
<div class="flex justify-between items-center">
11+
<h3 class="text-2xl font-semibold text-primary-100 dark:text-white">Request Validation</h3>
12+
<button type="button"
13+
(click)="closeModal.emit()"
14+
class="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
15+
<svg class="min-w-4 w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
16+
<path fill="currentColor" d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c9.4-9.4 24.6-9.4 33.9 0l47 47 47-47c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-47 47 47 47c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-47-47-47 47c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l47-47-47-47c-9.4-9.4-9.4-24.6 0-33.9z"/>
17+
</svg>
18+
<span class="sr-only">Close drawer</span>
19+
</button>
20+
</div>
21+
22+
<p class="mt-2 mb-5 text-sm text-gray-500 dark:text-gray-400">
23+
Requesting a validation will submit your certificates and self attestation to the compliance service for validation.
24+
</p>
25+
26+
<div class="mb-6">
27+
<h4 class="mb-2 font-semibold text-gray-900 dark:text-white">Evidence to be submitted</h4>
28+
<div class="relative overflow-x-auto shadow-md sm:rounded-lg w-full bg-white dark:bg-secondary-300">
29+
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-200">
30+
<thead class="text-xs text-gray-700 uppercase bg-gray-100 dark:bg-secondary-200 dark:text-white">
31+
<tr>
32+
<th scope="col" class="px-6 py-3">Type</th>
33+
<th scope="col" class="px-6 py-3">Name</th>
34+
<th scope="col" class="px-6 py-3">Document</th>
35+
</tr>
36+
</thead>
37+
<tbody>
38+
@for (evidence of validationEvidenceRows; track evidence.type + evidence.name + evidence.value) {
39+
<tr class="border-b hover:bg-gray-200 dark:bg-secondary-300 dark:border-gray-700 dark:hover:bg-secondary-200">
40+
<td class="px-6 py-4">{{ evidence.type }}</td>
41+
<td class="px-6 py-4">{{ evidence.name }}</td>
42+
<td class="px-6 py-4 break-all">
43+
@if (evidence.missing) {
44+
<span class="font-medium text-red-600">Not provided</span>
45+
} @else if (isHttpUrl(evidence.value)) {
46+
<a [href]="evidence.value" target="_blank" class="font-medium text-blue-600 dark:text-blue-500 hover:underline">
47+
{{ evidence.value }}
48+
</a>
49+
} @else {
50+
{{ evidence.value }}
51+
}
52+
</td>
53+
</tr>
54+
} @empty {
55+
<tr>
56+
<td colspan="3" class="px-6 py-4 text-center text-gray-500 dark:text-gray-300">
57+
No compliance evidence available.
58+
</td>
59+
</tr>
60+
}
61+
</tbody>
62+
</table>
63+
</div>
64+
</div>
65+
66+
<div class="mb-6">
67+
<h4 class="mb-2 font-semibold text-gray-900 dark:text-white">Organization info</h4>
68+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
69+
<div class="p-4 bg-white border rounded-lg dark:bg-secondary-300"
70+
[ngClass]="isMissing(organizationInfo.organizationName) ? 'border-red-500 dark:border-red-500' : 'border-gray-200 dark:border-gray-700'">
71+
<p class="text-xs uppercase text-gray-500 dark:text-gray-300">Organization name</p>
72+
<p class="mt-1 text-sm text-gray-900 dark:text-white break-all">{{ organizationInfo.organizationName || '-' }}</p>
73+
</div>
74+
<div class="p-4 bg-white border rounded-lg dark:bg-secondary-300"
75+
[ngClass]="isMissing(organizationInfo.organizationVat) ? 'border-red-500 dark:border-red-500' : 'border-gray-200 dark:border-gray-700'">
76+
<p class="text-xs uppercase text-gray-500 dark:text-gray-300">Organization VAT</p>
77+
<p class="mt-1 text-sm text-gray-900 dark:text-white break-all">{{ organizationInfo.organizationVat || '-' }}</p>
78+
</div>
79+
<div class="p-4 bg-white border rounded-lg dark:bg-secondary-300"
80+
[ngClass]="isMissing(organizationInfo.organizationAddress) ? 'border-red-500 dark:border-red-500' : 'border-gray-200 dark:border-gray-700'">
81+
<p class="text-xs uppercase text-gray-500 dark:text-gray-300">Organization address</p>
82+
<p class="mt-1 text-sm text-gray-900 dark:text-white break-all">{{ organizationInfo.organizationAddress || '-' }}</p>
83+
</div>
84+
<div class="p-4 bg-white border rounded-lg dark:bg-secondary-300"
85+
[ngClass]="isMissing(organizationInfo.organizationCountry) ? 'border-red-500 dark:border-red-500' : 'border-gray-200 dark:border-gray-700'">
86+
<p class="text-xs uppercase text-gray-500 dark:text-gray-300">Organization country</p>
87+
<p class="mt-1 text-sm text-gray-900 dark:text-white break-all">{{ organizationInfo.organizationCountry || '-' }}</p>
88+
</div>
89+
<div class="p-4 bg-white border rounded-lg dark:bg-secondary-300"
90+
[ngClass]="isMissing(organizationInfo.organizationWebsite) ? 'border-red-500 dark:border-red-500' : 'border-gray-200 dark:border-gray-700'">
91+
<p class="text-xs uppercase text-gray-500 dark:text-gray-300">Organization website</p>
92+
<p class="mt-1 text-sm text-gray-900 dark:text-white break-all">{{ organizationInfo.organizationWebsite || '-' }}</p>
93+
</div>
94+
<div class="p-4 bg-white border rounded-lg dark:bg-secondary-300"
95+
[ngClass]="isMissing(organizationInfo.organizationEmail) ? 'border-red-500 dark:border-red-500' : 'border-gray-200 dark:border-gray-700'">
96+
<p class="text-xs uppercase text-gray-500 dark:text-gray-300">Organization email</p>
97+
<p class="mt-1 text-sm text-gray-900 dark:text-white break-all">{{ organizationInfo.organizationEmail || '-' }}</p>
98+
</div>
99+
</div>
100+
</div>
101+
102+
@if (hasMissingOrganizationInfo()) {
103+
<div class="mb-6 p-4 text-sm text-amber-800 rounded-lg bg-amber-100 dark:bg-amber-900 dark:text-amber-200" role="alert">
104+
Some information of your organization required for the validation process is missing. Please update you organization profile.
105+
</div>
106+
}
107+
@if (requestValidationError) {
108+
<div class="mb-6 p-4 text-sm text-red-800 rounded-lg bg-red-100 dark:bg-red-900 dark:text-red-200" role="alert">
109+
{{ requestValidationError }}
110+
</div>
111+
}
112+
113+
<div class="flex justify-end items-center space-x-4">
114+
<button (click)="closeModal.emit()"
115+
type="button"
116+
class="py-2 px-3 text-sm font-medium text-gray-500 bg-white rounded-lg border border-gray-200 hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-primary-300 hover:text-gray-900 focus:z-10">
117+
Cancel
118+
</button>
119+
<button (click)="submitRequestValidation()"
120+
[disabled]="hasMissingOrganizationInfo() || requestValidationLoading || !productSpec"
121+
type="button"
122+
[ngClass]="(hasMissingOrganizationInfo() || requestValidationLoading || !productSpec) ? 'opacity-50 cursor-not-allowed' : 'hover:bg-primary-50'"
123+
class="py-2 px-3 text-sm font-medium text-center text-white bg-primary-100 rounded-lg focus:ring-4 focus:outline-none focus:ring-primary-50">
124+
Request Validation
125+
</button>
126+
</div>
127+
</div>
128+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
2+
import { LoginInfo } from 'src/app/models/interfaces';
3+
import { AccountServiceService } from 'src/app/services/account-service.service';
4+
import { LocalStorageService } from 'src/app/services/local-storage.service';
5+
import { ProductSpecServiceService } from 'src/app/services/product-spec-service.service';
6+
7+
type ValidationEvidenceRow = {
8+
type: string;
9+
name: string;
10+
value: string;
11+
missing: boolean;
12+
};
13+
type OrganizationInfo = {
14+
organizationName: string;
15+
organizationVat: string;
16+
organizationAddress: string;
17+
organizationCountry: string;
18+
organizationWebsite: string;
19+
organizationEmail: string;
20+
};
21+
22+
@Component({
23+
selector: 'request-validation-modal',
24+
templateUrl: './request-validation-modal.component.html',
25+
styleUrl: './request-validation-modal.component.css'
26+
})
27+
export class RequestValidationModalComponent implements OnChanges {
28+
@Input() showModal: boolean = false;
29+
@Input() productSpec: any;
30+
@Input() selfAtt: any;
31+
@Input() selectedISOS: any[] = [];
32+
@Input() additionalISOS: any[] = [];
33+
requestValidationLoading: boolean = false;
34+
requestValidationError: string = '';
35+
organizationInfo: OrganizationInfo = this.getEmptyOrganizationInfo();
36+
37+
@Output() closeModal = new EventEmitter<void>();
38+
39+
constructor(
40+
private localStorage: LocalStorageService,
41+
private accountService: AccountServiceService,
42+
private prodSpecService: ProductSpecServiceService
43+
) {}
44+
45+
ngOnChanges(changes: SimpleChanges): void {
46+
if (changes['showModal']?.currentValue === true) {
47+
this.requestValidationError = '';
48+
this.loadOrganizationInfoFromCurrentSession();
49+
}
50+
}
51+
52+
get validationEvidenceRows(): ValidationEvidenceRow[] {
53+
const rows: ValidationEvidenceRow[] = [];
54+
const selfAttestationValue = this.selfAtt?.productSpecCharacteristicValue?.[0]?.value ?? '';
55+
56+
rows.push({
57+
type: 'Self attestation',
58+
name: 'Self attestation',
59+
value: selfAttestationValue,
60+
missing: !selfAttestationValue
61+
});
62+
63+
for (const certification of this.selectedISOS) {
64+
const value = certification?.url ?? '';
65+
rows.push({
66+
type: 'Third-party certification',
67+
name: this.normalizeName(certification?.name),
68+
value,
69+
missing: !value
70+
});
71+
}
72+
73+
for (const certification of this.additionalISOS) {
74+
const value = certification?.url ?? '';
75+
rows.push({
76+
type: 'Additional certification',
77+
name: this.normalizeName(certification?.name),
78+
value,
79+
missing: !value
80+
});
81+
}
82+
83+
return rows;
84+
}
85+
86+
normalizeName(name?: string): string {
87+
return name?.replace(/compliance:/i, '').trim() ?? '';
88+
}
89+
90+
isHttpUrl(value?: string): boolean {
91+
return /^https?:\/\//i.test(value ?? '');
92+
}
93+
94+
hasMissingOrganizationInfo(): boolean {
95+
return [
96+
this.organizationInfo.organizationName,
97+
this.organizationInfo.organizationVat,
98+
this.organizationInfo.organizationAddress,
99+
this.organizationInfo.organizationCountry,
100+
this.organizationInfo.organizationWebsite,
101+
this.organizationInfo.organizationEmail
102+
].some((value) => this.isMissing(value));
103+
}
104+
105+
isMissing(value?: string): boolean {
106+
return !value || value.trim() === '';
107+
}
108+
109+
submitRequestValidation(): void {
110+
if (this.hasMissingOrganizationInfo() || this.requestValidationLoading) {
111+
return;
112+
}
113+
114+
if (!this.productSpec) {
115+
this.requestValidationError = 'Product specification information is not available.';
116+
return;
117+
}
118+
119+
this.requestValidationLoading = true;
120+
this.requestValidationError = '';
121+
122+
this.prodSpecService.requestComplianceCertificate(this.buildRequestPayload(this.productSpec)).subscribe({
123+
next: () => {
124+
this.requestValidationLoading = false;
125+
this.closeModal.emit();
126+
},
127+
error: (error) => {
128+
console.error('There was an error while requesting validation!', error);
129+
this.requestValidationLoading = false;
130+
this.requestValidationError = this.extractErrorMessage(error);
131+
}
132+
});
133+
}
134+
135+
private async loadOrganizationInfoFromCurrentSession(): Promise<void> {
136+
const organizationPartyId = this.getOrganizationPartyId();
137+
if (!organizationPartyId) {
138+
this.organizationInfo = this.getEmptyOrganizationInfo();
139+
return;
140+
}
141+
142+
try {
143+
const organization = await this.accountService.getOrgInfo(organizationPartyId);
144+
this.organizationInfo = this.mapOrganizationInfo(organization);
145+
} catch (error) {
146+
console.error('There was an error while loading organization info!', error);
147+
this.organizationInfo = this.getEmptyOrganizationInfo();
148+
}
149+
}
150+
151+
private getOrganizationPartyId(): string {
152+
const loginInfo = this.localStorage.getObject('login_items') as LoginInfo;
153+
154+
if (!loginInfo || JSON.stringify(loginInfo) === '{}') {
155+
return '';
156+
}
157+
158+
if (loginInfo.logged_as === loginInfo.id) {
159+
return '';
160+
}
161+
162+
const organizations = Array.isArray(loginInfo.organizations) ? loginInfo.organizations : [];
163+
const loggedOrganization = organizations.find((organization: any) => organization?.id == loginInfo.logged_as);
164+
return loggedOrganization?.partyId ?? '';
165+
}
166+
167+
private mapOrganizationInfo(organization: any): OrganizationInfo {
168+
const contactMedium = Array.isArray(organization?.contactMedium) ? organization.contactMedium : [];
169+
const partyCharacteristics = Array.isArray(organization?.partyCharacteristic) ? organization.partyCharacteristic : [];
170+
const externalReferences = Array.isArray(organization?.externalReference) ? organization.externalReference : [];
171+
172+
const emailMedium = contactMedium.find((medium: any) => medium?.mediumType === 'Email');
173+
const postalMedium = contactMedium.find((medium: any) => medium?.mediumType === 'PostalAddress');
174+
const postalCharacteristic = postalMedium?.characteristic ?? {};
175+
const characteristicCountry = this.getPartyCharacteristicValue(partyCharacteristics, 'country');
176+
177+
return {
178+
organizationName: organization?.tradingName ?? organization?.name ?? '',
179+
organizationVat: this.getOrganizationVat(externalReferences),
180+
organizationAddress: this.formatPostalAddress(postalCharacteristic),
181+
organizationCountry: characteristicCountry,
182+
organizationWebsite: this.getPartyCharacteristicValue(partyCharacteristics, 'website'),
183+
organizationEmail: emailMedium?.characteristic?.emailAddress ?? ''
184+
};
185+
}
186+
187+
private getOrganizationVat(externalReferences: any[]): string {
188+
if (!externalReferences.length) {
189+
return '';
190+
}
191+
192+
const idmReference = externalReferences.find((reference: any) =>
193+
(reference?.externalReferenceType ?? '').toLowerCase() === 'idm_id'
194+
);
195+
196+
return idmReference?.name ?? '';
197+
}
198+
199+
private getPartyCharacteristicValue(characteristics: any[], key: string): string {
200+
const characteristic = characteristics.find((item: any) => item?.name === key);
201+
return characteristic?.value ?? '';
202+
}
203+
204+
private formatPostalAddress(address: any): string {
205+
if (!address) {
206+
return '';
207+
}
208+
209+
return [
210+
address.street1,
211+
address.postCode,
212+
address.city,
213+
address.stateOrProvince
214+
].filter((value: any) => !!value).join(', ');
215+
}
216+
217+
private getEmptyOrganizationInfo(): OrganizationInfo {
218+
return {
219+
organizationName: '',
220+
organizationVat: '',
221+
organizationAddress: '',
222+
organizationCountry: '',
223+
organizationWebsite: '',
224+
organizationEmail: ''
225+
};
226+
}
227+
228+
private buildRequestPayload(productSpec: any): any {
229+
return JSON.parse(JSON.stringify(productSpec));
230+
}
231+
232+
private extractErrorMessage(error: any): string {
233+
const message = error?.error?.error ?? error?.error?.message ?? error?.message;
234+
return message ? `Error: ${message}` : 'Error: there was a problem requesting the validation.';
235+
}
236+
}

0 commit comments

Comments
 (0)