Skip to content

Commit

Permalink
Allow to search Open Fact Foods to create products (#197)
Browse files Browse the repository at this point in the history
* Adde dialog

* Trying

* Works

* Works

* Works

* Works

* Added rating

* Added system products

* Added improved info

* Fixed loading
  • Loading branch information
turulomio authored Aug 24, 2024
1 parent 6bf8f99 commit fda104e
Show file tree
Hide file tree
Showing 5 changed files with 270 additions and 11 deletions.
8 changes: 4 additions & 4 deletions src/components/AutocompleteAdditives.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@
<v-autocomplete :readonly="readonly" :items="new_additives" v-model="new_value" multiple :label="mylabel" :return-object="returnObject" item-value="url" :rules="RulesSelection(true)">
<template v-slot:item="{ props, item }">
<v-list-item v-bind="props" title="">
<v-list-item-content>
<!-- <v-list-item-content> -->
<span v-html="additives_html_fullname(item.raw)"></span>
</v-list-item-content>
<!-- </v-list-item-content> -->
</v-list-item>
</template>
<template v-slot:selection="{ props, item }">
<v-list-item>
<v-list-item-content v-bind="props">
<!-- <v-list-item-content v-bind="props"> -->
<span v-html="additives_html_fullname(item.raw)"></span>
</v-list-item-content>
<!-- </v-list-item-content> -->
</v-list-item>
</template>
</v-autocomplete>
Expand Down
207 changes: 207 additions & 0 deletions src/components/OpenFoodFactsSearch.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
<template>
<div class="ma-4">
<h1>{{ $t(`Open Food Facts search`) }}
</h1>
<v-card width="50%" class="d-flex flex-row mx-auto my-5" flat >
<v-text-field clearable density="default" :disabled="loading" class="mb-3" v-model="search" prepend-icon="mdi-magnify" :label="$t('Search in Open Food Facts api')" single-line hide-details :placeholder="$t('Add a string to filter table')" @keyup.enter="on_search_change" />

<v-btn class="ml-8" color="primary" @click="on_search_change">{{ $t("Search")}}</v-btn>
</v-card>
<v-data-table :headers="off_headers" :items="off_items" class="elevation-1 cursorpointer" :items-per-page="100000" :loading="loading" :key="key" @click:row="showOffPage" :sortBy="[{key:'nutriments_number', order:'desc'}]" fixed-header height="60vh" >
<template #item.last_updated_t="{item}">
{{ localtime(new Date(item.last_updated_t*1000).toISOString()) }}
</template>
<template #item.completeness="{item}">
<v-rating readonly :length="5" :size="32" :model-value="item.completeness*5" active-color="primary" />
</template>
<template #item.actions="{item}">
<v-icon small class="mr-1" @click.stop="addProduct(item)">mdi-plus</v-icon>
<v-icon v-if="!useStore().catalog_manager" small class="mr-1" @click.stop="addSystemProduct(item)" color="red">mdi-plus</v-icon>
<v-icon small class="mr-1" @click.stop="showOffPage(null, {item:item})">mdi-search-web</v-icon>
</template>
</v-data-table>

<!-- DIALOG PRODUCTS CRUD -->
<v-dialog v-model="dialog_products_crud" width="90%">
<v-card class="pa-4">
<ProductsCRUD :product="product" :mode="product_cu_mode" :info="product_crud_info" :key="key" @cruded="on_ProductsCRUD_cruded" />
</v-card>
</v-dialog>
<!-- DIALOG SYSTEM PRODUCTS CRUD -->
<v-dialog v-model="dialog_system_products_crud" width="90%">
<v-card class="pa-4">
<SystemProductsCRUD :system_product="system_product" :mode="system_product_cu_mode" :info="system_product_crud_info" :key="key" @cruded="on_SystemProductsCRUD_cruded" />
</v-card>
</v-dialog>
</div>
</template>

<script>
import axios from 'axios'
import {localtime} from 'vuetify_rules'
import { empty_products, empty_system_products } from '@/empty_objects';
import { useStore } from '@/store.js'
import ProductsCRUD from './ProductsCRUD.vue';
import SystemProductsCRUD from './SystemProductsCRUD.vue';
export default {
components: {
ProductsCRUD,
SystemProductsCRUD,
},
data(){
return {
off_headers: [
{ title: this.$t('Date and time'), sortable: true, key: 'last_updated_t', width:"8%"},
{ title: this.$t('Name'), sortable: true, key: 'product_name', width:"40%"},
{ title: this.$t('Brand'), sortable: true, key: 'brands'},
{ title: this.$t('Country'), sortable: true, key: 'countries'},
{ title: this.$t('Nutriments'), sortable: true, key: 'nutriments_number'},
{ title: this.$t('Completeness'), sortable: true, key: 'completeness'},
{ title: this.$t('Actions'), key: 'actions', sortable: false, width: "8%"},
],
off_items:[],
search:"",
loading:false,
key:0,
//CRUD PRODUCT
product:null,
product_cu_mode:null,
dialog_products_crud:false,
product_crud_info:"",
//CRUD SYSTEM PRODUCT
system_product:null,
system_product_cu_mode:null,
dialog_system_products_crud:false,
system_product_crud_info:"",
}
},
methods:{
useStore,
localtime,
empty_products,
empty_system_products,
async on_search_change(){
this.loading=true
const apiUrl = 'https://world.openfoodfacts.org/cgi/search.pl';
try {
const response = await axios.get(apiUrl, {
params: {
search_terms: this.search,
search_simple: 1,
action: 'process',
json: 1
}
});
this.off_items = response.data.products;
this.off_items.forEach(o=>{
o.nutriments_number=Object.keys(o.nutriments).length
})
this.loading=false
} catch (error) {
this.error = 'Error fetching data';
console.error('API Error:', error);
} finally {
this.loading = false;
}
},
showOffPage(event,object){
window.open(`https://es.openfoodfacts.org/producto/${object.item.id}`)
},
get_off_products_info(item){
console.log(item)
var r={}
r.brand=item.brands
r.additives=item.additives_original_tags
r.ingredients=item.ingredients_text_with_allergens
Object.entries(item.nutriments).forEach(([key, value]) => {
if (key.includes("_100g")){
r[key]=value
}
});
r.image_front_url=item.image_front_url
r.image_url=item.image_url
return r
},
objectToLinks(obj) {
let html = '<ul>';
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
if (typeof obj[key] === 'string' && obj[key].startsWith('http')) {
// If the value is a URL, create a link
html += `<li><strong>${key}:</strong> <a href="${obj[key]}" target="_blank">${obj[key]}</a></li>`;
} else {
// Otherwise, display the text normally
html += `<li><strong>${key}:</strong> ${obj[key]}</li>`;
}
}
}
html += '</ul>';
return html;
},
addProduct(item){
this.product_crud_info=this.objectToLinks(this.get_off_products_info(item))
this.product_cu_mode="C"
this.product=this.empty_products()
this.product.name=item.product_name
this.product.company=item.brands
this.product.amount=100
this.product.calories=item.nutriments["energy-kcal_100g"]
this.product.fat=item.nutriments.fat_100g
this.product.saturated_fat=item.nutriments["saturated-fat_100g"]
this.product.protein=item.nutriments.proteins_100g
this.product.salt=item.nutriments.salt_100g
this.product.sodium=item.nutriments.sodium_100g
this.product.sugars=item.nutriments.sugars_100g
this.product.carbohydrate=item.nutriments.carbohydrates_100g
this.product.calcium=item.nutriments.calcium_100g
this.product.cholesterol=item.nutriments.cholesterol_100g
this.product.potassium=item.nutriments.potassium_100g
this.product.fiber=item.nutriments.fiber_100g
this.product.ferrum=item.nutriments.iron_100g
this.product.magnesium=item.nutriments.magnesium_100g
this.product.phosphor=item.nutriments.phosphor_100g
this.key=this.key+1
this.dialog_products_crud=true
},
addSystemProduct(item){
this.system_product_crud_info=this.objectToLinks(this.get_off_products_info(item))
this.system_product_cu_mode="C"
this.system_product=this.empty_system_products()
this.system_product.name=item.product_name
this.system_product.company=item.brands
this.system_product.amount=100
this.system_product.calories=item.nutriments["energy-kcal_100g"]
this.system_product.fat=item.nutriments.fat_100g
this.system_product.saturated_fat=item.nutriments["saturated-fat_100g"]
this.system_product.protein=item.nutriments.proteins_100g
this.system_product.salt=item.nutriments.salt_100g
this.system_product.sodium=item.nutriments.sodium_100g
this.system_product.sugars=item.nutriments.sugars_100g
this.system_product.carbohydrate=item.nutriments.carbohydrates_100g
this.system_product.calcium=item.nutriments.calcium_100g
this.system_product.cholesterol=item.nutriments.cholesterol_100g
this.system_product.potassium=item.nutriments.potassium_100g
this.system_product.fiber=item.nutriments.fiber_100g
this.system_product.ferrum=item.nutriments.iron_100g
this.system_product.magnesium=item.nutriments.magnesium_100g
this.system_product.phosphor=item.nutriments.phosphor_100g
this.key=this.key+1
this.dialog_system_products_crud=true
},
on_SystemProductsCRUD_cruded(){
this.dialog_system_products_crud=false
},
on_ProductsCRUD_cruded(){
this.dialog_products_crud=false
},
},
mounted(){
}
}
</script>
27 changes: 27 additions & 0 deletions src/components/Products.vue
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@
<SystemProductsCRUD :system_product="system_product" :mode="system_product_cu_mode" :key="'B'+key" @cruded="on_SystemProductsCRUD_cruded()"></SystemProductsCRUD>
</v-card>
</v-dialog>
<!-- DIALOG OFF -->
<v-dialog v-model="dialog_off" width="100%">
<v-card class="pa-4">
<OpenFoodFactsSearch key="'B'+key" @cruded="on_OFF_cruded" />
</v-card>
</v-dialog>
</div>
</template>

