From 2568db746a3e87531cd4c6f7920af50012823ba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Odini?= Date: Thu, 19 Dec 2024 12:56:08 +0100 Subject: [PATCH] feat: new Price Validator Assistant page (#1145) --- .../ContributionAssistantPriceFormCard.vue | 47 ++++- src/i18n/locales/en.json | 6 + src/router.js | 1 + src/services/api.js | 40 +++- src/views/ContributionAssistant.vue | 20 +- src/views/Experiments.vue | 7 + src/views/PriceValidatorAssistant.vue | 176 ++++++++++++++++++ 7 files changed, 275 insertions(+), 22 deletions(-) create mode 100644 src/views/PriceValidatorAssistant.vue diff --git a/src/components/ContributionAssistantPriceFormCard.vue b/src/components/ContributionAssistantPriceFormCard.vue index a9104e6f090..4c2496f50c7 100644 --- a/src/components/ContributionAssistantPriceFormCard.vue +++ b/src/components/ContributionAssistantPriceFormCard.vue @@ -23,17 +23,45 @@ + + + + - + {{ $t('Common.Delete') }} + + + {{ $t('Common.Error') }} + + + {{ $t('Common.Unreadable') }} + + + + {{ $t('Common.Upload') }} + + @@ -46,11 +74,13 @@ export default { components: { ProductInputRow: defineAsyncComponent(() => import('../components/ProductInputRow.vue')), PriceInputRow: defineAsyncComponent(() => import('../components/PriceInputRow.vue')), + ProofFooterRow: defineAsyncComponent(() => import('../components/ProofFooterRow.vue')), }, props: { productPriceForm: { type: Object, default: () => ({ + id: null, type: null, category_tag: null, origins_tags: '', @@ -65,8 +95,12 @@ export default { detected_product_code: null }) }, + mode: { + type: String, + default: 'Contribution' // or 'Validation' + } }, - emits: ['removePrice'], + emits: ['removePriceTag', 'validatePriceTag'], data() { return { productFormFilled: false, @@ -79,8 +113,11 @@ export default { } }, methods: { - removePrice() { - this.$emit('removePrice') + removePriceTag(status=null) { + this.$emit('removePriceTag', status) + }, + validatePriceTag() { + this.$emit('validatePriceTag') } } } diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 8206760adf7..2c5f363ec94 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -175,6 +175,7 @@ "DisplayPriceMap": "Map", "DisplayPriceChart": "Chart", "Done": "Done", + "Error": "Error", "Experiments": "Experiments", "Label": "Label", "Labels": "Labels", @@ -241,6 +242,7 @@ "Physical": "Physical", "PRICE_TAG": "Price tag", "PriceCount": "{count} prices | {count} price | {count} prices", + "PriceToValidateCount": "{count} prices to validate | {count} price to validate | {count} prices to validate", "PriceCreated": "Price created!", "PricesMore": "More prices", "Private": "Private", @@ -290,6 +292,7 @@ "Top": "Top", "Total": "Total", "Type": "Type", + "Unreadable": "Unreadable", "Upload": "Upload", "UploadMultipleImages": "Upload {count} image | Upload {count} images", "UploadMultiplePrices": "Upload {count} price | Upload {count} prices", @@ -585,6 +588,9 @@ }, "ContributionAssistant": { "Title": "Contribution Assistant" + }, + "PriceValidatorAssistant": { + "Title": "Price Validator Assistant" } }, "Search": { diff --git a/src/router.js b/src/router.js index 5f10fd8b134..a0816a86b75 100644 --- a/src/router.js +++ b/src/router.js @@ -36,6 +36,7 @@ const routes = [ { path: '/users/:username', name: 'user-detail', component: () => import('./views/UserDetail.vue'), meta: { title: 'User detail' }}, { path: '/experiments', name: 'experiments', component: () => import('./views/Experiments.vue'), meta: { title: 'Experiments', icon: 'mdi-test-tube', drawerMenu: true, drawerMenuConditionalDisplay: 'drawer_display_experiments', breadcrumbs: [{title: 'Experiments', disabled: true }] }}, { path: '/experiments/contribution-assistant', name: 'contribution-assistant', component: () => import('./views/ContributionAssistant.vue'), meta: { title: 'ContributionAssistant', icon: 'mdi-draw', requiresAuth: true, breadcrumbs: [{title: 'Experiments', disabled: false, to: '/experiments' }, {title: 'ContributionAssistant', disabled: true }] }}, + { path: '/experiments/price-validator-assistant', name: 'price-validator-assistant', component: () => import('./views/PriceValidatorAssistant.vue'), meta: { title: 'PriceValidatorAssistant', icon: 'mdi-checkbox-marked-circle-plus-outline', requiresAuth: true, breadcrumbs: [{title: 'Experiments', disabled: false, to: '/experiments' }, {title: 'PriceValidatorAssistant', disabled: true }] }}, { path: '/reuses', name: 'reuses', component: () => import('./views/Reuses.vue'), meta: { title: 'Reuses', icon: 'mdi-account-group', drawerMenu: false, breadcrumbs: [{title: 'Reuses', disabled: true }] }}, { path: '/community', name: 'community', component: () => import('./views/Community.vue'), meta: { title: 'Community', icon: 'mdi-account-group', drawerMenu: true, breadcrumbs: [{title: 'Community', disabled: true }] }}, { path: '/stats', name: 'stats', component: () => import('./views/Stats.vue'), meta: { title: 'Stats', icon: 'mdi-chart-box-outline', drawerMenu: true, breadcrumbs: [{title: 'Stats', disabled: true }] }}, diff --git a/src/services/api.js b/src/services/api.js index a6797ca6adc..8a265fbeda9 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -23,7 +23,7 @@ function buildURLParams(params = {}) { } function filterBodyWithAllowedKeys(data, allowedKeys) { - const filteredData = {} + let filteredData = {} for (const key in data) { if (allowedKeys.includes(key)) { filteredData[key] = data[key] @@ -32,6 +32,20 @@ function filterBodyWithAllowedKeys(data, allowedKeys) { return filteredData } +function extraPriceCreateFiltering(data) { + let filteredData = {...data} + if (filteredData.type == constants.PRICE_TYPE_PRODUCT) { + delete filteredData.price_per + delete filteredData.category_tag + delete filteredData.origins_tags + delete filteredData.labels_tags + } else if (filteredData.type == constants.PRICE_TYPE_CATEGORY) { + delete filteredData.product_code + delete filteredData.product + } + return filteredData +} + export default { signIn(username, password) { @@ -157,7 +171,8 @@ export default { }, createPrice(inputData, source = null) { - const data = filterBodyWithAllowedKeys(inputData, PRICE_CREATE_FIELDS) + let data = filterBodyWithAllowedKeys(inputData, PRICE_CREATE_FIELDS) + data = extraPriceCreateFiltering(data) const store = useAppStore() store.user.last_product_product_used = data.product_code ? constants.PRICE_TYPE_PRODUCT : constants.PRICE_TYPE_CATEGORY const url = `${import.meta.env.VITE_OPEN_PRICES_API_URL}/prices?${buildURLParams({'app_version': source})}` @@ -353,5 +368,26 @@ export default { body: formData, }) .then((response) => response.json()) + }, + getPriceTags(params = {}) { + const defaultParams = {page: 1, size: OP_DEFAULT_PAGE_SIZE} // order_by default ? + const url = `${import.meta.env.VITE_OPEN_PRICES_API_URL}/price-tags?${buildURLParams({...defaultParams, ...params})}` + return fetch(url, { + method: 'GET', + headers: OP_DEFAULT_HEADERS, + }) + .then((response) => response.json()) + }, + updatePriceTag(priceTagId, inputData = {}) { + const store = useAppStore() + const url = `${import.meta.env.VITE_OPEN_PRICES_API_URL}/price-tags/${priceTagId}?${buildURLParams()}` + return fetch(url, { + method: 'PATCH', + headers: Object.assign({}, OP_DEFAULT_HEADERS, { + 'Authorization': `Bearer ${store.user.token}`, + }), + body: JSON.stringify(inputData), + }) + .then((response) => response.json()) } } diff --git a/src/views/ContributionAssistant.vue b/src/views/ContributionAssistant.vue index 7e057322d29..ab6cb2bbc5b 100644 --- a/src/views/ContributionAssistant.vue +++ b/src/views/ContributionAssistant.vue @@ -74,7 +74,7 @@ md="6" xl="4" > - + @@ -140,8 +140,8 @@ import { defineAsyncComponent } from 'vue' import { mapStores } from 'pinia' import { useAppStore } from '../store' import api from '../services/api' -import utils from '../utils.js' import constants from '../constants' +import utils from '../utils.js' export default { components: { @@ -220,8 +220,8 @@ export default { }, setProof(event) { const image = new Image() - // image.src = 'https://prices.openfoodfacts.org/img/0024/tM0NEloNU3.webp' - // image.src = 'https://prices.openfoodfacts.org/img/0023/f6tJvMcsDk.webp' + // image.src = 'https://prices.openfoodfacts.org/img/0024/tM0NEloNU3.webp' // barcodes + // image.src = 'https://prices.openfoodfacts.org/img/0023/f6tJvMcsDk.webp' // categories image.src = `${import.meta.env.VITE_OPEN_PRICES_APP_URL}/img/${event.file_path}` image.crossOrigin = 'Anonymous' this.image = image @@ -303,7 +303,7 @@ export default { } this.tab = 'Cleanup' }, - removePrice(index) { + removePriceTag(index) { this.productPriceForms.splice(index, 1) }, addPrices() { @@ -329,16 +329,6 @@ export default { location_osm_type: this.proofForm.location_osm_type, proof_id: this.proofForm.id } - // Cleanup unwanted fields for API - if (productPriceForm.type == constants.PRICE_TYPE_PRODUCT) { - delete priceData.price_per - delete priceData.category_tag - delete priceData.origins_tags - delete priceData.labels_tags - } else if (productPriceForm.type == constants.PRICE_TYPE_CATEGORY) { - delete priceData.product_code - delete priceData.product - } api.createPrice(priceData, this.$route.path).then(() => { // TODO: error handling this.productPriceForms[i].processed = true diff --git a/src/views/Experiments.vue b/src/views/Experiments.vue index cf7bc269735..50b5a0d8eb7 100644 --- a/src/views/Experiments.vue +++ b/src/views/Experiments.vue @@ -28,6 +28,13 @@ to="/experiments/contribution-assistant" /> + + + diff --git a/src/views/PriceValidatorAssistant.vue b/src/views/PriceValidatorAssistant.vue new file mode 100644 index 00000000000..5ed8a9a0a15 --- /dev/null +++ b/src/views/PriceValidatorAssistant.vue @@ -0,0 +1,176 @@ + + +