erp-city-vue-components 0.1.0
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/README.md +76 -0
- package/package.json +20 -0
- package/src/components/AddressForm.vue +249 -0
- package/src/components/AuthButton.vue +35 -0
- package/src/components/AuthSidebar.vue +218 -0
- package/src/components/CartButton.vue +18 -0
- package/src/components/CartDetails.vue +152 -0
- package/src/components/FavoriteProductCard.vue +35 -0
- package/src/components/FavoritesSidebar.vue +47 -0
- package/src/components/OrderConfirmation.vue +30 -0
- package/src/components/OrdersSidebar.vue +129 -0
- package/src/components/PaymentOrderConfirmation.vue +28 -0
- package/src/components/ProductCard.vue +69 -0
- package/src/components/QuantityInput.vue +44 -0
- package/src/components/Sidebar.vue +66 -0
- package/src/composables/handlesFormErrors.js +39 -0
- package/src/config.js +11 -0
- package/src/index.js +24 -0
- package/src/state/authState.js +189 -0
- package/src/state/cartState.js +106 -0
- package/src/state/favoritesState.js +94 -0
- package/src/state/ordersState.js +42 -0
- package/src/state/productsState.js +16 -0
- package/src/state/sidebarState.js +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# erp-city-vue-components
|
|
2
|
+
|
|
3
|
+
Shared Vue 3 components + reactive state used for ERP storefront flows (auth, cart, checkout, favorites, orders).
|
|
4
|
+
Designed to integrate with `erp-city` and `erp-city-client`:
|
|
5
|
+
|
|
6
|
+
- https://github.com/AntonioPrimera/erp-city
|
|
7
|
+
- https://github.com/AntonioPrimera/erp-city-client
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
Local development (recommended in monorepo-style):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install ../erp-city-vue-components
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or add to your app `package.json`:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"erp-city-vue-components": "file:../erp-city-vue-components"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Then run:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Requirements
|
|
34
|
+
|
|
35
|
+
- Vue 3
|
|
36
|
+
- Axios
|
|
37
|
+
- Ziggy JS (`ziggy-js`)
|
|
38
|
+
- A global `route()` helper (Ziggy) available at runtime
|
|
39
|
+
- A globally registered `v-input` component (used by forms in the sidebar)
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
import { createApp } from 'vue';
|
|
45
|
+
import {
|
|
46
|
+
Sidebar,
|
|
47
|
+
cartState,
|
|
48
|
+
productsState,
|
|
49
|
+
authState,
|
|
50
|
+
favoritesState,
|
|
51
|
+
sidebarState,
|
|
52
|
+
setErpCityUiConfig,
|
|
53
|
+
} from 'erp-city-vue-components';
|
|
54
|
+
|
|
55
|
+
setErpCityUiConfig({
|
|
56
|
+
showFavorites: false,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
productsState.loadProducts();
|
|
60
|
+
cartState.loadCart();
|
|
61
|
+
authState.bootstrap();
|
|
62
|
+
|
|
63
|
+
const app = createApp({});
|
|
64
|
+
app.component('v-sidebar', Sidebar);
|
|
65
|
+
app.mount('#app');
|
|
66
|
+
|
|
67
|
+
// Example usage
|
|
68
|
+
sidebarState.open('cart');
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Exports
|
|
72
|
+
|
|
73
|
+
- Components: `Sidebar`, `CartButton`, `AuthButton`, `AuthSidebar`, `OrdersSidebar`, `FavoritesSidebar`, `CartDetails`, `AddressForm`, `OrderConfirmation`, `PaymentOrderConfirmation`, `ProductCard`, `FavoriteProductCard`, `QuantityInput`
|
|
74
|
+
- State: `authState`, `cartState`, `favoritesState`, `ordersState`, `productsState`, `sidebarState`
|
|
75
|
+
- Composables: `useHandlesFormErrors`
|
|
76
|
+
- Config: `setErpCityUiConfig`, `erpCityUiConfig`
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "erp-city-vue-components",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./src/index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.js",
|
|
8
|
+
"./components/*": "./src/components/*.vue",
|
|
9
|
+
"./state/*": "./src/state/*.js",
|
|
10
|
+
"./composables/*": "./src/composables/*.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src"
|
|
14
|
+
],
|
|
15
|
+
"peerDependencies": {
|
|
16
|
+
"axios": "^1.0.0",
|
|
17
|
+
"vue": "^3.0.0",
|
|
18
|
+
"ziggy-js": "^2.0.0"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
import { sidebarState } from "../state/sidebarState.js";
|
|
4
|
+
import { cartState } from "../state/cartState.js";
|
|
5
|
+
import { authState } from "../state/authState.js";
|
|
6
|
+
import { useHandlesFormErrors } from "../composables/handlesFormErrors.js";
|
|
7
|
+
import { onMounted, computed, reactive, ref, watch } from "vue";
|
|
8
|
+
import { erpCityUiConfig } from "../config.js";
|
|
9
|
+
|
|
10
|
+
const {error, setErrorBag} = useHandlesFormErrors();
|
|
11
|
+
|
|
12
|
+
const addressData = reactive({
|
|
13
|
+
name: null,
|
|
14
|
+
address: null,
|
|
15
|
+
phone: null,
|
|
16
|
+
email: null,
|
|
17
|
+
company: null
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
let loading = ref(false);
|
|
21
|
+
const cardPaymentsEnabled = ref(true);
|
|
22
|
+
const paymentType = ref(cardPaymentsEnabled.value ? 'card' : 'on_delivery');
|
|
23
|
+
const paymentMethodsLoaded = ref(false);
|
|
24
|
+
const hasPrefilledFromProfile = ref(false);
|
|
25
|
+
const isPrefillingProfile = ref(false);
|
|
26
|
+
const profileFieldMap = {
|
|
27
|
+
name: "name",
|
|
28
|
+
email: "email",
|
|
29
|
+
phone: "phone",
|
|
30
|
+
address: "address",
|
|
31
|
+
company: "company",
|
|
32
|
+
};
|
|
33
|
+
const showCompany = computed(() => erpCityUiConfig.showCompany);
|
|
34
|
+
|
|
35
|
+
watch(addressData, (newData) => {
|
|
36
|
+
localStorage.setItem('addressData', JSON.stringify(newData));
|
|
37
|
+
}, { deep: true });
|
|
38
|
+
|
|
39
|
+
const isMissingAddressField = (value) => value === null || value === undefined || value === '';
|
|
40
|
+
|
|
41
|
+
async function prefillAddressFromProfile() {
|
|
42
|
+
if (!authState.isAuthenticated || hasPrefilledFromProfile.value || isPrefillingProfile.value) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
isPrefillingProfile.value = true;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const { data } = await axios.get(route('erp.auth.me'));
|
|
50
|
+
const profile = data?.data;
|
|
51
|
+
|
|
52
|
+
if (!profile) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
Object.entries(profileFieldMap).forEach(([profileKey, addressKey]) => {
|
|
57
|
+
const profileValue = profile[profileKey];
|
|
58
|
+
|
|
59
|
+
if (profileValue && isMissingAddressField(addressData[addressKey])) {
|
|
60
|
+
addressData[addressKey] = profileValue;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
hasPrefilledFromProfile.value = true;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.warn('Failed to prefill address data from ERP profile', error);
|
|
67
|
+
} finally {
|
|
68
|
+
isPrefillingProfile.value = false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function confirmOrder() {
|
|
73
|
+
loading.value = true;
|
|
74
|
+
|
|
75
|
+
axios.post(route('erp.orders.store'), {
|
|
76
|
+
...addressData,
|
|
77
|
+
items: cartState.items,
|
|
78
|
+
payment_type: cartState.price ? paymentType.value : null,
|
|
79
|
+
coupon_code: cartState.coupon?.code ?? null,
|
|
80
|
+
})
|
|
81
|
+
.then((response) => {
|
|
82
|
+
const checkoutUrl = response?.data?.checkout_url;
|
|
83
|
+
if (checkoutUrl) {
|
|
84
|
+
window.location.href = checkoutUrl;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (cartState.price) {
|
|
89
|
+
sidebarState.open('payment-confirmation');
|
|
90
|
+
} else {
|
|
91
|
+
sidebarState.open('confirmation');
|
|
92
|
+
}
|
|
93
|
+
cartState.clearCart();
|
|
94
|
+
localStorage.removeItem('addressData');
|
|
95
|
+
})
|
|
96
|
+
.catch(error => {
|
|
97
|
+
setErrorBag(error);
|
|
98
|
+
console.error("Error saving order:", error);
|
|
99
|
+
})
|
|
100
|
+
.finally(() => {
|
|
101
|
+
loading.value = false;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
onMounted(() => {
|
|
106
|
+
const savedAddress = localStorage.getItem('addressData');
|
|
107
|
+
|
|
108
|
+
if (savedAddress) {
|
|
109
|
+
Object.assign(addressData, JSON.parse(savedAddress));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (authState.isAuthenticated) {
|
|
113
|
+
prefillAddressFromProfile();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
sidebarState.setHeader('Adresa', true);
|
|
117
|
+
loadPaymentMethods();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
watch(
|
|
121
|
+
() => authState.isAuthenticated,
|
|
122
|
+
(isAuthenticated) => {
|
|
123
|
+
if (isAuthenticated) {
|
|
124
|
+
prefillAddressFromProfile();
|
|
125
|
+
} else {
|
|
126
|
+
hasPrefilledFromProfile.value = false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
async function loadPaymentMethods() {
|
|
132
|
+
try {
|
|
133
|
+
const { data } = await axios.get(route('erp.paymentMethods'));
|
|
134
|
+
const cardEnabled = data?.data?.card_enabled;
|
|
135
|
+
if (typeof cardEnabled === 'boolean') {
|
|
136
|
+
cardPaymentsEnabled.value = cardEnabled;
|
|
137
|
+
}
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.warn('Failed to load ERP payment methods', error);
|
|
140
|
+
} finally {
|
|
141
|
+
if (!cardPaymentsEnabled.value) {
|
|
142
|
+
paymentType.value = 'on_delivery';
|
|
143
|
+
}
|
|
144
|
+
paymentMethodsLoaded.value = true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
</script>
|
|
148
|
+
|
|
149
|
+
<template>
|
|
150
|
+
<div class="grid grid-cols-1 gap-4 lg:gap-4">
|
|
151
|
+
<v-input class="col-span-full"
|
|
152
|
+
id="name"
|
|
153
|
+
label="Nume și prenume"
|
|
154
|
+
placeholder="Nume și prenume"
|
|
155
|
+
:error="error('name')"
|
|
156
|
+
autofocus
|
|
157
|
+
v-model="addressData.name"
|
|
158
|
+
></v-input>
|
|
159
|
+
|
|
160
|
+
<v-input v-if="showCompany"
|
|
161
|
+
id="company"
|
|
162
|
+
label="Societatea"
|
|
163
|
+
placeholder="Societatea"
|
|
164
|
+
:error="error('company')"
|
|
165
|
+
v-model="addressData.company"
|
|
166
|
+
></v-input>
|
|
167
|
+
|
|
168
|
+
<v-input id="address"
|
|
169
|
+
label="Adresa"
|
|
170
|
+
placeholder="Adresa"
|
|
171
|
+
:error="error('address')"
|
|
172
|
+
v-model="addressData.address"
|
|
173
|
+
></v-input>
|
|
174
|
+
|
|
175
|
+
<v-input id="phone"
|
|
176
|
+
label="Număr de telefon"
|
|
177
|
+
placeholder="Număr de telefon"
|
|
178
|
+
inputmode="tel"
|
|
179
|
+
:error="error('phone')"
|
|
180
|
+
v-model="addressData.phone"
|
|
181
|
+
></v-input>
|
|
182
|
+
|
|
183
|
+
<v-input id="email"
|
|
184
|
+
label="Email"
|
|
185
|
+
placeholder="Email"
|
|
186
|
+
inputmode="email"
|
|
187
|
+
:error="error('email')"
|
|
188
|
+
v-model="addressData.email"
|
|
189
|
+
></v-input>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<div v-if="cartState.price && paymentMethodsLoaded" class="mt-8">
|
|
193
|
+
<div class="font-medium text-lg lg:text-xl mb-4">Selectează metoda de plată</div>
|
|
194
|
+
|
|
195
|
+
<div class="space-y-4 lg:space-y-6">
|
|
196
|
+
<div
|
|
197
|
+
v-if="cardPaymentsEnabled"
|
|
198
|
+
class="border border-primary p-7 rounded-xl flex items-center cursor-pointer"
|
|
199
|
+
:class="{ 'bg-primary/5': paymentType === 'card' }"
|
|
200
|
+
@click="paymentType = 'card'"
|
|
201
|
+
>
|
|
202
|
+
<div class="mr-5.5">
|
|
203
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
204
|
+
<circle cx="12" cy="12" r="11.25" fill="white" stroke="#2F5233" stroke-width="1.5"/>
|
|
205
|
+
<circle
|
|
206
|
+
cx="12.0007"
|
|
207
|
+
cy="12"
|
|
208
|
+
r="6.66667"
|
|
209
|
+
:fill="paymentType === 'card' ? '#2F5233' : 'white'"
|
|
210
|
+
:stroke="paymentType === 'card' ? '#2F5233' : 'white'"
|
|
211
|
+
/>
|
|
212
|
+
</svg>
|
|
213
|
+
</div>
|
|
214
|
+
<div class="font-medium text-lg lg:text-xl">Card</div>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<div
|
|
218
|
+
class="border border-primary p-7 rounded-xl flex items-center cursor-pointer"
|
|
219
|
+
:class="{ 'bg-primary/5': paymentType === 'on_delivery' }"
|
|
220
|
+
@click="paymentType = 'on_delivery'"
|
|
221
|
+
>
|
|
222
|
+
<div class="mr-5.5">
|
|
223
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
224
|
+
<circle cx="12" cy="12" r="11.25" fill="white" stroke="#2F5233" stroke-width="1.5"/>
|
|
225
|
+
<circle
|
|
226
|
+
cx="12.0007"
|
|
227
|
+
cy="12"
|
|
228
|
+
r="6.66667"
|
|
229
|
+
:fill="paymentType === 'on_delivery' ? '#2F5233' : 'white'"
|
|
230
|
+
:stroke="paymentType === 'on_delivery' ? '#2F5233' : 'white'"
|
|
231
|
+
/>
|
|
232
|
+
</svg>
|
|
233
|
+
</div>
|
|
234
|
+
<div class="font-medium text-lg lg:text-xl">Ramburs</div>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<button type="button"
|
|
240
|
+
class="btn btn-primary btn-full mt-6"
|
|
241
|
+
@click="confirmOrder"
|
|
242
|
+
>
|
|
243
|
+
{{
|
|
244
|
+
cartState.price
|
|
245
|
+
? (cardPaymentsEnabled && paymentType === 'card' ? 'Continuă spre plată' : 'Finalizează')
|
|
246
|
+
: 'Trimite solicitare'
|
|
247
|
+
}}
|
|
248
|
+
</button>
|
|
249
|
+
</template>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import {authState} from "../state/authState.js";
|
|
4
|
+
import {sidebarState} from "../state/sidebarState.js";
|
|
5
|
+
|
|
6
|
+
const iconSrc = '/img/icons/auth.svg';
|
|
7
|
+
const initials = computed(() => {
|
|
8
|
+
const name = authState.user?.name || [authState.user?.first_name, authState.user?.last_name].filter(Boolean).join(' ');
|
|
9
|
+
if (! name) {
|
|
10
|
+
return '';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return name
|
|
14
|
+
.split(' ')
|
|
15
|
+
.filter(Boolean)
|
|
16
|
+
.slice(0, 2)
|
|
17
|
+
.map(part => part[0]?.toUpperCase())
|
|
18
|
+
.join('');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function openAuthSidebar() {
|
|
22
|
+
sidebarState.setHeader(authState.isAuthenticated ? 'Contul meu' : 'Intră în cont', false);
|
|
23
|
+
sidebarState.open('auth');
|
|
24
|
+
}
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<button type="button"
|
|
29
|
+
class="cursor-pointer flex items-center justify-center ml-4"
|
|
30
|
+
:class="{'bg-primary text-white rounded-full h-8 w-8' : authState.isAuthenticated}"
|
|
31
|
+
@click="openAuthSidebar">
|
|
32
|
+
<span v-if="authState.isAuthenticated" class="text-sm font-semibold">{{ initials }}</span>
|
|
33
|
+
<img v-else :src="iconSrc" alt="Profil" class="h-5.5 w-5.5" />
|
|
34
|
+
</button>
|
|
35
|
+
</template>
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed, watch } from 'vue';
|
|
3
|
+
import { authState } from '../state/authState.js';
|
|
4
|
+
import { favoritesState } from '../state/favoritesState.js';
|
|
5
|
+
import { ordersState } from '../state/ordersState.js';
|
|
6
|
+
import { sidebarState } from '../state/sidebarState.js';
|
|
7
|
+
import { erpCityUiConfig } from '../config.js';
|
|
8
|
+
|
|
9
|
+
const mode = ref('login');
|
|
10
|
+
const form = ref({
|
|
11
|
+
email: '',
|
|
12
|
+
password: '',
|
|
13
|
+
password_confirmation: '',
|
|
14
|
+
name: '',
|
|
15
|
+
});
|
|
16
|
+
const formErrors = ref({});
|
|
17
|
+
const generalError = ref('');
|
|
18
|
+
const isLoadingFavorites = computed(() => favoritesState.loading);
|
|
19
|
+
const isLoadingOrders = computed(() => ordersState.loading);
|
|
20
|
+
const showFavorites = computed(() => erpCityUiConfig.showFavorites);
|
|
21
|
+
const showOrders = computed(() => erpCityUiConfig.showOrders);
|
|
22
|
+
|
|
23
|
+
const sidebarTitle = computed(() => {
|
|
24
|
+
if (authState.isAuthenticated) {
|
|
25
|
+
return 'Contul meu';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return mode.value === 'login' ? 'Intră în cont' : 'Creează cont';
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
watch(sidebarTitle, (title) => {
|
|
32
|
+
sidebarState.setHeader(title, false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
sidebarState.setHeader(sidebarTitle.value, false);
|
|
36
|
+
|
|
37
|
+
function resetForm() {
|
|
38
|
+
form.value = {
|
|
39
|
+
email: '',
|
|
40
|
+
password: '',
|
|
41
|
+
password_confirmation: '',
|
|
42
|
+
name: '',
|
|
43
|
+
};
|
|
44
|
+
formErrors.value = {};
|
|
45
|
+
generalError.value = '';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function onSubmit() {
|
|
49
|
+
formErrors.value = {};
|
|
50
|
+
generalError.value = '';
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
if (mode.value === 'login') {
|
|
54
|
+
await authState.login({
|
|
55
|
+
email: form.value.email,
|
|
56
|
+
password: form.value.password,
|
|
57
|
+
});
|
|
58
|
+
} else {
|
|
59
|
+
await authState.register({
|
|
60
|
+
name: form.value.name,
|
|
61
|
+
email: form.value.email,
|
|
62
|
+
password: form.value.password,
|
|
63
|
+
password_confirmation: form.value.password_confirmation,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const nextView = sidebarState.nextView;
|
|
68
|
+
sidebarState.nextView = null;
|
|
69
|
+
resetForm();
|
|
70
|
+
|
|
71
|
+
if (nextView) {
|
|
72
|
+
sidebarState.open(nextView);
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
const responseData = error.response?.data;
|
|
76
|
+
const responseErrors = responseData?.errors;
|
|
77
|
+
if (responseErrors) {
|
|
78
|
+
formErrors.value = Object.fromEntries(
|
|
79
|
+
Object.entries(responseErrors).map(([key, messages]) => [key, messages.join(' ')])
|
|
80
|
+
);
|
|
81
|
+
} else if (responseData?.message) {
|
|
82
|
+
generalError.value = responseData.message;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function switchMode(newMode) {
|
|
88
|
+
mode.value = newMode;
|
|
89
|
+
resetForm();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function viewFavorites() {
|
|
93
|
+
if (! showFavorites.value) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
favoritesState.fetchFavorites();
|
|
97
|
+
sidebarState.setHeader('Produse favorite', true);
|
|
98
|
+
sidebarState.open('favorites');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function viewOrders() {
|
|
102
|
+
if (! showOrders.value) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
ordersState.fetchOrders();
|
|
106
|
+
sidebarState.setHeader('Comenzile mele', true);
|
|
107
|
+
sidebarState.open('orders');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function onLogout() {
|
|
111
|
+
await authState.logout();
|
|
112
|
+
sidebarState.close();
|
|
113
|
+
}
|
|
114
|
+
</script>
|
|
115
|
+
|
|
116
|
+
<template>
|
|
117
|
+
<div class="flex flex-col">
|
|
118
|
+
<template v-if="!authState.isAuthenticated">
|
|
119
|
+
<form @submit.prevent="onSubmit" class="flex flex-col flex-1">
|
|
120
|
+
<div class="space-y-4">
|
|
121
|
+
<div v-if="mode === 'register'">
|
|
122
|
+
<v-input
|
|
123
|
+
id="name"
|
|
124
|
+
label="Nume complet"
|
|
125
|
+
:error="formErrors.name"
|
|
126
|
+
placeholder="Introduceți numele"
|
|
127
|
+
v-model="form.name"
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<v-input
|
|
132
|
+
id="email"
|
|
133
|
+
label="Email"
|
|
134
|
+
:error="formErrors.email"
|
|
135
|
+
placeholder="email@example.com"
|
|
136
|
+
type="email"
|
|
137
|
+
v-model="form.email"
|
|
138
|
+
/>
|
|
139
|
+
|
|
140
|
+
<v-input
|
|
141
|
+
id="password"
|
|
142
|
+
label="Parolă"
|
|
143
|
+
:error="formErrors.password"
|
|
144
|
+
placeholder="Parola"
|
|
145
|
+
type="password"
|
|
146
|
+
v-model="form.password"
|
|
147
|
+
/>
|
|
148
|
+
|
|
149
|
+
<div v-if="mode === 'register'">
|
|
150
|
+
<v-input
|
|
151
|
+
id="password_confirmation"
|
|
152
|
+
label="Confirmă parola"
|
|
153
|
+
:error="formErrors.password_confirmation"
|
|
154
|
+
placeholder="Reintroduceți parola"
|
|
155
|
+
type="password"
|
|
156
|
+
v-model="form.password_confirmation"
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div class="mt-6 mb-2 text-right text-sm text-gray-600">
|
|
161
|
+
<div v-if="mode === 'register'">
|
|
162
|
+
Ai deja un cont? <span class="underline cursor-pointer" @click="switchMode('login')">Intră în cont</span>
|
|
163
|
+
</div>
|
|
164
|
+
<div v-else>
|
|
165
|
+
Nu ai un cont? <span class="underline cursor-pointer" @click="switchMode('register')">Înregistrează-te</span>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<div v-if="generalError" class="text-red-600 text-sm mb-4">
|
|
171
|
+
{{ generalError }}
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<button type="submit" class="btn btn-primary btn-full mt-auto" :disabled="authState.status === 'loading'">
|
|
175
|
+
{{ mode === 'login' ? 'Autentifică-te' : 'Înregistrează-te' }}
|
|
176
|
+
</button>
|
|
177
|
+
</form>
|
|
178
|
+
</template>
|
|
179
|
+
|
|
180
|
+
<template v-else>
|
|
181
|
+
<div class="h-full flex flex-col">
|
|
182
|
+
<div class="-mt-10 mb-4">
|
|
183
|
+
<p class="text-sm text-gray-500 mb-1">Ești autentificat ca</p>
|
|
184
|
+
<p class="text-lg font-semibold">{{ authState.user?.name || [authState.user?.first_name, authState.user?.last_name].filter(Boolean).join(' ') }}</p>
|
|
185
|
+
<p class="text-gray-700">{{ authState.user?.email }}</p>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<div v-if="showFavorites" class="flex items-center justify-between pb-8 pt-8 border-b border-b-neutral-200">
|
|
189
|
+
<h4 class="text-lg font-semibold">
|
|
190
|
+
Produse favorite
|
|
191
|
+
<span class="text-neutral-400 font-normal">
|
|
192
|
+
({{ favoritesState.ids.length }})
|
|
193
|
+
</span>
|
|
194
|
+
</h4>
|
|
195
|
+
<button type="button" class="text-primary underline" @click="viewFavorites" :disabled="isLoadingFavorites">
|
|
196
|
+
Vezi
|
|
197
|
+
</button>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<div v-if="showOrders" class="flex items-center justify-between pb-8 pt-8 border-b border-b-neutral-200">
|
|
201
|
+
<h4 class="text-lg font-semibold">
|
|
202
|
+
Comenzile mele
|
|
203
|
+
<span class="text-neutral-400 font-normal">
|
|
204
|
+
({{ ordersState.orders.length }})
|
|
205
|
+
</span>
|
|
206
|
+
</h4>
|
|
207
|
+
<button type="button" class="text-primary underline" @click="viewOrders" :disabled="isLoadingOrders">
|
|
208
|
+
Vezi
|
|
209
|
+
</button>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
<button class="btn btn-primary btn-full mt-8" @click="onLogout" :disabled="authState.status === 'loading'">
|
|
214
|
+
Deconectează-te
|
|
215
|
+
</button>
|
|
216
|
+
</template>
|
|
217
|
+
</div>
|
|
218
|
+
</template>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { cartState, sidebarState } from "erp-city-vue-components";
|
|
3
|
+
|
|
4
|
+
const cartIconSrc = '/img/icons/cart.svg';
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<div class="block relative ml-auto cursor-pointer"
|
|
9
|
+
@click="sidebarState.open('cart')"
|
|
10
|
+
>
|
|
11
|
+
<div v-if="cartState.count"
|
|
12
|
+
class="absolute bottom-3 left-3 flex items-center justify-center rounded-full bg-[#C73838] w-5.5 h-5.5 text-white text-center"
|
|
13
|
+
>
|
|
14
|
+
{{ cartState.count}}
|
|
15
|
+
</div>
|
|
16
|
+
<img :src="cartIconSrc" alt="Cart">
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|