Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ import { QuotesModule } from "src/app/features/quotes/quotes.module"
import { MarkdownTextareaComponent } from "src/app/shared/forms/markdown-textarea/markdown-textarea.component"
import { ProviderRevenueSharingComponent } from "src/app/pages/user-profile/profile-sections/provider-revenue-sharing/provider-revenue-sharing.component"
import { OperatorRevenueSharingComponent } from "src/app/pages/admin/operator-revenue-sharing/operator-revenue-sharing.component"
import { RequestValidationModalComponent } from './pages/seller-offerings/offerings/seller-product-spec/update-product-spec/request-validation-modal/request-validation-modal.component';

@NgModule({
declarations: [
Expand Down Expand Up @@ -162,7 +163,8 @@ import { OperatorRevenueSharingComponent } from "src/app/pages/admin/operator-re
OrganizationDetailsComponent,
FaqComponent,
NewPricePlanComponent,
UpdatePricePlanComponent
UpdatePricePlanComponent,
RequestValidationModalComponent
],
imports: [
BrowserModule,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<div class="fixed inset-0 bg-black bg-opacity-50 flex justify-end transition-opacity duration-500 z-50"
style="opacity: 0; pointer-events: none;"
[style.opacity]="showModal ? '1' : '0'"
[style.pointer-events]="showModal ? 'auto' : 'none'"
(click)="closeModal.emit()">
<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"
style="transform: translateX(100%);"
[style.transform]="showModal ? 'translateX(0)' : 'translateX(100%)'"
(click)="$event.stopPropagation();">
<div class="flex justify-between items-center">
<h3 class="text-2xl font-semibold text-primary-100 dark:text-white">Request Validation</h3>
<button type="button"
(click)="closeModal.emit()"
class="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
<svg class="min-w-4 w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<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"/>
</svg>
<span class="sr-only">Close drawer</span>
</button>
</div>

<p class="mt-2 mb-5 text-sm text-gray-500 dark:text-gray-400">
Requesting a validation will submit your certificates and self attestation to the compliance service for validation.
</p>

<div class="mb-6">
<h4 class="mb-2 font-semibold text-gray-900 dark:text-white">Evidence to be submitted</h4>
<div class="relative overflow-x-auto shadow-md sm:rounded-lg w-full bg-white dark:bg-secondary-300">
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-200">
<thead class="text-xs text-gray-700 uppercase bg-gray-100 dark:bg-secondary-200 dark:text-white">
<tr>
<th scope="col" class="px-6 py-3">Type</th>
<th scope="col" class="px-6 py-3">Name</th>
<th scope="col" class="px-6 py-3">Document</th>
</tr>
</thead>
<tbody>
@for (evidence of validationEvidenceRows; track evidence.type + evidence.name + evidence.value) {
<tr class="border-b hover:bg-gray-200 dark:bg-secondary-300 dark:border-gray-700 dark:hover:bg-secondary-200">
<td class="px-6 py-4">{{ evidence.type }}</td>
<td class="px-6 py-4">{{ evidence.name }}</td>
<td class="px-6 py-4 break-all">
@if (evidence.missing) {
<span class="font-medium text-red-600">Not provided</span>
} @else if (isHttpUrl(evidence.value)) {
<a [href]="evidence.value" target="_blank" class="font-medium text-blue-600 dark:text-blue-500 hover:underline">
{{ evidence.value }}
</a>
} @else {
{{ evidence.value }}
}
</td>
</tr>
} @empty {
<tr>
<td colspan="3" class="px-6 py-4 text-center text-gray-500 dark:text-gray-300">
No compliance evidence available.
</td>
</tr>
}
</tbody>
</table>
</div>
</div>

<div class="mb-6">
<h4 class="mb-2 font-semibold text-gray-900 dark:text-white">Organization info</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="p-4 bg-white border rounded-lg dark:bg-secondary-300"
[ngClass]="isMissing(organizationInfo.organizationName) ? 'border-red-500 dark:border-red-500' : 'border-gray-200 dark:border-gray-700'">
<p class="text-xs uppercase text-gray-500 dark:text-gray-300">Organization name</p>
<p class="mt-1 text-sm text-gray-900 dark:text-white break-all">{{ organizationInfo.organizationName || '-' }}</p>
</div>
<div class="p-4 bg-white border rounded-lg dark:bg-secondary-300"
[ngClass]="isMissing(organizationInfo.organizationVat) ? 'border-red-500 dark:border-red-500' : 'border-gray-200 dark:border-gray-700'">
<p class="text-xs uppercase text-gray-500 dark:text-gray-300">Organization VAT</p>
<p class="mt-1 text-sm text-gray-900 dark:text-white break-all">{{ organizationInfo.organizationVat || '-' }}</p>
</div>
<div class="p-4 bg-white border rounded-lg dark:bg-secondary-300"
[ngClass]="isMissing(organizationInfo.organizationAddress) ? 'border-red-500 dark:border-red-500' : 'border-gray-200 dark:border-gray-700'">
<p class="text-xs uppercase text-gray-500 dark:text-gray-300">Organization address</p>
<p class="mt-1 text-sm text-gray-900 dark:text-white break-all">{{ organizationInfo.organizationAddress || '-' }}</p>
</div>
<div class="p-4 bg-white border rounded-lg dark:bg-secondary-300"
[ngClass]="isMissing(organizationInfo.organizationCountry) ? 'border-red-500 dark:border-red-500' : 'border-gray-200 dark:border-gray-700'">
<p class="text-xs uppercase text-gray-500 dark:text-gray-300">Organization country</p>
<p class="mt-1 text-sm text-gray-900 dark:text-white break-all">{{ organizationInfo.organizationCountry || '-' }}</p>
</div>
<div class="p-4 bg-white border rounded-lg dark:bg-secondary-300"
[ngClass]="isMissing(organizationInfo.organizationWebsite) ? 'border-red-500 dark:border-red-500' : 'border-gray-200 dark:border-gray-700'">
<p class="text-xs uppercase text-gray-500 dark:text-gray-300">Organization website</p>
<p class="mt-1 text-sm text-gray-900 dark:text-white break-all">{{ organizationInfo.organizationWebsite || '-' }}</p>
</div>
<div class="p-4 bg-white border rounded-lg dark:bg-secondary-300"
[ngClass]="isMissing(organizationInfo.organizationEmail) ? 'border-red-500 dark:border-red-500' : 'border-gray-200 dark:border-gray-700'">
<p class="text-xs uppercase text-gray-500 dark:text-gray-300">Organization email</p>
<p class="mt-1 text-sm text-gray-900 dark:text-white break-all">{{ organizationInfo.organizationEmail || '-' }}</p>
</div>
</div>
</div>

@if (hasMissingOrganizationInfo()) {
<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">
Some information of your organization required for the validation process is missing. Please update you organization profile.
</div>
}
@if (requestValidationError) {
<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">
{{ requestValidationError }}
</div>
}

<div class="flex justify-end items-center space-x-4">
<button (click)="closeModal.emit()"
type="button"
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">
Cancel
</button>
<button (click)="submitRequestValidation()"
[disabled]="hasMissingOrganizationInfo() || requestValidationLoading || !productSpec"
type="button"
[ngClass]="(hasMissingOrganizationInfo() || requestValidationLoading || !productSpec) ? 'opacity-50 cursor-not-allowed' : 'hover:bg-primary-50'"
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">
Request Validation
</button>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { LoginInfo } from 'src/app/models/interfaces';
import { AccountServiceService } from 'src/app/services/account-service.service';
import { LocalStorageService } from 'src/app/services/local-storage.service';
import { ProductSpecServiceService } from 'src/app/services/product-spec-service.service';

type ValidationEvidenceRow = {
type: string;
name: string;
value: string;
missing: boolean;
};
type OrganizationInfo = {
organizationName: string;
organizationVat: string;
organizationAddress: string;
organizationCountry: string;
organizationWebsite: string;
organizationEmail: string;
};

@Component({
selector: 'request-validation-modal',
templateUrl: './request-validation-modal.component.html',
styleUrl: './request-validation-modal.component.css'
})
export class RequestValidationModalComponent implements OnChanges {
@Input() showModal: boolean = false;
@Input() productSpec: any;
@Input() selfAtt: any;
@Input() selectedISOS: any[] = [];
@Input() additionalISOS: any[] = [];
requestValidationLoading: boolean = false;
requestValidationError: string = '';
organizationInfo: OrganizationInfo = this.getEmptyOrganizationInfo();

@Output() closeModal = new EventEmitter<void>();

constructor(
private localStorage: LocalStorageService,
private accountService: AccountServiceService,
private prodSpecService: ProductSpecServiceService
) {}

ngOnChanges(changes: SimpleChanges): void {
if (changes['showModal']?.currentValue === true) {
this.requestValidationError = '';
this.loadOrganizationInfoFromCurrentSession();
}
}

get validationEvidenceRows(): ValidationEvidenceRow[] {
const rows: ValidationEvidenceRow[] = [];
const selfAttestationValue = this.selfAtt?.productSpecCharacteristicValue?.[0]?.value ?? '';

rows.push({
type: 'Self attestation',
name: 'Self attestation',
value: selfAttestationValue,
missing: !selfAttestationValue
});

for (const certification of this.selectedISOS) {
const value = certification?.url ?? '';
rows.push({
type: 'Third-party certification',
name: this.normalizeName(certification?.name),
value,
missing: !value
});
}

for (const certification of this.additionalISOS) {
const value = certification?.url ?? '';
rows.push({
type: 'Additional certification',
name: this.normalizeName(certification?.name),
value,
missing: !value
});
}

return rows;
}

normalizeName(name?: string): string {
return name?.replace(/compliance:/i, '').trim() ?? '';
}

isHttpUrl(value?: string): boolean {
return /^https?:\/\//i.test(value ?? '');
}

hasMissingOrganizationInfo(): boolean {
return [
this.organizationInfo.organizationName,
this.organizationInfo.organizationVat,
this.organizationInfo.organizationAddress,
this.organizationInfo.organizationCountry,
this.organizationInfo.organizationWebsite,
this.organizationInfo.organizationEmail
].some((value) => this.isMissing(value));
}

isMissing(value?: string): boolean {
return !value || value.trim() === '';
}

submitRequestValidation(): void {
if (this.hasMissingOrganizationInfo() || this.requestValidationLoading) {
return;
}

if (!this.productSpec) {
this.requestValidationError = 'Product specification information is not available.';
return;
}

this.requestValidationLoading = true;
this.requestValidationError = '';

this.prodSpecService.requestComplianceCertificate(this.buildRequestPayload(this.productSpec)).subscribe({
next: () => {
this.requestValidationLoading = false;
this.closeModal.emit();
},
error: (error) => {
console.error('There was an error while requesting validation!', error);
this.requestValidationLoading = false;
this.requestValidationError = this.extractErrorMessage(error);
}
});
}

private async loadOrganizationInfoFromCurrentSession(): Promise<void> {
const organizationPartyId = this.getOrganizationPartyId();
if (!organizationPartyId) {
this.organizationInfo = this.getEmptyOrganizationInfo();
return;
}

try {
const organization = await this.accountService.getOrgInfo(organizationPartyId);
this.organizationInfo = this.mapOrganizationInfo(organization);
} catch (error) {
console.error('There was an error while loading organization info!', error);
this.organizationInfo = this.getEmptyOrganizationInfo();
}
}

private getOrganizationPartyId(): string {
const loginInfo = this.localStorage.getObject('login_items') as LoginInfo;

if (!loginInfo || JSON.stringify(loginInfo) === '{}') {
return '';
}

if (loginInfo.logged_as === loginInfo.id) {
return '';
}

const organizations = Array.isArray(loginInfo.organizations) ? loginInfo.organizations : [];
const loggedOrganization = organizations.find((organization: any) => organization?.id == loginInfo.logged_as);
return loggedOrganization?.partyId ?? '';
}

private mapOrganizationInfo(organization: any): OrganizationInfo {
const contactMedium = Array.isArray(organization?.contactMedium) ? organization.contactMedium : [];
const partyCharacteristics = Array.isArray(organization?.partyCharacteristic) ? organization.partyCharacteristic : [];
const externalReferences = Array.isArray(organization?.externalReference) ? organization.externalReference : [];

const emailMedium = contactMedium.find((medium: any) => medium?.mediumType === 'Email');
const postalMedium = contactMedium.find((medium: any) => medium?.mediumType === 'PostalAddress');
const postalCharacteristic = postalMedium?.characteristic ?? {};
const characteristicCountry = this.getPartyCharacteristicValue(partyCharacteristics, 'country');

return {
organizationName: organization?.tradingName ?? organization?.name ?? '',
organizationVat: this.getOrganizationVat(externalReferences),
organizationAddress: this.formatPostalAddress(postalCharacteristic),
organizationCountry: characteristicCountry,
organizationWebsite: this.getPartyCharacteristicValue(partyCharacteristics, 'website'),
organizationEmail: emailMedium?.characteristic?.emailAddress ?? ''
};
}

private getOrganizationVat(externalReferences: any[]): string {
if (!externalReferences.length) {
return '';
}

const idmReference = externalReferences.find((reference: any) =>
(reference?.externalReferenceType ?? '').toLowerCase() === 'idm_id'
);

return idmReference?.name ?? '';
}

private getPartyCharacteristicValue(characteristics: any[], key: string): string {
const characteristic = characteristics.find((item: any) => item?.name === key);
return characteristic?.value ?? '';
}

private formatPostalAddress(address: any): string {
if (!address) {
return '';
}

return [
address.street1,
address.postCode,
address.city,
address.stateOrProvince
].filter((value: any) => !!value).join(', ');
}

private getEmptyOrganizationInfo(): OrganizationInfo {
return {
organizationName: '',
organizationVat: '',
organizationAddress: '',
organizationCountry: '',
organizationWebsite: '',
organizationEmail: ''
};
}

private buildRequestPayload(productSpec: any): any {
return JSON.parse(JSON.stringify(productSpec));
}

private extractErrorMessage(error: any): string {
const message = error?.error?.error ?? error?.error?.message ?? error?.message;
return message ? `Error: ${message}` : 'Error: there was a problem requesting the validation.';
}
}
Loading
Loading