Expand All @@ -90,12 +96,14 @@
import SystemProductsCRUD from './SystemProductsCRUD.vue'
import TableElaboratedProducts from './TableElaboratedProducts.vue'
import { useStore } from '@/store.js'
import OpenFoodFactsSearch from './OpenFoodFactsSearch.vue'
export default {
components: {
MyMenuInline,
ProductsCRUD,
SystemProductsCRUD,
TableElaboratedProducts,
OpenFoodFactsSearch,
},
data(){
return {
Expand Down Expand Up @@ -164,6 +172,9 @@
system_product:null,
system_product_cu_mode:null,
dialog_system_products_crud:false,
//DIALOG OPEN FOOD FACTS
dialog_off:false,
}
},
methods:{
Expand Down Expand Up @@ -219,6 +230,19 @@
]
})
}
r.push({
subheader: this.$t("Open Food Facts Search"),
children: [
{
name: this.$t("Search in Open Food Facts"),
icon: "mdi-magnify",
code: function(){
this.dialog_off=true
}.bind(this),
},
]
})
return r
},
on_ProductsCRUD_cruded(){
Expand All @@ -241,6 +265,9 @@
this.parseResponseError(error)
});
},
on_OFF_cruded(){
this.update_all()
},
editProduct(item){
this.product=item
this.product_cu_mode="U"
Expand Down
22 changes: 19 additions & 3 deletions src/components/ProductsCRUD.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<template>
<div>
<h1>{{ title() }}</h1>
<v-card class="pa-6 mt-4" style="overflow-y: scroll" :height="600" >
<h1 @click="showinfo=!showinfo" v-show="showinfo">{{ title() }}</h1>
<div class="d-flex flex-row">
<v-card class="pa-6 mt-4" style="overflow-y: scroll" :height="600" min-width="50%">
<v-form ref="form" v-model="form_valid" lazy-validation >
<v-text-field id="name" :readonly="mode=='D' || mode=='R'" v-model="newproduct.name" :label="$t('Set product name')" :placeholder="$t('Set product name')" :rules="RulesString(200)" counter="200"/>
<v-autocomplete id="companies" v-model="newproduct.companies" :items="getArrayFromMap(useStore().companies)" :label="$t('Select a company')" item-title="name" item-value="url" :rules="RulesSelection(false)"/>
Expand Down Expand Up @@ -38,13 +39,18 @@
</v-data-table>
</v-card>
</v-form>
</v-card>

<v-card-actions>
<v-spacer></v-spacer>
<v-btn id="cmdFormat" color="primary" v-if="['C','U'].includes(mode)" @click="addFormat()" >{{ $t("Add a format") }}</v-btn>
<v-btn id="cmd" color="primary" v-if="['C','U','D'].includes(mode)" @click="acceptDialog()">{{ button() }}</v-btn>
<v-btn id="cmdClose" color="error" @click="$emit('cruded')" >{{ $t("Close") }}</v-btn>
</v-card-actions>
</v-card>

<p class="ma-5" v-html="info"></p>

</div>

<!-- DIALOG FORMATS CRUD -->
<v-dialog v-model="dialog_formats_crud" width="45%">
Expand Down Expand Up @@ -77,12 +83,22 @@
mode: {
required: true
},
info:{//Used to show info
required:false,
default:""
},
showinfo:{
required: false,
default:true
}
},
data(){
return{
form_valid:false,
newproduct: null,
key:0,
formats_headers: [
{ title: this.$t('Format'), sortable: true, key: 'formats'},
Expand Down
Loading

0 comments on commit fda104e

Please sign in to comment.