project-booster-vue 9.41.1 → 9.42.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/products/PbProducts.vue +1 -2
- package/src/components/products/default-payload.json +1 -4
- package/src/components/restitution/PbRestitutionList.stories.mdx +47 -40
- package/src/components/restitution/PbRestitutionList.vue +46 -40
- package/src/components/restitution/PbRestitutionListBlock.vue +13 -2
- package/src/components/restitution/PbRestitutionListLine.vue +12 -5
- package/src/components/restitution/restitution-list-default.json +14 -1
- package/src/components/trezor/PbTrezor.stories.mdx +47 -0
- package/src/components/trezor/PbTrezor.vue +356 -0
- package/src/components/trezor/__snapshots__/storyshots-puppeteer-test-puppeteer-ts-image-storyshots-project-booster-components-trezor-pb-trezor-/360/237/246/240-101-sandbox-1-snap.png +0 -0
- package/src/components/trezor/default-payload.json +17 -0
- package/src/services/api/productApi.ts +10 -7
- package/src/services/api/trezorApi.ts +30 -0
- package/src/stores/modules/productsStore.ts +5 -5
- package/src/stores/modules/trezorStore.ts +39 -0
- package/src/stores/store.ts +2 -0
package/package.json
CHANGED
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
:left-icon="BACK_ICON"
|
|
7
7
|
:class="{
|
|
8
8
|
'pb-products__back-button': true,
|
|
9
|
-
'pb-products__back-button--hidden':
|
|
10
|
-
!showBackButton && !decorate(answers, runtimeOptions, payload.viewModel.forceBackButton),
|
|
9
|
+
'pb-products__back-button--hidden': true,
|
|
11
10
|
}"
|
|
12
11
|
@click.once="$emit('go-back')"
|
|
13
12
|
/>
|
|
@@ -3,11 +3,8 @@
|
|
|
3
3
|
"backLabel": "Question précédente",
|
|
4
4
|
"label": "Estimer votre projet de rénovation énergétique en 2mn",
|
|
5
5
|
"defaultProduct": {
|
|
6
|
-
"price": {
|
|
7
|
-
"initialUnitAmount": 200
|
|
8
|
-
},
|
|
9
6
|
"designation": "Pompe à chaleur air/air",
|
|
10
|
-
"photo": "https://storage.googleapis.com/project-booster-media/energyrenovation/
|
|
7
|
+
"photo": "https://storage.googleapis.com/project-booster-media/energyrenovation/default_heat_pump.jpg"
|
|
11
8
|
}
|
|
12
9
|
},
|
|
13
10
|
"callToActions": [
|
|
@@ -23,49 +23,67 @@ import CALL_TO_ACTION_WITH_MODAL from './restitution-call-to-actions-with-modal.
|
|
|
23
23
|
/>
|
|
24
24
|
|
|
25
25
|
export const summary = {
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
subprojectTemplateId: '44cddf9d-f0e8-4d3d-82dc-d717dc70342a',
|
|
27
|
+
subprojectTemplateLabel: 'Pompe à chaleur air-eau',
|
|
28
28
|
businessUnit: 'LMFR',
|
|
29
|
-
summaryDate: '
|
|
30
|
-
subprojectTemplateLabel: '<strong>Pompe à chaleur air/air</strong>',
|
|
29
|
+
summaryDate: '2022-10-13T12:24:39.316059261Z',
|
|
31
30
|
cost: {
|
|
32
|
-
min:
|
|
33
|
-
max:
|
|
31
|
+
min: 12103.78,
|
|
32
|
+
max: 19103.78,
|
|
34
33
|
currency: 'EURO',
|
|
35
34
|
},
|
|
36
35
|
components: [
|
|
37
36
|
{
|
|
38
|
-
componentId: '
|
|
39
|
-
title: '
|
|
37
|
+
componentId: 'FURNISHING',
|
|
38
|
+
title: 'Pompe à chaleur installée (hors aides)',
|
|
39
|
+
details: 'Travaux réalisés par nos artisans partenaires Leroy Merlin',
|
|
40
40
|
cost: {
|
|
41
|
-
min:
|
|
42
|
-
max:
|
|
41
|
+
min: 13000,
|
|
42
|
+
max: 20000,
|
|
43
43
|
currency: 'EURO',
|
|
44
44
|
},
|
|
45
|
-
details: 'Test du message vert',
|
|
46
45
|
lines: [
|
|
47
46
|
{
|
|
48
|
-
text: '
|
|
49
|
-
details: 'Terrain constructible, délimité et viabilisé. La nature du sous-sol est garantie.',
|
|
47
|
+
text: 'Pompe à chaleur',
|
|
50
48
|
cost: {
|
|
51
|
-
min:
|
|
52
|
-
max:
|
|
49
|
+
min: 10000,
|
|
50
|
+
max: 15000,
|
|
53
51
|
currency: 'EURO',
|
|
54
52
|
},
|
|
55
53
|
},
|
|
56
54
|
{
|
|
57
|
-
text: '
|
|
55
|
+
text: 'Installation',
|
|
58
56
|
cost: {
|
|
59
|
-
min:
|
|
60
|
-
max:
|
|
57
|
+
min: 3000,
|
|
58
|
+
max: 5000,
|
|
59
|
+
currency: 'EURO',
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
componentId: 'AID',
|
|
66
|
+
title: 'Aides estimées (MaPrimeRénov, Prime Energie)',
|
|
67
|
+
details: 'Les aides varient selon certains critères.',
|
|
68
|
+
cost: {
|
|
69
|
+
min: -896.22,
|
|
70
|
+
max: -950.22,
|
|
71
|
+
currency: 'EURO',
|
|
72
|
+
},
|
|
73
|
+
lines: [
|
|
74
|
+
{
|
|
75
|
+
text: 'Estimation Prime énergie Leroy Merlin',
|
|
76
|
+
cost: {
|
|
77
|
+
min: -96.22,
|
|
78
|
+
max: -650.22,
|
|
61
79
|
currency: 'EURO',
|
|
62
80
|
},
|
|
63
81
|
},
|
|
64
82
|
{
|
|
65
|
-
text: '
|
|
66
|
-
type: 'REDUCTION',
|
|
83
|
+
text: 'Estimation MaPrimeRénov',
|
|
67
84
|
cost: {
|
|
68
|
-
|
|
85
|
+
min: -800,
|
|
86
|
+
max: -900,
|
|
69
87
|
currency: 'EURO',
|
|
70
88
|
},
|
|
71
89
|
},
|
|
@@ -73,21 +91,15 @@ export const summary = {
|
|
|
73
91
|
},
|
|
74
92
|
],
|
|
75
93
|
subprojectAttributes: {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
94
|
+
age: 'NEW',
|
|
95
|
+
housingType: 'HOUSE',
|
|
96
|
+
income: 'MIDDLE',
|
|
97
|
+
inhabitantsCount: 1,
|
|
98
|
+
location: '20137',
|
|
99
|
+
situation: 'OWNER',
|
|
100
|
+
surfaceAreaInSquareMeters: 40,
|
|
83
101
|
},
|
|
84
102
|
suggestedTypologies: [
|
|
85
|
-
{
|
|
86
|
-
domesticSpaceLabel: 'Cuisine',
|
|
87
|
-
domesticSpaceHref: '/domestic-spaces/59405',
|
|
88
|
-
projectKindHref: '/project-kinds/34048',
|
|
89
|
-
projectTypeHref: '/project-types/29239',
|
|
90
|
-
},
|
|
91
103
|
{
|
|
92
104
|
domesticSpaceLabel: 'Tout le logement',
|
|
93
105
|
domesticSpaceHref: '/domestic-spaces/59461',
|
|
@@ -95,11 +107,6 @@ export const summary = {
|
|
|
95
107
|
projectTypeHref: '/project-types/61855',
|
|
96
108
|
},
|
|
97
109
|
],
|
|
98
|
-
legalMentions: [
|
|
99
|
-
"Les prix ci-dessus ne sont proposés qu'à titre indicatif et sont basés sur les prix du marché.",
|
|
100
|
-
"L'outil d'estimation ne vaut pas offre de vente.",
|
|
101
|
-
"La disponibilité des produits au moment de l'achat n'est pas garantie.",
|
|
102
|
-
],
|
|
103
110
|
};
|
|
104
111
|
|
|
105
112
|
# 🦠 `PbRestitutionList` - Component
|
|
@@ -131,7 +138,7 @@ export const TemplateSandbox = (args, { argTypes }) => ({
|
|
|
131
138
|
<Canvas>
|
|
132
139
|
<Story
|
|
133
140
|
name="101 Sandbox"
|
|
134
|
-
args={{ payload: DEFAULT_PAYLOAD, runtimeOptions: { isLoggedIn: true } }}
|
|
141
|
+
args={{ payload: DEFAULT_PAYLOAD, runtimeOptions: { isLoggedIn: true, projectMode: true } }}
|
|
135
142
|
parameters={{
|
|
136
143
|
msw: [
|
|
137
144
|
rest.get('/api/inhabitant-projects', getProjectsResolver),
|
|
@@ -50,6 +50,13 @@
|
|
|
50
50
|
:label="button.label"
|
|
51
51
|
@click="callAction(button.action)"
|
|
52
52
|
:theme="button.bordered ? 'bordered' : 'solid'"
|
|
53
|
+
v-if="!button.conditions"
|
|
54
|
+
/>
|
|
55
|
+
<m-button
|
|
56
|
+
:label="button.label"
|
|
57
|
+
@click="callAction(button.action)"
|
|
58
|
+
:theme="button.bordered ? 'bordered' : 'solid'"
|
|
59
|
+
v-if="checkConditional(button.conditions) && button.conditions"
|
|
53
60
|
/>
|
|
54
61
|
</div>
|
|
55
62
|
<div class="pb-restitution-list__footer__save">
|
|
@@ -74,53 +81,36 @@
|
|
|
74
81
|
<div class="pb-restitution-list__modal__details__title" v-html="summary.subprojectTemplateLabel"></div>
|
|
75
82
|
|
|
76
83
|
<div class="pb-restitution-list__modal__details__row">
|
|
77
|
-
<div
|
|
78
|
-
<div
|
|
79
|
-
|
|
80
|
-
<
|
|
84
|
+
<div v-for="component in summary.components" :key="component.details">
|
|
85
|
+
<div class="pb-restitution-list__modal__details__row__title" justify-content="space-between">
|
|
86
|
+
<div v-html="component.title"></div>
|
|
87
|
+
<div v-if="component.cost.min != component.cost.max">
|
|
88
|
+
<strong>{{ component.cost.min }}€ et {{ component.cost.max }}€</strong>
|
|
89
|
+
</div>
|
|
90
|
+
<div v-else>
|
|
91
|
+
<strong>{{ component.cost.min }}€</strong>
|
|
92
|
+
</div>
|
|
81
93
|
</div>
|
|
82
|
-
</div>
|
|
83
94
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
{{ line.details }}
|
|
95
|
+
<div class="pb-restitution-list__modal__details__row__label" v-if="component.details">
|
|
96
|
+
{{ component.details }}
|
|
87
97
|
</div>
|
|
88
|
-
</div>
|
|
89
98
|
|
|
90
|
-
<div
|
|
91
|
-
v-for="line in summary.components[0].lines"
|
|
92
|
-
:key="line.text"
|
|
93
|
-
class="pb-restitution-list__modal__details__row__list"
|
|
94
|
-
:class="{ ' pb-restitution-list__modal__details__row': line.cost.reduce }"
|
|
95
|
-
>
|
|
96
|
-
<ul>
|
|
97
|
-
<div>
|
|
98
|
-
<li v-if="!line.cost.reduce">
|
|
99
|
-
<div v-html="line.text" v-if="!line.cost.reduce"></div>
|
|
100
|
-
<div v-if="!line.cost.reduce">{{ line.cost.min }}€ - {{ line.cost.max }}€</div>
|
|
101
|
-
</li>
|
|
102
|
-
</div>
|
|
103
|
-
</ul>
|
|
104
|
-
</div>
|
|
105
|
-
|
|
106
|
-
<div v-for="line in summary.components[0].lines" :key="line.text">
|
|
107
99
|
<div
|
|
108
|
-
v-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
100
|
+
v-for="line in component.lines"
|
|
101
|
+
:key="line.text"
|
|
102
|
+
class="pb-restitution-list__modal__details__row__list"
|
|
103
|
+
:class="{ ' pb-restitution-list__modal__details__row': line.cost.reduce }"
|
|
112
104
|
>
|
|
113
|
-
<
|
|
114
|
-
<div
|
|
115
|
-
<
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
<div v-if="line.cost.reduce" class="green">
|
|
121
|
-
<strong>{{ line.cost.reduce }}€</strong>
|
|
105
|
+
<ul>
|
|
106
|
+
<div>
|
|
107
|
+
<li>
|
|
108
|
+
<div v-html="line.text"></div>
|
|
109
|
+
<div v-if="line.cost.min != line.cost.max">{{ line.cost.min }}€ et {{ line.cost.max }}€</div>
|
|
110
|
+
<div v-else>{{ line.cost.min }}€</div>
|
|
111
|
+
</li>
|
|
122
112
|
</div>
|
|
123
|
-
</
|
|
113
|
+
</ul>
|
|
124
114
|
</div>
|
|
125
115
|
</div>
|
|
126
116
|
</div>
|
|
@@ -159,6 +149,7 @@ import { RestitutionPayload, RestitutionPayloadAction } from '@/types/pb/Restitu
|
|
|
159
149
|
import { ScenarioStepAnswer } from '@/types/pb/Scenario';
|
|
160
150
|
import PbRestitutionListBlock from './PbRestitutionListBlock.vue';
|
|
161
151
|
import { Project } from '../../types/pb/Project';
|
|
152
|
+
import { areConditionsValid } from '../../services/scenarioConditionals';
|
|
162
153
|
|
|
163
154
|
export default defineComponent({
|
|
164
155
|
components: {
|
|
@@ -228,11 +219,21 @@ export default defineComponent({
|
|
|
228
219
|
created() {
|
|
229
220
|
if (this.payload?.callToActions) {
|
|
230
221
|
if (this.payload?.callToActions) {
|
|
222
|
+
const saveAction = Object.values(this.payload.callToActions).find((cta) => {
|
|
223
|
+
return (
|
|
224
|
+
areConditionsValid(cta.conditions!, this.answers, this.runtimeOptions) &&
|
|
225
|
+
cta.action?.type === 'MODAL' &&
|
|
226
|
+
cta.action?.component === 'PbProjectItemSave'
|
|
227
|
+
);
|
|
228
|
+
});
|
|
231
229
|
this.showSaveProjectItem = this.showSaveEstimate;
|
|
232
230
|
}
|
|
233
231
|
}
|
|
234
232
|
},
|
|
235
233
|
methods: {
|
|
234
|
+
checkConditional(cta: any) {
|
|
235
|
+
return areConditionsValid(cta, this.answers, this.runtimeOptions);
|
|
236
|
+
},
|
|
236
237
|
handleShowDialog(content: any) {
|
|
237
238
|
this.dialogContent = content;
|
|
238
239
|
this.showDialog = !this.showDialog;
|
|
@@ -268,6 +269,11 @@ export default defineComponent({
|
|
|
268
269
|
this.showSaveProjectItem = true;
|
|
269
270
|
}
|
|
270
271
|
|
|
272
|
+
this.$emit('click-save-item', {
|
|
273
|
+
answers: [],
|
|
274
|
+
action: action,
|
|
275
|
+
});
|
|
276
|
+
} else if (action.code === 'SAVED_ITEM') {
|
|
271
277
|
this.$emit('click-save-item', {
|
|
272
278
|
answers: [],
|
|
273
279
|
action: action,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<m-flex align-items="center" direction="column">
|
|
2
|
+
<m-flex align-items="center" direction="column" v-if="payload.componentId != 'AID'">
|
|
3
3
|
<m-flex :class="`pb-restitution-list-block`" direction="column" ref="pbRestitutionListBlock">
|
|
4
4
|
<div class="pb-restitution-list-block__title" v-html="summary.subprojectTemplateLabel"></div>
|
|
5
5
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<div class="pb-restitution-list-line">
|
|
8
8
|
<div class="pb-restitution-list-line__title" v-html="summary.components[0].title"></div>
|
|
9
9
|
<div class="pb-restitution-list-line__price">
|
|
10
|
-
<div class="pb-restitution-list-line__price__min">{{ summary.components[0].cost.min }}€ 
|
|
10
|
+
<div class="pb-restitution-list-line__price__min">{{ summary.components[0].cost.min }}€ et </div>
|
|
11
11
|
<div class="pb-restitution-list-line__price__max">{{ summary.components[0].cost.max }}€</div>
|
|
12
12
|
</div>
|
|
13
13
|
</div>
|
|
@@ -19,6 +19,17 @@
|
|
|
19
19
|
:line="line"
|
|
20
20
|
></pb-restitution-list-line>
|
|
21
21
|
|
|
22
|
+
<div v-for="aidComponent in summary.components" :key="aidComponent.componentId">
|
|
23
|
+
<div v-if="aidComponent.componentId === 'AID'">
|
|
24
|
+
<pb-restitution-list-line
|
|
25
|
+
:key="aidComponent.title"
|
|
26
|
+
:component="aidComponent"
|
|
27
|
+
:line="aidComponent"
|
|
28
|
+
:renovAid="true"
|
|
29
|
+
></pb-restitution-list-line>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
22
33
|
<div class="pb-restitution-list-block__body__row unbordered">
|
|
23
34
|
<div class="pb-restitution-list-block__body__row__title full">
|
|
24
35
|
<div><strong>Montant total</strong> (aides déduites)</div>
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="pb-restitution-list-line" :class="{ green:
|
|
3
|
-
<div class="pb-restitution-list-line__title" v-html="line.text"></div>
|
|
4
|
-
<div class="pb-restitution-list-line__price" v-if="
|
|
2
|
+
<div class="pb-restitution-list-line" :class="{ green: renovAid }" v-if="renovAid">
|
|
3
|
+
<div class="pb-restitution-list-line__title" v-html="line.text || line.title"></div>
|
|
4
|
+
<div class="pb-restitution-list-line__price" v-if="!renovAid">
|
|
5
5
|
<div class="pb-restitution-list-line__price__min">
|
|
6
|
-
{{ formatPriceRange(line.cost.min, line.cost.max).min }}€ 
|
|
6
|
+
{{ formatPriceRange(line.cost.min, line.cost.max).min }}€ et
|
|
7
7
|
</div>
|
|
8
8
|
<div class="pb-restitution-list-line__price__max">{{ formatPriceRange(line.cost.min, line.cost.max).max }}€</div>
|
|
9
9
|
</div>
|
|
10
|
-
<div class="pb-restitution-list-line__price">
|
|
10
|
+
<div class="pb-restitution-list-line__price">
|
|
11
|
+
<span v-if="line.cost.min === line.cost.max">{{ line.cost.min }}€</span>
|
|
12
|
+
<span v-else>{{ line.cost.min }} et {{ line.cost.max }}€</span>
|
|
13
|
+
</div>
|
|
11
14
|
</div>
|
|
12
15
|
</template>
|
|
13
16
|
|
|
@@ -24,6 +27,10 @@ export default defineComponent({
|
|
|
24
27
|
type: Object,
|
|
25
28
|
default: () => ({}),
|
|
26
29
|
},
|
|
30
|
+
renovAid: {
|
|
31
|
+
type: Boolean,
|
|
32
|
+
default: () => false,
|
|
33
|
+
},
|
|
27
34
|
},
|
|
28
35
|
methods: {
|
|
29
36
|
handleLinkClick() {
|
|
@@ -6,9 +6,22 @@
|
|
|
6
6
|
{
|
|
7
7
|
"type": "button",
|
|
8
8
|
"label": "Être rappelé par un expert",
|
|
9
|
-
"
|
|
9
|
+
"action": {
|
|
10
|
+
"type": "STEP",
|
|
11
|
+
"code": "STEP_CODE"
|
|
12
|
+
},
|
|
10
13
|
"bordered": false
|
|
11
14
|
},
|
|
15
|
+
{
|
|
16
|
+
"conditions": ["runtimeOptions.projectMode === true"],
|
|
17
|
+
"type": "button",
|
|
18
|
+
"label": "J'enregistre mon estimation",
|
|
19
|
+
"bordered": true,
|
|
20
|
+
"action": {
|
|
21
|
+
"type": "MODAL",
|
|
22
|
+
"code": "SUMMARY"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
12
25
|
{
|
|
13
26
|
"conditions": ["!runtimeOptions.projectMode && !runtimeOptions.estimateCreated"],
|
|
14
27
|
"type": "button",
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { nestedAppDecorator } from '../../../.storybook/nested-app-decorator';
|
|
2
|
+
import { ArgsTable, Canvas, Meta, Source, Story } from '@storybook/addon-docs';
|
|
3
|
+
import cloneDeep from 'lodash.clonedeep';
|
|
4
|
+
import PbTrezor from './PbTrezor';
|
|
5
|
+
import DEFAULT_PAYLOAD from './default-payload.json';
|
|
6
|
+
|
|
7
|
+
<Meta
|
|
8
|
+
title="Project Booster/Components/Trezor/PbTrezor 🦠"
|
|
9
|
+
component={PbTrezor}
|
|
10
|
+
decorators={[nestedAppDecorator({}, [])]}
|
|
11
|
+
parameters={{ layout: 'fullscreen' }}
|
|
12
|
+
/>
|
|
13
|
+
|
|
14
|
+
# 🦠 `PbTrezor` - Components
|
|
15
|
+
|
|
16
|
+
[](https://github.com/adeo/project-booster-vue/tree/master/src/components/question)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
The `PbTrezor` Vue component allows select family amount persons. It
|
|
21
|
+
is customizable through the payload property.
|
|
22
|
+
|
|
23
|
+
It has a default provided payload, used in the Sandbox below.
|
|
24
|
+
|
|
25
|
+
The input field is automatically focused.
|
|
26
|
+
|
|
27
|
+
# `PbTrezor`
|
|
28
|
+
|
|
29
|
+
export const TemplateSandbox = (args, { argTypes }) => ({
|
|
30
|
+
props: Object.keys(argTypes),
|
|
31
|
+
components: { PbTrezor },
|
|
32
|
+
setup() {
|
|
33
|
+
return { args };
|
|
34
|
+
},
|
|
35
|
+
template: `<pb-trezor
|
|
36
|
+
:payload="args.payload"
|
|
37
|
+
:show-back-button="args.showBackButton"
|
|
38
|
+
:completed-event-name="args.completedEventName"
|
|
39
|
+
@step-completed="args.onStepCompleted"
|
|
40
|
+
/>`,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
<Canvas>
|
|
44
|
+
<Story name="101 Sandbox" args={{ payload: DEFAULT_PAYLOAD }}>
|
|
45
|
+
{TemplateSandbox.bind({})}
|
|
46
|
+
</Story>
|
|
47
|
+
</Canvas>
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<m-flex class="pb-trezor" direction="column">
|
|
3
|
+
<div class="pb-trezor__title" v-if="payload?.viewModel?.label">
|
|
4
|
+
{{ payload.viewModel.label }}
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<form action="" class="pb-trezor__form">
|
|
8
|
+
<m-flex class="pb-trezor__form__row">
|
|
9
|
+
<m-text-input
|
|
10
|
+
v-model="formData.values.lastname"
|
|
11
|
+
:error="formData.errors.lastname"
|
|
12
|
+
@focus="validate('lastname')"
|
|
13
|
+
@keypress="validate('lastname')"
|
|
14
|
+
type="text"
|
|
15
|
+
label="Nom"
|
|
16
|
+
:required="true"
|
|
17
|
+
class="pb-trezor__input"
|
|
18
|
+
></m-text-input>
|
|
19
|
+
</m-flex>
|
|
20
|
+
|
|
21
|
+
<m-flex class="pb-trezor__form__row">
|
|
22
|
+
<m-text-input
|
|
23
|
+
v-model="formData.values.firstname"
|
|
24
|
+
:error="formData.errors.firstname"
|
|
25
|
+
@focus="validate('firstname')"
|
|
26
|
+
@keypress="validate('firstname')"
|
|
27
|
+
type="text"
|
|
28
|
+
label="Prénom"
|
|
29
|
+
:required="true"
|
|
30
|
+
class="pb-trezor__input"
|
|
31
|
+
></m-text-input>
|
|
32
|
+
</m-flex>
|
|
33
|
+
|
|
34
|
+
<m-flex class="pb-trezor__form__row">
|
|
35
|
+
<m-text-input
|
|
36
|
+
v-model="formData.values.zipcode"
|
|
37
|
+
:error="formData.errors.zipcode"
|
|
38
|
+
@focus="validate('zipcode')"
|
|
39
|
+
@keypress="validate('zipcode')"
|
|
40
|
+
type="text"
|
|
41
|
+
label="Code postal"
|
|
42
|
+
:required="true"
|
|
43
|
+
class="pb-trezor__input"
|
|
44
|
+
></m-text-input>
|
|
45
|
+
</m-flex>
|
|
46
|
+
|
|
47
|
+
<m-flex class="pb-trezor__form__row">
|
|
48
|
+
<m-text-input
|
|
49
|
+
v-model="formData.values.email"
|
|
50
|
+
:error="formData.errors.email"
|
|
51
|
+
@focus="validate('email')"
|
|
52
|
+
@keypress="validate('email')"
|
|
53
|
+
type="email"
|
|
54
|
+
label="E-mail"
|
|
55
|
+
:required="true"
|
|
56
|
+
class="pb-trezor__input"
|
|
57
|
+
></m-text-input>
|
|
58
|
+
</m-flex>
|
|
59
|
+
|
|
60
|
+
<m-flex class="pb-trezor__form__row">
|
|
61
|
+
<m-text-input
|
|
62
|
+
v-model="formData.values.phone"
|
|
63
|
+
:error="formData.errors.phone"
|
|
64
|
+
@focus="validate('phone')"
|
|
65
|
+
@keypress="validate('phone')"
|
|
66
|
+
type="phone"
|
|
67
|
+
label="Numéro de téléphone"
|
|
68
|
+
:required="true"
|
|
69
|
+
class="pb-trezor__input"
|
|
70
|
+
></m-text-input>
|
|
71
|
+
</m-flex>
|
|
72
|
+
|
|
73
|
+
<m-flex class="pb-trezor__form__row space" direction="column">
|
|
74
|
+
<label for="" class="pb-trezor__form__label"><span>- obligatoire</span></label>
|
|
75
|
+
<m-checkbox
|
|
76
|
+
class="pb-trezor__form__checkbox"
|
|
77
|
+
:required="true"
|
|
78
|
+
label="J'ai lu est accepte sans réserve les conditions générales d'utilisation"
|
|
79
|
+
v-model="formData.values.optin"
|
|
80
|
+
></m-checkbox>
|
|
81
|
+
</m-flex>
|
|
82
|
+
|
|
83
|
+
<m-flex class="pb-trezor__form__row space" direction="column">
|
|
84
|
+
<m-checkbox
|
|
85
|
+
class="pb-trezor__form__checkbox"
|
|
86
|
+
:required="false"
|
|
87
|
+
v-model="formData.values.optinPartners"
|
|
88
|
+
label="J'autorise Leroy Merlin à transmettre les données collectées ici à l'un de nos partenaires susceptibles de réaliser le chantier (en fonction des critères géographiques)"
|
|
89
|
+
></m-checkbox>
|
|
90
|
+
</m-flex>
|
|
91
|
+
|
|
92
|
+
<div class="pb-trezor__container">
|
|
93
|
+
<m-flex class="pb-trezor__container--button" direction="row">
|
|
94
|
+
<m-button
|
|
95
|
+
:theme="action.bordered ? 'bordered' : 'solid'"
|
|
96
|
+
class="pb-trezor__container--button__ok mc-button--full"
|
|
97
|
+
:type="action.type"
|
|
98
|
+
:label="action.label"
|
|
99
|
+
icon-position="right"
|
|
100
|
+
:href="action.href"
|
|
101
|
+
v-for="action in payload.callToActions"
|
|
102
|
+
:key="action.label"
|
|
103
|
+
@click.prevent="callAction(action)"
|
|
104
|
+
/>
|
|
105
|
+
</m-flex>
|
|
106
|
+
</div>
|
|
107
|
+
</form>
|
|
108
|
+
</m-flex>
|
|
109
|
+
</template>
|
|
110
|
+
|
|
111
|
+
<script lang="ts">
|
|
112
|
+
import { defineComponent, PropType, ref } from 'vue';
|
|
113
|
+
import { useStore } from 'vuex';
|
|
114
|
+
import MFlex from './../mozaic/flex/MFlex.vue';
|
|
115
|
+
import MButton from '../mozaic/buttons/MButton.vue';
|
|
116
|
+
import MCheckbox from '../mozaic/checkbox/MCheckbox.vue';
|
|
117
|
+
import MTextInput from '../mozaic/text-input/MTextInput.vue';
|
|
118
|
+
import { ScenarioStepAnswer } from '@/types/pb/Scenario';
|
|
119
|
+
import { object, string, boolean } from 'yup';
|
|
120
|
+
|
|
121
|
+
export default defineComponent({
|
|
122
|
+
name: 'PbTrezor',
|
|
123
|
+
components: {
|
|
124
|
+
MFlex,
|
|
125
|
+
MButton,
|
|
126
|
+
MTextInput,
|
|
127
|
+
MCheckbox,
|
|
128
|
+
},
|
|
129
|
+
props: {
|
|
130
|
+
/**
|
|
131
|
+
* The component view model and business data as an object. The provided prop
|
|
132
|
+
* is merged with the default payload value so only overriden values will change
|
|
133
|
+
* from the default ones.
|
|
134
|
+
*/
|
|
135
|
+
payload: {
|
|
136
|
+
type: Object,
|
|
137
|
+
default: () => ({}),
|
|
138
|
+
},
|
|
139
|
+
/**
|
|
140
|
+
* The options provided at runtime to customize component behaviour
|
|
141
|
+
*/
|
|
142
|
+
runtimeOptions: {
|
|
143
|
+
type: Object,
|
|
144
|
+
default: () => ({}),
|
|
145
|
+
},
|
|
146
|
+
/**
|
|
147
|
+
* Indicates whether the back button should be displayed
|
|
148
|
+
*/
|
|
149
|
+
showBackButton: {
|
|
150
|
+
type: Boolean,
|
|
151
|
+
default: true,
|
|
152
|
+
},
|
|
153
|
+
/**
|
|
154
|
+
* Name for the event to send when the step is questio is answered
|
|
155
|
+
*/
|
|
156
|
+
completedEventName: {
|
|
157
|
+
type: String,
|
|
158
|
+
default: 'step-completed',
|
|
159
|
+
},
|
|
160
|
+
/**
|
|
161
|
+
* The previous answers to inject
|
|
162
|
+
*/
|
|
163
|
+
answers: {
|
|
164
|
+
type: Object as PropType<Map<string, ScenarioStepAnswer[]>>,
|
|
165
|
+
default: () => new Map<string, ScenarioStepAnswer[]>(),
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
setup(props, { emit }) {
|
|
169
|
+
const store = useStore();
|
|
170
|
+
let formData = ref({
|
|
171
|
+
values: {
|
|
172
|
+
lastname: '',
|
|
173
|
+
firstname: '',
|
|
174
|
+
zipcode: '',
|
|
175
|
+
email: '',
|
|
176
|
+
phone: '',
|
|
177
|
+
optin: false,
|
|
178
|
+
optinPartners: false,
|
|
179
|
+
},
|
|
180
|
+
errors: {
|
|
181
|
+
lastname: '',
|
|
182
|
+
firstname: '',
|
|
183
|
+
zipcode: '',
|
|
184
|
+
email: '',
|
|
185
|
+
phone: '',
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const validationSchema = object({
|
|
190
|
+
lastname: string().required(),
|
|
191
|
+
firstname: string().required(),
|
|
192
|
+
zipcode: string().required(),
|
|
193
|
+
email: string().email().required(),
|
|
194
|
+
phone: string().required(),
|
|
195
|
+
optin: boolean(),
|
|
196
|
+
optinPartners: boolean(),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const validate = async (field: string) => {
|
|
200
|
+
await validationSchema
|
|
201
|
+
.validateAt(field, formData.value.values)
|
|
202
|
+
.then(() => {
|
|
203
|
+
formData.value.errors[field] = '';
|
|
204
|
+
})
|
|
205
|
+
.catch((err) => {
|
|
206
|
+
formData.value.errors[err.path] = err.message;
|
|
207
|
+
});
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const validateAllFields = async (action: any) => {
|
|
211
|
+
await validationSchema.validate(formData.value.values).then(async () => {
|
|
212
|
+
const sending = await store.dispatch(
|
|
213
|
+
'trezor/sendFormData',
|
|
214
|
+
{
|
|
215
|
+
customer: {
|
|
216
|
+
lastname: formData.value.values.lastname,
|
|
217
|
+
firstname: formData.value.values.firstname,
|
|
218
|
+
zipcode: formData.value.values.zipcode,
|
|
219
|
+
email: formData.value.values.email,
|
|
220
|
+
phone: formData.value.values.phone,
|
|
221
|
+
},
|
|
222
|
+
optin: formData.value.values.optin,
|
|
223
|
+
optinPartners: formData.value.values.optinPartners,
|
|
224
|
+
},
|
|
225
|
+
props.payload.viewModel.typeLead,
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
if (sending) {
|
|
229
|
+
emit('step-completed', {
|
|
230
|
+
answers: [],
|
|
231
|
+
nextStep: action,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const callAction = async (item: any) => {
|
|
238
|
+
if (item.action.type === 'submit') {
|
|
239
|
+
await validateAllFields(item);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
formData,
|
|
245
|
+
callAction,
|
|
246
|
+
validate,
|
|
247
|
+
};
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
</script>
|
|
251
|
+
|
|
252
|
+
<style lang="scss" scoped>
|
|
253
|
+
@import 'pb-variables';
|
|
254
|
+
@import 'settings-tools/all-settings';
|
|
255
|
+
@import 'typography/_t.bodys';
|
|
256
|
+
|
|
257
|
+
$small-responsive-breakpoint: 's-large';
|
|
258
|
+
$responsive-breakpoint: 'm';
|
|
259
|
+
|
|
260
|
+
.pb-trezor {
|
|
261
|
+
margin: 0 auto;
|
|
262
|
+
max-width: 1024px;
|
|
263
|
+
&__title {
|
|
264
|
+
@include set-font-face('semi-bold');
|
|
265
|
+
@include set-font-scale('08', 'm');
|
|
266
|
+
max-width: calc(100% - 60px);
|
|
267
|
+
padding: 30px;
|
|
268
|
+
position: relative;
|
|
269
|
+
|
|
270
|
+
@include set-from-screen($responsive-breakpoint) {
|
|
271
|
+
margin: auto;
|
|
272
|
+
padding: $mu250 0 $mu250 0;
|
|
273
|
+
text-align: center;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
&__input {
|
|
278
|
+
width: 100%;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
&__form {
|
|
282
|
+
padding: 30px;
|
|
283
|
+
width: calc(100% - 60px);
|
|
284
|
+
|
|
285
|
+
@include set-from-screen($responsive-breakpoint) {
|
|
286
|
+
margin: auto;
|
|
287
|
+
padding: 0;
|
|
288
|
+
width: 400px;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
&__checkbox {
|
|
292
|
+
align-items: center;
|
|
293
|
+
display: flex;
|
|
294
|
+
justify-content: center;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
&__radio {
|
|
298
|
+
margin-right: 15px;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
&__label {
|
|
302
|
+
@include set-font-face('regular');
|
|
303
|
+
@include set-font-scale('04', 's');
|
|
304
|
+
margin-bottom: $mu050;
|
|
305
|
+
|
|
306
|
+
span {
|
|
307
|
+
@include set-font-face('regular');
|
|
308
|
+
@include set-font-scale('03', 's');
|
|
309
|
+
color: $color-grey-500;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
&__row {
|
|
314
|
+
margin-bottom: $mu050;
|
|
315
|
+
|
|
316
|
+
&.space {
|
|
317
|
+
margin-bottom: $mu150;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
&__col {
|
|
322
|
+
width: 100%;
|
|
323
|
+
|
|
324
|
+
@include set-from-screen($responsive-breakpoint) {
|
|
325
|
+
width: 50%;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
&:first-child {
|
|
329
|
+
padding-right: 15px;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
&:last-child {
|
|
333
|
+
padding-left: 15px;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
&__container--button {
|
|
339
|
+
margin: $mu250 0;
|
|
340
|
+
|
|
341
|
+
button {
|
|
342
|
+
&:first-child {
|
|
343
|
+
margin-right: 15px;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
&:last-child {
|
|
347
|
+
margin-left: 15px;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
&__ok {
|
|
352
|
+
margin-bottom: $mu100;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"viewModel": {
|
|
3
|
+
"backLabel": "Question précédente",
|
|
4
|
+
"label": "Renseignez notre formulaire pour être recontacté",
|
|
5
|
+
"typeLead": "HEAT_PUMP"
|
|
6
|
+
},
|
|
7
|
+
"callToActions": [
|
|
8
|
+
{
|
|
9
|
+
"type": "button",
|
|
10
|
+
"label": "Envoyer",
|
|
11
|
+
"action": {
|
|
12
|
+
"type": "submit"
|
|
13
|
+
},
|
|
14
|
+
"bordered": false
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -20,11 +20,14 @@ export const updateProductApiClient = (config: { apiKey: string; baseUrl: string
|
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
export const getProductById = async (productId: string, storeId: string) => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
try {
|
|
24
|
+
const response = await clientApi.get(`/products/${productId}${storeId ? '?storeId=' + storeId : ''}`);
|
|
25
|
+
return JSON.parse(
|
|
26
|
+
JSON.stringify(response.data).replace(/:"([^"]+)"/g, (match, $1) => {
|
|
27
|
+
return `: "${escape($1)}"`;
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
30
33
|
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { escape } from '../../services/htmlEscape';
|
|
3
|
+
|
|
4
|
+
export const clientApi = axios.create({
|
|
5
|
+
baseURL: '/project-booster/api',
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
if ((<any>window).config) {
|
|
9
|
+
clientApi.defaults.baseURL = (<any>window).config.VUE_APP_DEFAULT_BASE_URL;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const updateTrezorApiClient = (config: { apiKey: string; baseUrl: string; apiHeader: string }) => {
|
|
13
|
+
if (config.apiKey) {
|
|
14
|
+
const header = config.apiHeader ?? 'X-Gateway-ApiKey';
|
|
15
|
+
clientApi.defaults.headers.common[header] = config.apiKey;
|
|
16
|
+
}
|
|
17
|
+
if (config.baseUrl) {
|
|
18
|
+
clientApi.defaults.baseURL = config.baseUrl;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const sendTrezorForm = async (typeLead: string, payload: object) => {
|
|
23
|
+
const response = await clientApi.post(`leads/${typeLead}`, payload);
|
|
24
|
+
|
|
25
|
+
return JSON.parse(
|
|
26
|
+
JSON.stringify(response.data).replace(/:"([^"]+)"/g, (match, $1) => {
|
|
27
|
+
return `: "${escape($1)}"`;
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
30
|
+
};
|
|
@@ -37,12 +37,12 @@ export default {
|
|
|
37
37
|
commit('setRefProduct', ref);
|
|
38
38
|
},
|
|
39
39
|
async loadProduct({ commit, state }: ProductContext, { payload, sessions }: { payload: object; sessions: any }) {
|
|
40
|
-
|
|
41
|
-
commit('setCurrentProduct', payload);
|
|
42
|
-
} else {
|
|
43
|
-
const product = await getProductById(state.refProduct, sessions.storeId);
|
|
40
|
+
const product = await getProductById(state.refProduct, (sessions && sessions.storeId) || null);
|
|
44
41
|
|
|
45
|
-
|
|
42
|
+
if (product) {
|
|
43
|
+
commit('setCurrentProduct', { ...payload, ...product });
|
|
44
|
+
} else {
|
|
45
|
+
commit('setCurrentProduct', payload);
|
|
46
46
|
}
|
|
47
47
|
},
|
|
48
48
|
},
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ActionContext } from 'vuex';
|
|
2
|
+
import { State } from '@/stores/state';
|
|
3
|
+
import { sendTrezorForm } from '@/services/api/trezorApi';
|
|
4
|
+
|
|
5
|
+
export interface TrezorState {
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
7
|
+
typeLead: string;
|
|
8
|
+
formData: object;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type TrezorContext = ActionContext<TrezorState, State>;
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
namespaced: true,
|
|
15
|
+
state: {
|
|
16
|
+
typeLead: '',
|
|
17
|
+
formData: '',
|
|
18
|
+
},
|
|
19
|
+
getters: {},
|
|
20
|
+
mutations: {
|
|
21
|
+
setTypeLead: (state: TrezorState, typeLead: string) => {
|
|
22
|
+
state.typeLead = typeLead;
|
|
23
|
+
},
|
|
24
|
+
setFormData: (state: TrezorState, formData: object) => {
|
|
25
|
+
state.formData = formData;
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
actions: {
|
|
29
|
+
sendFormData: async (
|
|
30
|
+
{ commit, state }: TrezorContext,
|
|
31
|
+
{ payload, typeLead }: { payload: object; typeLead: string },
|
|
32
|
+
) => {
|
|
33
|
+
if (!typeLead) {
|
|
34
|
+
const sendData = await sendTrezorForm(typeLead, payload);
|
|
35
|
+
commit('setFormData', sendData);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
package/src/stores/store.ts
CHANGED
|
@@ -9,6 +9,7 @@ import toolsStore from './modules/toolsStore';
|
|
|
9
9
|
import documentsStore from './modules/documentsStore';
|
|
10
10
|
import consentStore from './modules/consentStore';
|
|
11
11
|
import productsStore from './modules/productsStore';
|
|
12
|
+
import trezorStore from './modules/trezorStore';
|
|
12
13
|
import cloneDeep from 'lodash.clonedeep';
|
|
13
14
|
import { State } from '@/stores/state';
|
|
14
15
|
import { ActionContext } from 'vuex';
|
|
@@ -28,6 +29,7 @@ export default {
|
|
|
28
29
|
documentsPictures: cloneDeep(documentsStore),
|
|
29
30
|
consent: consentStore,
|
|
30
31
|
products: productsStore,
|
|
32
|
+
trezor: trezorStore,
|
|
31
33
|
},
|
|
32
34
|
mutations: {
|
|
33
35
|
eventBusSendEvent(state: State, { code, payload }: { code: string; payload: any }) {
|