diff --git a/README.md b/README.md index 8d9145d..5de1a6b 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,7 @@ The following is a list of test statistics for the project by date and commit | 2023-01-04 | [359948b](https://github.com/stamp-web/stamp-web-aurelia/commit/359948b689f088ec8c8554044cab96c24ffe1a77) | 107 | 18.80% | | 2023-09-21 | [4412730](https://github.com/stamp-web/stamp-web-aurelia/commit/441273055dc1af57257aba29f929923799563325) | 123 | 20.27% | | 2023-10-08 | [054cb00](https://github.com/stamp-web/stamp-web-aurelia/commit/054cb004b15133867f98c10276397a1f2a1f88be) | 134 | 20.68% | +| 2023-11-01 | | 160 | 22.38% | ## Optimizing for Browsers diff --git a/resources/locales/en/stamp-web.json b/resources/locales/en/stamp-web.json index e64ffe2..c13808a 100644 --- a/resources/locales/en/stamp-web.json +++ b/resources/locales/en/stamp-web.json @@ -78,10 +78,13 @@ "description": "Description", "details-title": "Details", "filtering-catalogue": "Filtering Catalogue", + "generate-report": "Generate Report", "grade": "Grade", "grade-select": "Select grade", "hide": "close", "issue": "Year of Issue", + "includeCountries": "Include Countries", + "includeNotes": "Include Notes", "name": "Name", "modify-existing": "Modify existing values", "modifyTimestamp": "Modified", @@ -100,7 +103,8 @@ "seller-select": "Select a seller", "stamp-collection": "Stamp collection", "stamp-collection-select": "Select a stamp collection", - "switchToCreate-hint": "Switch to create stamp" + "switchToCreate-hint": "Switch to create stamp", + "update-image-paths": "Update image paths" }, "filters": { "filter": "Filter", @@ -138,6 +142,7 @@ "nameRequired": "Name is required", "numberRequired": "Number is required", "numberInvalid": "Number exceeds the 25 character limit", + "stampCollectionRequired": "Stamp Collection is required", "resolveErrors": "Resolve issues to continue", "valueInvalid": "Value is not a number" }, @@ -211,6 +216,17 @@ "paging-toolbar": { "page": "Page" }, + "reports": { + "name": "Report Title", + "action": "Generate", + "condition": "Condition", + "country": "Country", + "defaultTitle": "Filtered Stamps", + "description": "Description", + "notes": "Notes", + "number": "Number", + "value": "Cat. Value" + }, "footer-statistics": { "catalogue-value": "Report on catalogue value", "purchased": "Report on price paid", diff --git a/src/events/event-managed.js b/src/events/event-managed.js index 8c44acc..eaad609 100644 --- a/src/events/event-managed.js +++ b/src/events/event-managed.js @@ -36,6 +36,7 @@ export const EventNames = { create: 'create', manageEntity: 'manage-entity', entityDelete: 'entity-delete', + generateReport: 'generate-report', selectEntity: 'select-entity', entityFilter: 'entity-filter', loadingStarted: 'loading-started', diff --git a/src/reports/report-helper.js b/src/reports/report-helper.js index a6235d0..979424a 100644 --- a/src/reports/report-helper.js +++ b/src/reports/report-helper.js @@ -16,7 +16,7 @@ import {LogManager} from 'aurelia-framework'; import {Condition, CurrencyCode} from '../util/common-models'; import {asCurrencyValueConverter} from '../resources/value-converters/as-currency-formatted'; - +import {I18N} from 'aurelia-i18n'; import * as reportStyles from './report-styles.json'; import _ from 'lodash'; @@ -25,14 +25,69 @@ const logger = LogManager.getLogger('report-helper'); export class ReportHelper { + countries = []; + static inject() { - return [ReportValueConverter]; + return [ReportValueConverter, I18N]; } - constructor(reportValueConverter) { + constructor(reportValueConverter, i18next) { this.converter = reportValueConverter; + this.i18next = i18next; } + buildReport(stamps, countries, reportValue, options) { + let reportModel = _.get(options, 'model', {}); + let title = _.get(reportModel, 'title', this.i18next.tr('reports.defaultTitle')); + let includeCountries = _.get(reportModel, 'includeCountries', false); + let includeNotes = _.get(reportModel, 'includeNotes', false); + + let columns = [ + {name: ' ', type: 'issues', value: 'stampOwnerships[0]'}, + {name: this.i18next.tr('reports.number'), type: 'catalogueNumber', value: 'activeCatalogueNumber.number'}, + {name: this.i18next.tr('reports.description'), type: 'text', value: 'rate', additional: ['description'], width: '*'}, + {name: this.i18next.tr('reports.condition'), type: 'condition', value: 'activeCatalogueNumber.condition'}, + { + name: this.i18next.tr('reports.value'), + type: 'currencyValue', + value: 'activeCatalogueNumber.value', + additional: ['activeCatalogueNumber.code'] + } + ]; + if(includeCountries) { + columns.splice(1,0, {name: this.i18next.tr('reports.country'), type: 'country', value: 'countryRef'}); + } + if(includeNotes) { + let index = includeCountries ? 4 : 3; + columns.splice(index,0, {name: this.i18next.tr('reports.notes'), type: 'notes', value: 'stampOwnerships[0]', width: '*'}); + } + + let tmodel = this.generateTableModel(stamps, countries,{ + cols: columns + }); + let styles = this.getStandardStyleDefinition(); + let opts = { + content: [] + }; + + opts.content.push(this.generateText(`Report: ${title}`, 'header')); + opts.content.push(this.generateText(`Total number of stamps: ${stamps.length}`, 'text')); + opts.content.push(this.generateText(`Total value: ${reportValue}`, 'text')); + opts.content.push({ + table: tmodel, style: 'table', layout: { + hLineColor: (i, node) => { + return '#aaa'; + }, + vLineColor: (i, node) => { + return '#aaa'; + } + } + }); + opts.styles = styles; + return opts; + } + + getStandardStyleDefinition() { return reportStyles; } @@ -44,48 +99,69 @@ export class ReportHelper { }; } - generateTableModel(stamps, config) { + generateTableModel(stamps, countries, config) { let model = { - body: [] + body: [], + widths: [] }; if (!_.isEmpty(config.cols)) { let tr = []; + _.forEach(config.cols, col => { tr.push({ text: col.name || col.type, style: 'tableHeader'}); + model.widths.push(col.width || 'auto'); }); model.headerRows = 1; - model.widths = ['auto', '*', 'auto', 'auto']; model.body.push(tr); - } - _.forEach(stamps, stamp => { - let row = []; - _.forEach(config.cols, col => { - let val = _.get(stamp, col.value); - switch (col.type) { - case 'catalogueNumber': - row.push(val); - break; - case 'condition': - row.push(this.converter.fromCondition(val)); - break; - case 'currencyValue': - let currencyCode = _.get(stamp, col.code, 'EUR'); - let v = this.converter.fromCurrencyValue(val, currencyCode); - row.push(v); - break; - case 'text': - _.forEach(col.additional, a => { - val += ' ' + _.get(stamp, a, ''); - }); - row.push(val); - break; - } + _.forEach(stamps, stamp => { + let row = []; + _.forEach(config.cols, col => { + row.push(this.generateTableCellValue(stamp, col, countries)); + }); + model.body.push(row); }); - model.body.push(row); - }); + } return model; } + + generateTableCellValue(stamp, col, countries) { + let val = _.get(stamp, col.value); + let result = ''; + switch (col.type) { + case 'catalogueNumber': + result = val; + break; + case 'condition': + result = this.converter.fromCondition(val); + break; + case 'currencyValue': + let currencyCode = _.get(stamp, col.code, CurrencyCode.USD.key); + result = this.converter.fromCurrencyValue(val, currencyCode); + break; + case 'country': + let c = _.find(countries, {id: val}); + result = c ? c.name : ''; + break; + case 'issues': + if (val && val.deception > 0) { + result = '\u0394'; + } else if (val && val.defects > 0) { + result = '\u00D7'; + } + break; + case 'notes': + result = (val && val.notes) ? val.notes : ''; + break; + case 'text': + _.forEach(col.additional, a => { + val += ' ' + _.get(stamp, a, ''); + }); + result = val; + break; + } + return result; + } } export class ReportValueConverter { @@ -108,7 +184,7 @@ export class ReportValueConverter { let value = ''; switch (condition) { case Condition.MINT.ordinal: - case Condition.MINT_HH: + case Condition.MINT_HH.ordinal: value = '*'; break; case Condition.MINT_NH.ordinal: @@ -119,7 +195,7 @@ export class ReportValueConverter { break; case Condition.USED.ordinal: case Condition.CTO.ordinal: - value = 'u'; + value = 'u'; // '\u00f8'; break; case Condition.MANUSCRIPT.ordinal: value = '~'; diff --git a/src/resources/elements/editor-dialog.html b/src/resources/elements/editor-dialog.html index da8bcf3..4d26ca9 100644 --- a/src/resources/elements/editor-dialog.html +++ b/src/resources/elements/editor-dialog.html @@ -13,7 +13,7 @@
${errorMsg}
- + diff --git a/src/resources/elements/editor-dialog.js b/src/resources/elements/editor-dialog.js index 094026d..0d72700 100644 --- a/src/resources/elements/editor-dialog.js +++ b/src/resources/elements/editor-dialog.js @@ -24,10 +24,13 @@ import {EventNames, EventManaged} from '../../events/event-managed'; @bindable('content') @bindable('title') @bindable('icon') +@bindable('saveText') + export class EditorDialog extends EventManaged { static inject = [EventAggregator, I18N]; + @bindable publishEvent = EventNames.save; errorMsg = ''; subscriptions = []; valid = true; @@ -37,6 +40,7 @@ export class EditorDialog extends EventManaged { this.i18n = i18n; this.eventBus = eventBus; this.setupSubscriptions(); + this.saveText = this.saveText || this.i18n.tr('actions.save'); } modelChanged() { @@ -67,7 +71,7 @@ export class EditorDialog extends EventManaged { } save() { - this.eventBus.publish(EventNames.save, {model: this.model, aspects: this.aspects}); + this.eventBus.publish(this.publishEvent, {model: this.model, aspects: this.aspects}); } } diff --git a/src/resources/elements/editor-dialog.scss b/src/resources/elements/editor-dialog.scss index 133d8c7..93596e7 100644 --- a/src/resources/elements/editor-dialog.scss +++ b/src/resources/elements/editor-dialog.scss @@ -21,8 +21,7 @@ editor-dialog { background-color: $theme-appbar-bg; color: $theme-appbar-color; display: flex; - - padding: $theme-padding-base; + padding: 0; } } @@ -34,8 +33,15 @@ editor-dialog { font-size: $theme-font-size-sm; } - .checkbox { + .checkbox, input[type='checkbox'] { height: 1.6rem; + margin-right: $theme-margin-thin; + } + + label { + align-items: start; + padding-top: $theme-padding-thin + $theme-padding-thinner; + display: flex; } } diff --git a/src/resources/elements/reports/report-builder.html b/src/resources/elements/reports/report-builder.html new file mode 100644 index 0000000..2a0b7c4 --- /dev/null +++ b/src/resources/elements/reports/report-builder.html @@ -0,0 +1,25 @@ + diff --git a/src/resources/elements/reports/report-builder.js b/src/resources/elements/reports/report-builder.js new file mode 100644 index 0000000..5926467 --- /dev/null +++ b/src/resources/elements/reports/report-builder.js @@ -0,0 +1,32 @@ +/** + Copyright 2023 Jason Drake + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import _ from 'lodash'; +import {EventNames} from "../../../events/event-managed"; +import {inject} from "aurelia-framework"; + +@inject() +export class ReportBuilder { + + model; + + activate(options) { + this.model = options; + } + + save() { + this.eventBus.publish(EventNames.generateReport, {model: this.model}); + } +} diff --git a/src/resources/elements/reports/report-builder.scss b/src/resources/elements/reports/report-builder.scss new file mode 100644 index 0000000..1e7c849 --- /dev/null +++ b/src/resources/elements/reports/report-builder.scss @@ -0,0 +1 @@ +@import "../../../theme/_semantic.scss"; diff --git a/src/resources/views/albums/album-editor.html b/src/resources/views/albums/album-editor.html index 1cd45bb..b8423b9 100644 --- a/src/resources/views/albums/album-editor.html +++ b/src/resources/views/albums/album-editor.html @@ -3,26 +3,26 @@ -
-
- -
- + +
+ +
+ -
-
-
- -
- -
-
-
- -
- -
-
- +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ diff --git a/src/resources/views/albums/album-editor.js b/src/resources/views/albums/album-editor.js index cf8aca5..16c5de8 100644 --- a/src/resources/views/albums/album-editor.js +++ b/src/resources/views/albums/album-editor.js @@ -13,38 +13,95 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {inject} from 'aurelia-framework'; +import {bindable, inject} from 'aurelia-framework'; import {StampCollections} from '../../../services/stampCollections'; import {LogManager} from 'aurelia-framework'; import $ from 'jquery'; import _ from 'lodash'; +import {EventAggregator} from "aurelia-event-aggregator"; +import {BindingEngine} from "aurelia-binding"; +import {I18N} from "aurelia-i18n"; +import {NewInstance} from "aurelia-dependency-injection"; +import {validateTrigger, ValidationController, ValidationRules} from "aurelia-validation"; +import {EventNames} from "../../../events/event-managed"; +import {ValidationHelper} from "../../../util/validation-helper"; const logger = LogManager.getLogger('albumEditor'); -@inject(StampCollections) export class albumEditor { - model; + static inject = [EventAggregator, BindingEngine, I18N, NewInstance.of(ValidationController), StampCollections]; + + @bindable model; + stampCollections = []; + _modelSubscribers = []; + + constructor(eventBus, bindingEngine, i18n, validationController, stampCollectionService) { + this.eventBus = eventBus; + this.bindingEngine = bindingEngine; + this.i18n = i18n; + this.validationController = validationController; + this.validationController.validationTigger = validateTrigger.manual; + this.stampCollectionService = stampCollectionService; + } - constructor(stampCollections) { - this.stampCollectionService = stampCollections; + deactivate() { + this._modelSubscribers.forEach(sub => { + sub.dispose(); + }); } activate(options) { this.model = options; - var that = this; var p = this.stampCollectionService.find(); p.then(results => { - that.stampCollections = results.models; + this.stampCollections = results.models; }).catch(err => { logger.error("Error with stamp collections", err); }); - if( !(this.model.id > 0) ) { + this.setupValidation(); + if( !this.model.id > 0 ) { + this.model.name = ''; _.debounce( () => { $('#editor-name').focus(); }, 125)(); } + this._modelSubscribers.push(this.bindingEngine.propertyObserver(this.model, 'name').subscribe(this._validate.bind(this))); + this._modelSubscribers.push(this.bindingEngine.propertyObserver(this.model, 'stampCollectionRef').subscribe(this._stampCollectionChanged.bind(this))); + _.debounce( () => { + this._validate(); + }, 125)(); return p; } + + _validate() { + this.validationController.validate().then( result => { + this.eventBus.publish(EventNames.valid, result.valid); + }); + } + + _stampCollectionChanged(collectionRef) { + if (collectionRef > 0) { + this.selectedCatalogue = _.find(this.stampCollections, {id: +collectionRef}); + if( this.selectedCatalogue ) { + this._validate(); + } + } else { + this._validate(); + } + } + + setupValidation() { + ValidationHelper.defineNumericRangeRule( ValidationRules, 'collection-selection',0); + ValidationRules + .ensure('name') + .required().withMessage(this.i18n.tr('messages.nameRequired')) + .ensure('stampCollectionRef') + .required().withMessage(this.i18n.tr('messages.stampCollectionRequired')) + .satisfiesRule('collection-selection').withMessage(this.i18n.tr('messages.stampCollectionRequired')) + .on(this.model); + + } + } diff --git a/src/resources/views/countries/country-editor.html b/src/resources/views/countries/country-editor.html index c8ec67a..99515d3 100644 --- a/src/resources/views/countries/country-editor.html +++ b/src/resources/views/countries/country-editor.html @@ -2,13 +2,13 @@
- +
- +
@@ -17,7 +17,7 @@
- Update image paths + ${'editor.update-image-paths'|t}
diff --git a/src/resources/views/sellers/seller-editor.html b/src/resources/views/sellers/seller-editor.html index 10001b4..80d76d7 100644 --- a/src/resources/views/sellers/seller-editor.html +++ b/src/resources/views/sellers/seller-editor.html @@ -1,14 +1,14 @@