|
|
@@ -0,0 +1,504 @@
|
|
|
+import { Store } from "@ngxs/store";
|
|
|
+import { TMService } from "tm-ui/tm.service";
|
|
|
+import { Controller } from "fis-commons/src/controller/controller";
|
|
|
+import { Field, Fields } from "fis-commons/src/ui/field/field.i";
|
|
|
+import documentSchema from '../qt.document.schema.json';
|
|
|
+import { BehaviorSubject, MonoTypeOperatorFunction, Observable, Subject, concat, firstValueFrom, lastValueFrom, map, skip, take, takeWhile } from "rxjs";
|
|
|
+import { Document } from "fis-commons/src/tm/document/tm.document.i";
|
|
|
+import { Table, UITable } from "angularlib/table/table.i";
|
|
|
+import { Event } from "fis-commons/src/event/event.i";
|
|
|
+import { TreeTableEvent } from "angularlib/table/tree.table/tree.table.e";
|
|
|
+import { Ops } from "dp-ui/message.interface";
|
|
|
+import { ComponentService } from "angularlib/component.service";
|
|
|
+import { ServiceID } from "../serviceid";
|
|
|
+import { Process } from "fis-commons/process";
|
|
|
+
|
|
|
+/**operator to decide whether to use cahced or live responses
|
|
|
+ * @param skipCached boolean, if undefined, default value is false, which returns cached data
|
|
|
+*/
|
|
|
+const skipCache = (skipCached?: boolean) => (source: Observable<any>) => {
|
|
|
+ if (skipCached) return source.pipe(skip(1),take(1)); // skip first response, which usually is the cached data, take following response from backend
|
|
|
+ return source.pipe(take(1)); //takes first response; either cached or live
|
|
|
+}
|
|
|
+
|
|
|
+export class QuotationListingController extends Controller {
|
|
|
+
|
|
|
+ protected tag: string = 'qttable';
|
|
|
+ protected submitted:boolean;
|
|
|
+
|
|
|
+ /**has submit submit / allow for submit */
|
|
|
+ protected submitable: boolean = true;
|
|
|
+
|
|
|
+ /** latest tender submission document */
|
|
|
+ protected submittedItems: {[key:string]: Document};
|
|
|
+
|
|
|
+ private _table: BehaviorSubject<UITable> = new BehaviorSubject({
|
|
|
+ fields: [],
|
|
|
+ data: []
|
|
|
+ });
|
|
|
+
|
|
|
+ protected options: Table.Options = {
|
|
|
+ showAppendControls: false
|
|
|
+ }
|
|
|
+
|
|
|
+ protected eventListener: Subject<Event> = new Subject();
|
|
|
+
|
|
|
+ protected process: Process = new Process();
|
|
|
+
|
|
|
+ private activeEntryNo;
|
|
|
+ private errors = [];
|
|
|
+ private successMessages = [];
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ constructor(
|
|
|
+ private store: Store,
|
|
|
+ private tms: TMService,
|
|
|
+ private cs: ComponentService,
|
|
|
+ private fieldColumns: any[],
|
|
|
+ public document: Document,
|
|
|
+ private documentCache: Document[],
|
|
|
+ private untilDestroy?: MonoTypeOperatorFunction<any>
|
|
|
+ ) {
|
|
|
+ super();
|
|
|
+ if (!this.document.editState) this.document.editState = "VIEW";
|
|
|
+
|
|
|
+ // get the document header data, check if document had been posted
|
|
|
+ this.tms.data.get({
|
|
|
+ serviceId: ServiceID.DocumentHeaderData,
|
|
|
+ parameter: `id=${this.document.docId}`
|
|
|
+ },this.process).pipe(skipCache(true)).subscribe(res => {
|
|
|
+ const data = res.data?.GenericFisData?.data?.DataService?.rows?.row[0]?.column;
|
|
|
+ this.generateFields(fieldColumns);
|
|
|
+ this.update().then((res) => {
|
|
|
+ this.table = {
|
|
|
+ ...this.table,
|
|
|
+ data: res
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ generateFields(fieldColumns) {
|
|
|
+ let fields = new Fields([]);
|
|
|
+ let tempColumns = {};
|
|
|
+ documentSchema.details.visibleFields.forEach(key => {
|
|
|
+ if (!fieldColumns) window.location.reload();
|
|
|
+ let column = fieldColumns[key];
|
|
|
+ tempColumns = {
|
|
|
+ ...tempColumns,
|
|
|
+ [key]: column
|
|
|
+ };
|
|
|
+ });
|
|
|
+ fields = new Fields(this.tms.fields.convert(tempColumns));
|
|
|
+ fields.map((f: Field) => {
|
|
|
+ f.scope = f.key;
|
|
|
+ if (f.type === 'number') {f.attributes.align='right';f.attributes.decimal='2'};
|
|
|
+ if (f.key === 'prd_uom') {f.attributes.align='center';};
|
|
|
+ if (f.key === 'prd_qty_credit') {f.attributes.decimal='0';};
|
|
|
+ if (f.key === 'entry_ref') {f.label= {key:'prd_code',default:'Item Code'}};
|
|
|
+ if (f.key === 'prd_price') {f.attributes.hint = {key:'enter_unit_price',default:'Please enter unit price'}};
|
|
|
+ if (f.key === 'prd_price' && this.submitted === true) {f.attributes.readonly = true};
|
|
|
+ if (f.type === 'date' || f.type === 'datetime') f.attributes.dateFormat = 'yyyy-MM-dd';
|
|
|
+ if (this.document.notEditable()) f.attributes.readonly = true;
|
|
|
+ });
|
|
|
+ this.table.fields = fields;
|
|
|
+ this.table.editColDef = JSON.parse(JSON.stringify(documentSchema.details.editColDef));
|
|
|
+ return fields;
|
|
|
+ }
|
|
|
+
|
|
|
+ override update(skipCached?: boolean): Promise<any> {
|
|
|
+ return new Promise<any>((resolve,reject) => {
|
|
|
+ // get quoted items listed by tenderee (tenderee offers contract)
|
|
|
+ const getInviteItems = this.tms.data.get({
|
|
|
+ serviceId: ServiceID.DetailData,
|
|
|
+ parameter: '__clearSearchValue__=true,doctypecodelist=S2'
|
|
|
+ },this.process,{caching:"false"}).pipe(takeWhile(res => res !== undefined), // skip undefined responses
|
|
|
+ skipCache(skipCached),
|
|
|
+ map((res:any) => {
|
|
|
+ const rows = res?.data?.GenericFisData?.data?.DataService?.rows?.row?.
|
|
|
+ filter(x => x.column.doc_id === this.document.docId)?.
|
|
|
+ map(x => {return x.column});
|
|
|
+ return rows?rows:[];
|
|
|
+ }));
|
|
|
+
|
|
|
+ // get quoted items submitted by current tenderer (tenderer submits quotation offered by tenderee)
|
|
|
+ const getSubmittedItems = this.tms.data.get({
|
|
|
+ serviceId: ServiceID.DetailData,
|
|
|
+ parameter: '__clearSearchValue__=true,doctypecodelist=S3'
|
|
|
+ },null,{caching:"false"}).pipe(takeWhile(res => res !== undefined), // skip undefined responses
|
|
|
+ skipCache(skipCached),
|
|
|
+ map((res:any) => {
|
|
|
+ let rows = res?.data?.GenericFisData?.data?.DataService?.rows?.row?.
|
|
|
+ filter(x => x.column.allocatedtodocid === this.document.docId)?.
|
|
|
+ map(x => {return {...x.column,rowId:x.rowId,rowNumber:x.rowNumber}});
|
|
|
+ if (rows?.length > 0) {
|
|
|
+ const latestSubmission = rows.reduce((prev,curr) => {
|
|
|
+ const prevDate = new Date(prev.doc_dt);
|
|
|
+ const currDate = new Date(curr.doc_dt);
|
|
|
+ return (prevDate > currDate)?prev:curr
|
|
|
+ });
|
|
|
+ this.submittedItems = rows.filter(x => x.doc_id === latestSubmission.doc_id)?.reduce((prev,curr) => {
|
|
|
+ prev[curr.allocatedtodocentryno] = prev[curr.allocatedtodocentryno] || [];
|
|
|
+ prev[curr.allocatedtodocentryno].push(curr);
|
|
|
+ return prev;
|
|
|
+ },{});
|
|
|
+ }
|
|
|
+ return rows?rows:[];
|
|
|
+ }));
|
|
|
+
|
|
|
+ getInviteItems.pipe(map(async inviteItems => {
|
|
|
+ await firstValueFrom(getSubmittedItems).catch(error => console.error(error)).then(submittedItems => {});
|
|
|
+ inviteItems = inviteItems.map(item => {
|
|
|
+ if (this.submittedItems && this.submittedItems[item.entry_no_a0]) {
|
|
|
+ item = {...item,prd_price_original:item.prd_price,prd_price:this.submittedItems[item.entry_no_a0][0].prd_price,
|
|
|
+ entry_cur_credit:this.submittedItems[item.entry_no_a0][0].entry_cur_credit
|
|
|
+ };
|
|
|
+ }
|
|
|
+ return item;
|
|
|
+ });
|
|
|
+ return inviteItems;
|
|
|
+ })).subscribe({
|
|
|
+ next: promise => {
|
|
|
+ promise.catch(error => reject(error)).then(res => {
|
|
|
+ res.map(row => {
|
|
|
+ const prd_price = parseFloat(row.prd_price)?.toFixed(2);
|
|
|
+ if (!row.prd_price_original) row.prd_price_original = 0;
|
|
|
+ const prd_price_original = parseFloat(row.prd_price_original)?.toFixed(2);
|
|
|
+ row.prd_price_hint = "Original requested price: "+prd_price_original;
|
|
|
+ row.prd_uom = row.prd_package_name;
|
|
|
+ row.prd_qty_credit = parseInt(row.prd_qty_credit);
|
|
|
+ row.prd_price = prd_price;
|
|
|
+ row.entry_cur_credit = row.prd_price * row.prd_qty_credit,
|
|
|
+ row.checked = false; //set default checkbox state to unchecked;
|
|
|
+ row.checkbox_hint = 'check to bid';
|
|
|
+ });
|
|
|
+ let data: any[] = res;
|
|
|
+ resolve(data);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ protected onChangeListener(event: Event) {
|
|
|
+ switch (event.name) {
|
|
|
+ case TreeTableEvent.ValueChange().name: {
|
|
|
+ this.onValueChangeEvent(event);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case TreeTableEvent.InputFocus().name: {
|
|
|
+ this.errors = [];
|
|
|
+ switch (true) {
|
|
|
+ // no existing tender submission document:
|
|
|
+ case this.document.editState === "VIEW" && !this.submittedItems : {
|
|
|
+ this.sendNewCommand()?.then(newRes => {
|
|
|
+ this.sendExecuteCommand(event.payload);
|
|
|
+ });
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ // has existing tender submission document:
|
|
|
+ case this.document.editState === "VIEW" && this.submittedItems !== undefined : {
|
|
|
+ this.sendExecuteCommand(event.payload);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ // when another row is selected:
|
|
|
+ case (this.document.editState === 'NEW' || this.document.editState === 'MODIFY') &&
|
|
|
+ event.payload?.entry_no_a0 !== this.activeEntryNo &&
|
|
|
+ !this.process.hasActiveProcesses: {
|
|
|
+ this.sendExecuteCommand(event.payload);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ default: break;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ default: break;
|
|
|
+ }
|
|
|
+ //this.eventListener.next(event);
|
|
|
+ }
|
|
|
+
|
|
|
+ sendNewCommand() {
|
|
|
+ const NEW = new Observable(observer => {
|
|
|
+ const processId = this.process.addActiveProcess('New');
|
|
|
+ this.tms.command({
|
|
|
+ serviceId: ServiceID.Submission,
|
|
|
+ operation: Ops.NEW,
|
|
|
+ payload: {
|
|
|
+ serviceId: ServiceID.Submission,
|
|
|
+ }
|
|
|
+ }).pipe(take(1)).subscribe({
|
|
|
+ next: res => {
|
|
|
+ this.process.endActiveProcess(processId);
|
|
|
+ this.document.editState = "NEW";
|
|
|
+ this.document.submitted = false;
|
|
|
+ observer.next(res);
|
|
|
+ observer.complete();
|
|
|
+ },
|
|
|
+ error: error => {
|
|
|
+ observer.error(error);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ const SET_TENDERER_ID = new Observable(observer => {
|
|
|
+ const processId = this.process.addActiveProcess('SetColumn - tendererid');
|
|
|
+ this.tms.setColumn({
|
|
|
+ serviceId: ServiceID.Submission,
|
|
|
+ alias: 'header',
|
|
|
+ column: {
|
|
|
+ row: 1,
|
|
|
+ name: 'tendererid',
|
|
|
+ value: this.document.tendererId
|
|
|
+ }
|
|
|
+ }).pipe(take(1)).subscribe({
|
|
|
+ next: res => {
|
|
|
+ this.process.endActiveProcess(processId);
|
|
|
+ observer.next(res);
|
|
|
+ observer.complete();
|
|
|
+ },
|
|
|
+ error: error => {
|
|
|
+ this.process.endActiveProcess(processId);
|
|
|
+ observer.error(error);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ return !this.submitted?lastValueFrom(concat(NEW, SET_TENDERER_ID)):null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**retrieve tender submission document
|
|
|
+ * @param {{doc_id,entry_no_a0}} payload
|
|
|
+ * @param {string} [parameter] Execute parameter
|
|
|
+ */
|
|
|
+ sendExecuteCommand(payload, parameter?: string) {
|
|
|
+ if (!parameter) parameter = `eventname=ue_trigger_copy,doc_id=${payload.doc_id},entry_no=${payload.entry_no_a0}`;
|
|
|
+ this.submitted = true;
|
|
|
+ this.activeEntryNo = payload.entry_no_a0;
|
|
|
+ const EXECUTE = new Observable(observer => {
|
|
|
+ const processId = this.process.addActiveProcess('Execute');
|
|
|
+ this.tms.command({
|
|
|
+ serviceId: ServiceID.Submission,
|
|
|
+ operation: Ops.Execute,
|
|
|
+ messageName: 'Retrieve Tender Submission',
|
|
|
+ payload:{
|
|
|
+ //parameter: `objecttype=TM,eventname=ue_trigger_copy,doc_id=836569,entry_no=2`,
|
|
|
+ serviceId: ServiceID.Submission,
|
|
|
+ parameter: `objecttype=TM,${parameter}`,
|
|
|
+ dataRow: null
|
|
|
+ }
|
|
|
+ }).pipe(take(1)).subscribe({
|
|
|
+ next: res => {
|
|
|
+ this.process.endActiveProcess(processId);
|
|
|
+ observer.complete();
|
|
|
+ },
|
|
|
+ error: error => {
|
|
|
+ observer.error(error);
|
|
|
+ observer.complete();
|
|
|
+ this.process.endActiveProcess(processId);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ });
|
|
|
+ const GET_DATA_AFTER_EXECUTE = new Observable(observer => {
|
|
|
+ const processId = this.process.addActiveProcess('GetData after Execute');
|
|
|
+ this.tms.query(
|
|
|
+ ServiceID.Submission
|
|
|
+ ).pipe(take(1)).subscribe({
|
|
|
+ next: res => {
|
|
|
+ this.process.endActiveProcess(processId);
|
|
|
+ observer.complete();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ concat(EXECUTE,GET_DATA_AFTER_EXECUTE).subscribe();
|
|
|
+ }
|
|
|
+
|
|
|
+ sendCancelChangesCommand(): Promise<unknown> {
|
|
|
+ this.errors = [];
|
|
|
+ this.successMessages = [];
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ const processId = this.process.addActiveProcess('Cancel Changes');
|
|
|
+ this.tms.command({
|
|
|
+ serviceId: ServiceID.Submission,
|
|
|
+ operation: Ops.CANCEL_CHANGES,
|
|
|
+ payload: {
|
|
|
+ serviceId: ServiceID.Submission,
|
|
|
+ }
|
|
|
+ }).pipe(take(1)).subscribe({
|
|
|
+ next: res => {
|
|
|
+ this.documentCache.map(doc => {
|
|
|
+ if (doc.docId !== this.document.docId) {
|
|
|
+ doc.listingController.document.editState = 'VIEW';
|
|
|
+ }
|
|
|
+ return doc;
|
|
|
+ });
|
|
|
+ this.process.endActiveProcess(processId);
|
|
|
+ resolve(res);
|
|
|
+ },
|
|
|
+ error: error => reject(error)
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private onValueChangeEvent(event: Event) {
|
|
|
+ if (this.document.editState !== 'VIEW') new Observable(observer => {
|
|
|
+ const processId = this.process.addActiveProcess(`SetColumn - ${event.payload?.field}`);
|
|
|
+ this.tms.setColumn({
|
|
|
+ serviceId: ServiceID.Submission,
|
|
|
+ alias: 'details',
|
|
|
+ column: {
|
|
|
+ row: event.payload?.entry_no_a0,
|
|
|
+ name: event.payload?.field,
|
|
|
+ value: event.payload[event.payload?.field]
|
|
|
+ }
|
|
|
+ }).pipe(take(1)).subscribe({
|
|
|
+ next: res => {
|
|
|
+ this.setAmount(event.payload).then(amount => {
|
|
|
+ observer.complete();
|
|
|
+ const data = this.table.data;
|
|
|
+ data.map(row => {
|
|
|
+ if (row.entry_no_a0 === event.payload?.entry_no_a0) {
|
|
|
+ row[event.payload?.field+'_error'] = undefined;
|
|
|
+ row.entry_cur_credit = amount;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ this.table = {
|
|
|
+ ...this.table,
|
|
|
+ data: data
|
|
|
+ }
|
|
|
+ this.process.endActiveProcess(processId);
|
|
|
+ });
|
|
|
+ },
|
|
|
+ error: error => {
|
|
|
+ this.process.endActiveProcess(processId);
|
|
|
+ const data = this.table.data;
|
|
|
+ data.map(row => {
|
|
|
+ if (row.entry_no_a0 === event.payload?.entry_no_a0) {
|
|
|
+ row[event.payload?.field+'_error'] = error.message;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ this.table = {
|
|
|
+ ...this.table,
|
|
|
+ data: data
|
|
|
+ }
|
|
|
+ observer.error(error)
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }).subscribe();
|
|
|
+ }
|
|
|
+
|
|
|
+ onSave() {
|
|
|
+ this.errors = [];
|
|
|
+ const processId = this.process.addActiveProcess('Save');
|
|
|
+ new Promise((resolve, reject) => {
|
|
|
+ this.tms.command({
|
|
|
+ serviceId: ServiceID.Submission,
|
|
|
+ operation: Ops.SAVE
|
|
|
+ }).pipe(take(1)).subscribe({
|
|
|
+ next: save => {
|
|
|
+ this.document.editState = 'VIEW';
|
|
|
+ this.process.endActiveProcess(processId);
|
|
|
+ this.successMessages = [this.cs.getLabel('saved','Saved')];
|
|
|
+ // this.update(true).then(res => {
|
|
|
+ // this.table = {
|
|
|
+ // ...this.table,
|
|
|
+ // data: res
|
|
|
+ // }
|
|
|
+ // });
|
|
|
+ resolve(save);
|
|
|
+
|
|
|
+ },
|
|
|
+ error: error => reject(error)
|
|
|
+ });
|
|
|
+
|
|
|
+ }).catch(error => {
|
|
|
+ console.error(error);
|
|
|
+ this.process.endActiveProcess(processId);
|
|
|
+ this.errors.push(error.message);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ onSubmit() {
|
|
|
+ this.errors = [];
|
|
|
+ this.successMessages = [];
|
|
|
+ const POST = new Observable<any>(observer => {
|
|
|
+ const processId = this.process.addActiveProcess(`Post (${ServiceID.Submission})`);
|
|
|
+ this.tms.command({
|
|
|
+ serviceId: ServiceID.Submission,
|
|
|
+ operation: Ops.POST
|
|
|
+ }).pipe(take(1)).subscribe({
|
|
|
+ next: res => {
|
|
|
+ observer.next(res);
|
|
|
+ this.process.endActiveProcess(processId);
|
|
|
+ this.document.editState = "VIEW";
|
|
|
+ this.document.submitted = true;
|
|
|
+ let fields: Fields = new Fields(this.table.fields);
|
|
|
+ let priceF: Field = fields.find(x => x.key === 'prd_price');
|
|
|
+ fields.replace('prd_price',{
|
|
|
+ ...priceF,
|
|
|
+ attributes:{...priceF.attributes,readonly:true}
|
|
|
+ });
|
|
|
+ this.table = {
|
|
|
+ ...this.table,
|
|
|
+ fields: fields
|
|
|
+ };
|
|
|
+ observer.complete();
|
|
|
+ },
|
|
|
+ error: error => {
|
|
|
+ this.process.endActiveProcess(processId);
|
|
|
+ observer.error(error);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ this.cs.dialog.showComfirmDialog({
|
|
|
+ title: 'Submit',
|
|
|
+ content: 'Do you want to submit this tender?<div><i><small><b>You\'ll no longer be able to edit this tender after submitting.</b></small></i></div>',
|
|
|
+ confirm: () => {
|
|
|
+ POST.subscribe({
|
|
|
+ next: res => {
|
|
|
+ this.successMessages = [this.cs.getLabel('submitted','Submitted')];
|
|
|
+ },
|
|
|
+ error: error => this.errors = [error.message]
|
|
|
+ })
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private setAmount(payload) {
|
|
|
+ return new Promise<number>((resolve, reject) => {
|
|
|
+ const amount = +payload.prd_price * payload.prd_qty_credit;
|
|
|
+ const found = this.table.data.find(x => x.allocatedToDocEntryNo === payload.allocatedToDocEntryNo);
|
|
|
+ if (this.document.editState !== 'VIEW') new Observable(observer => {
|
|
|
+ const processId = this.process.addActiveProcess(`SetColumn - entry_cur_credit`);
|
|
|
+ this.tms.setColumn({
|
|
|
+ serviceId: ServiceID.Submission,
|
|
|
+ alias: 'details',
|
|
|
+ column: {
|
|
|
+ row: payload?.entry_no_a0,
|
|
|
+ name: 'entry_cur_credit',
|
|
|
+ value: amount
|
|
|
+ }
|
|
|
+ }).pipe(take(1)).subscribe({
|
|
|
+ next: res => {
|
|
|
+ observer.complete();
|
|
|
+ this.process.endActiveProcess(processId);
|
|
|
+ },
|
|
|
+ error: error => observer.error(error)
|
|
|
+ })
|
|
|
+ }).subscribe();
|
|
|
+ resolve(amount);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ get table() {
|
|
|
+ return this._table.getValue();
|
|
|
+ }
|
|
|
+
|
|
|
+ set table(table) {
|
|
|
+ this._table.next(table);
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+}
|