Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(proof add): new page to add a proof #639

Merged
merged 1 commit into from
Jun 20, 2024
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
6 changes: 5 additions & 1 deletion src/components/ProofInputRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<span class="d-sm-none">{{ $t('AddPriceSingle.PriceDetails.Gallery') }}</span>
<span class="d-none d-sm-inline-flex">{{ $t('AddPriceSingle.PriceDetails.SelectFromGallery') }}</span>
</v-btn>
<v-btn class="mb-2" size="small" prepend-icon="mdi-receipt-text-clock" @click="showUserRecentProofsDialog">
<v-btn v-if="!hideRecentProofOption" class="mb-2" size="small" prepend-icon="mdi-receipt-text-clock" @click="showUserRecentProofsDialog">
<span class="d-sm-none">{{ $t('AddPriceSingle.PriceDetails.RecentProof') }}</span>
<span class="d-none d-sm-inline-flex">{{ $t('AddPriceSingle.PriceDetails.SelectRecentProof') }}</span>
</v-btn>
Expand Down Expand Up @@ -104,6 +104,10 @@ export default {
type: Object,
default: () => ({ proof_id: null, date: utils.currentDate() })
},
hideRecentProofOption: {
type: Boolean,
default: false
},
},
data() {
return {
Expand Down
10 changes: 10 additions & 0 deletions src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,14 @@
"Language": "Languages",
"Locations": "Locations",
"Date": "Date",
"Details": "Details",
"Delete": "Delete",
"Discount": "Discount",
"Edit": "Edit",
"Filter": "Filter",
"FilterProductWithPriceCountHide": "Hide products with prices",
"FilterPriceMoreThan30DaysHide": "Hide prices older than 30 days",
"Image": "Image",
"Order": "Order",
"OrderProductUniqueScansDESC": "Number of scans",
"OrderProductPriceCountDESC": "Number of prices",
Expand All @@ -127,6 +129,7 @@
"OrderPriceDateDESC": "Price date",
"SignInOFFAccount": "Sign in with your {url} account",
"Source": "Source",
"Type": "Type",
"Yes": "Yes",
"No": "No",
"Food": "Food",
Expand Down Expand Up @@ -158,6 +161,7 @@
"Home": {
"AddPrice": "Add a price",
"LatestPrices": "Latest prices",
"ProofAdd": "Add a proof",
"SearchProduct": "Search for a product",
"SettingsUpdated": "Settings updated!",
"TodayPriceStat": "No prices added today | {todayPriceNumber} new prices added today | {todayPriceNumber} new prices added today!",
Expand Down Expand Up @@ -278,6 +282,9 @@
"RECEIPT": "Receipt",
"GDPR_REQUEST": "GDPR request"
},
"ProofCreate": {
"Success": "Proof created!"
},
"ProofDelete": {
"Confirmation": "Are you sure you want to delete this proof?",
"Delete": "Delete",
Expand Down Expand Up @@ -307,6 +314,9 @@
"LatestPrices": {
"Title": "Latest prices"
},
"ProofAddSingle": {
"Title": "Add a proof"
},
"Search": {
"Title": "Search"
},
Expand Down
3 changes: 2 additions & 1 deletion src/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const routes = [
{ path: '/settings', name: 'settings', component: () => import('./views/UserSettings.vue'), meta: { title: 'Settings', requiresAuth: true } },
{ path: '/search', name: 'search', component: () => import('./views/Search.vue'), meta: { title: 'Search', icon: 'mdi-magnify', drawerMenu: true }},
{ path: '/prices', name: 'prices', component: () => import('./views/PriceList.vue'), meta: { title: 'LatestPrices', icon: 'mdi-tag-multiple-outline', drawerMenu: true }},
{ path: '/prices/add', name: 'add-price', component: () => import('./views/AddPriceHome.vue'), meta: { title: 'AddPrice', icon: 'mdi-plus', drawerMenu: true, color: 'primary', requiresAuth: true }},
{ path: '/prices/add', name: 'price-add', component: () => import('./views/AddPriceHome.vue'), meta: { title: 'AddPrice', icon: 'mdi-plus', drawerMenu: true, color: 'primary', requiresAuth: true }},
{ path: '/prices/add/single', name: 'add-price-single', component: () => import('./views/AddPriceSingle.vue'), meta: { title: 'Add a single price (price tag)', requiresAuth: true }},
{ path: '/prices/add/multiple/price-tag', name: 'add-price-multiple-price-tag', component: () => import('./views/AddPriceMultiple.vue'), meta: { title: 'Add multiple prices (price tags)', requiresAuth: true }},
{ path: '/prices/add/multiple/receipt', name: 'add-price-multiple-receipt', component: () => import('./views/AddPriceMultiple.vue'), meta: { title: 'Add multiple prices (receipt)', requiresAuth: true }},
Expand All @@ -22,6 +22,7 @@ const routes = [
{ path: '/locations/:id', name: 'location-detail', component: () => import('./views/LocationDetail.vue'), meta: { title: 'Location detail' }},
{ path: '/brands/:id', name: 'brand-detail', component: () => import('./views/BrandDetail.vue'), meta: { title: 'Brand detail' }},
{ path: '/categories/:id', name: 'category-detail', component: () => import('./views/CategoryDetail.vue'), meta: { title: 'Category detail' }},
{ path: '/proofs/add/single', name: 'proof-add', component: () => import('./views/ProofAddSingle.vue'), meta: { title: 'ProofAddSingle', icon: 'mdi-plus', color: 'primary', requiresAuth: true }},
{ path: '/proofs/:id', name: 'proof-detail', component: () => import('./views/ProofDetail.vue'), meta: { title: 'Proof detail', requiresAuth: true }},
{ path: '/users', name: 'users', component: () => import('./views/UserList.vue'), meta: { title: 'TopContributors', icon: 'mdi-account-star-outline', drawerMenu: true }},
{ path: '/users/:username', name: 'user-detail', component: () => import('./views/UserDetail.vue'), meta: { title: 'User detail' }},
Expand Down
13 changes: 12 additions & 1 deletion src/services/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ function buildURLParams(params = {}) {
return new URLSearchParams({...DEFAULT_PARAMS, ...params})
}

function filterParamsWithAllowedKeys(params, allowedKeys) {
const filteredParams = {}
for (const key in params) {
if (allowedKeys.includes(key)) {
filteredParams[key] = params[key]
}
}
return filteredParams
}


export default {
signIn(username, password) {
Expand Down Expand Up @@ -84,14 +94,15 @@ export default {
},

updateProof(proofId, params = {}) {
const PROOF_UPDATABLE_FIELDS = ['type', 'date', 'currency']
const store = useAppStore()
const url = `${import.meta.env.VITE_OPEN_PRICES_API_URL}/proofs/${proofId}?${buildURLParams()}`
return fetch(url, {
method: 'PATCH',
headers: Object.assign({}, DEFAULT_HEADERS, {
'Authorization': `Bearer ${store.user.token}`,
}),
body: JSON.stringify(params),
body: JSON.stringify(filterParamsWithAllowedKeys(params, PROOF_UPDATABLE_FIELDS)),
})
.then((response) => response.json())
},
Expand Down
4 changes: 2 additions & 2 deletions src/views/AddPriceMultiple.vue
Original file line number Diff line number Diff line change
Expand Up @@ -238,10 +238,10 @@ export default {
origins_tags: '',
labels_tags: [],
price: null,
price_per: null, // see initPriceSingleForm
price_per: null, // see PriceInputRow
price_is_discounted: false,
price_without_discount: null,
currency: null, // see initPriceMultipleForm
currency: null, // see PriceInputRow
},
categoryPricePerList: [
{key: 'KILOGRAM', value: this.$t('AddPriceSingle.CategoryPricePer.PerKg'), icon: 'mdi-weight-kilogram'},
Expand Down
16 changes: 14 additions & 2 deletions src/views/Home.vue
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@
</v-col>
</v-row>

<v-snackbar
v-model="proofSingleSuccessMessage"
color="success"
:timeout="2000"
>
{{ $t('ProofCreate.Success') }}
</v-snackbar>
<v-snackbar
v-model="settingsSuccessMessage"
color="success"
Expand All @@ -81,9 +88,11 @@ export default {
data() {
return {
APP_NAME: constants.APP_NAME,
settingsSuccessMessage: false,
todayPriceCount: null,
loading: false
loading: false,
// success messages
proofSingleSuccessMessage: false,
settingsSuccessMessage: false,
}
},
computed: {
Expand All @@ -93,6 +102,9 @@ export default {
},
},
mounted() {
if (this.$route.query.proofSingleSuccess === 'true') {
this.proofSingleSuccessMessage = true
}
if (this.$route.query.settingsSuccess === 'true') {
this.settingsSuccessMessage = true
}
Expand Down
170 changes: 170 additions & 0 deletions src/views/ProofAddSingle.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
<template>
<h1 class="text-h5 mb-1">
{{ $t('Home.ProofAdd') }}
</h1>

<v-form @submit.prevent="createProof">
<v-row>
<!-- Step 1: proof type -->
<v-col cols="12" md="6" lg="4">
<v-card
:title="$t('Common.Type')"
:prepend-icon="(addProofSingleForm.type === 'RECEIPT') ? 'mdi-receipt-text-outline' : 'mdi-library-shelves'"
height="100%"
:style="proofTypeFormFilled ? 'border: 1px solid #4CAF50' : 'border: 1px solid transparent'"
>
<template v-if="proofTypeFormFilled" #append>
<v-icon icon="mdi-checkbox-marked-circle" color="success" />
</template>
<v-divider />
<v-card-text>
<v-item-group v-model="addProofSingleForm.type" class="d-inline" mandatory>
<v-item v-for="pt in proofTypeList" :key="pt.key" v-slot="{ isSelected, toggle }" :value="pt.key">
<v-chip class="mr-1" :style="isSelected ? 'border: 1px solid #9E9E9E' : 'border: 1px solid transparent'" @click="toggle">
<v-icon start :icon="isSelected ? 'mdi-checkbox-marked-circle' : 'mdi-circle-outline'" />
{{ pt.value }}
</v-chip>
</v-item>
</v-item-group>
</v-card-text>
<v-overlay v-model="disableProofTypeForm" scrim="#E8F5E9" contained persistent />
</v-card>
</v-col>

<!-- Step 2: proof image -->
<v-col cols="12" md="6" lg="4">
<v-card
:title="$t('Common.Image')"
prepend-icon="mdi-image"
height="100%"
:style="proofImageFormFilled ? 'border: 1px solid #4CAF50' : 'border: 1px solid transparent'"
>
<template v-if="proofImageFormFilled" #append>
<v-icon icon="mdi-checkbox-marked-circle" color="success" />
</template>
<v-divider />
<v-card-text>
<ProofInputRow :proofType="addProofSingleForm.type" :proofForm="addProofSingleForm" :hideRecentProofOption="true" />
</v-card-text>
<v-overlay v-model="disableProofImageForm" scrim="#E8F5E9" contained persistent />
</v-card>
</v-col>

<!-- Step 3: date & currency -->
<v-col cols="12" md="6" lg="4">
<v-card
:title="$t('Common.Details')"
prepend-icon="mdi-cog-outline"
height="100%"
:style="proofDetailsFormSuccess ? 'border: 1px solid #4CAF50' : 'border: 1px solid transparent'"
>
<template v-if="proofDetailsFormSuccess" #append>
<v-icon icon="mdi-checkbox-marked-circle" color="success" />
</template>
<v-divider />
<v-card-text>
<h3 class="mb-1">
{{ $t('AddPriceSingle.WhereWhen.Date') }}
</h3>
<v-row>
<v-col cols="12" sm="6">
<v-text-field
v-model="addProofSingleForm.date"
:label="$t('AddPriceSingle.WhereWhen.DateLabel')"
type="date"
/>
</v-col>
</v-row>
</v-card-text>
<v-overlay v-model="disableProofDetailsForm" scrim="#E8F5E9" contained persistent />
</v-card>
</v-col>
</v-row>

<v-row>
<v-col>
<v-btn
type="submit"
:color="formFilled ? 'success' : ''"
:loading="loading"
:disabled="!formFilled"
>
{{ $t('AddPriceSingle.Create') }}
</v-btn>
</v-col>
</v-row>
</v-form>
</template>

<script>
import { defineAsyncComponent } from 'vue'
import { mapStores } from 'pinia'
import { useAppStore } from '../store'
import api from '../services/api'
import utils from '../utils.js'

export default {
components: {
ProofInputRow: defineAsyncComponent(() => import('../components/ProofInputRow.vue')),
},
data() {
return {
proofTypeList: [
{key: 'PRICE_TAG', value: this.$t('AddPriceHome.MultipleProductMode.Title'), icon: 'mdi-library-shelves'},
{key: 'RECEIPT', value: this.$t('AddPriceHome.ReceiptMode.Title'), icon: 'mdi-receipt-text-outline'}
],
addProofSingleForm: {
type: 'PRICE_TAG',
proof_id: null,
date: utils.currentDate(),
},
loading: false,
}
},
computed: {
...mapStores(useAppStore),
proofTypeFormFilled() {
let keys = ['type']
return Object.keys(this.addProofSingleForm).filter(k => keys.includes(k)).every(k => !!this.addProofSingleForm[k])
},
proofImageFormFilled() {
let keys = ['proof_id']
return Object.keys(this.addProofSingleForm).filter(k => keys.includes(k)).every(k => !!this.addProofSingleForm[k])
},
proofDetailsFormFilled() {
let keys = ['date']
return Object.keys(this.addProofSingleForm).filter(k => keys.includes(k)).every(k => !!this.addProofSingleForm[k])
},
formFilled() {
return this.proofTypeFormFilled && this.proofImageFormFilled && this.proofDetailsFormFilled
},
proofDetailsFormSuccess() {
return this.proofDetailsFormFilled && this.proofImageFormFilled
},
disableProofTypeForm() {
return this.proofImageFormFilled
},
disableProofImageForm() {
return !this.proofTypeFormFilled
},
disableProofDetailsForm() {
return !this.proofTypeFormFilled || !this.proofImageFormFilled
}
},
methods: {
createProof() {
this.loading = true
api.updateProof(this.addProofSingleForm.proof_id, this.addProofSingleForm)
.then(() => {
this.$router.push({ path: '/', query: { proofSingleSuccess: 'true' } })
})
.catch(err => {
this.$store.app.showError(err)
})
.finally(() => {
this.loading = false
})
}
}
}
</script>
Loading