@monkeyplus/payscope 1.0.2 → 1.0.3
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/dist/_chunks/database.mjs +1 -1
- package/dist/_chunks/db.d.mts +9 -5
- package/dist/_chunks/taxes.mjs +214 -0
- package/dist/server/router.d.mts +8 -5
- package/dist/server/router.mjs +19 -218
- package/dist/server/taxes.d.mts +131 -0
- package/dist/server/taxes.mjs +2 -0
- package/package.json +1 -1
- package/storefront/cart/ShoppinCart.vue +3 -12
- package/storefront/cart/ShoppinCartItem.vue +13 -11
- package/storefront/checkout/App.vue +1 -1
- package/storefront/checkout/AppCart.vue +31 -24
- package/storefront/checkout/AppCartTotals.vue +19 -15
- package/storefront/checkout/pages/Auth.vue +8 -0
- package/storefront/checkout/pages/Basic/BasicBillingForm.vue +95 -0
- package/storefront/checkout/pages/Basic/BasicCustomerForm.vue +188 -0
- package/storefront/checkout/pages/Basic/BasicShippingForm.vue +93 -0
- package/storefront/checkout/pages/Basic/BasicSummary.vue +63 -0
- package/storefront/checkout/pages/Basic.vue +82 -0
- package/storefront/checkout/pages/Pay/Pay.vue +3 -50
- package/storefront/checkout/pages/Pay/PayForms.vue +55 -0
- package/storefront/checkout/pages/Payment/Payment.vue +1 -1
- package/storefront/checkout/router.ts +49 -39
- package/storefront/product/AddProduct.vue +87 -103
- package/storefront/product/composables.ts +0 -0
- package/storefront/stores.ts +26 -4
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
interface TypeTax {
|
|
2
|
+
/**
|
|
3
|
+
* Codigo del typo de impuesto
|
|
4
|
+
* ej.
|
|
5
|
+
*/
|
|
6
|
+
code: string;
|
|
7
|
+
/**
|
|
8
|
+
* Nombre del impuesto
|
|
9
|
+
* ej. IVA,ICE
|
|
10
|
+
*/
|
|
11
|
+
name: string;
|
|
12
|
+
}
|
|
13
|
+
interface TaxItem {
|
|
14
|
+
/**
|
|
15
|
+
* Valor de impuesto en porcentaje
|
|
16
|
+
*/
|
|
17
|
+
value: string | number;
|
|
18
|
+
/**
|
|
19
|
+
* Codigo de impuesto
|
|
20
|
+
*/
|
|
21
|
+
code: string;
|
|
22
|
+
/**
|
|
23
|
+
* Descripcion del impuesto
|
|
24
|
+
*/
|
|
25
|
+
description: string;
|
|
26
|
+
/**
|
|
27
|
+
* Impuesto incluido
|
|
28
|
+
*/
|
|
29
|
+
included: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Calcular impuesto antes de descuento
|
|
32
|
+
*/
|
|
33
|
+
beforeTaxes?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
*
|
|
36
|
+
*/
|
|
37
|
+
excludeDiscount?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Tipo de impuesto
|
|
40
|
+
*/
|
|
41
|
+
type: TypeTax;
|
|
42
|
+
}
|
|
43
|
+
interface TaxItemFull extends TaxItem {
|
|
44
|
+
amount: number;
|
|
45
|
+
totalAmount: number;
|
|
46
|
+
base: number;
|
|
47
|
+
totalBase: number;
|
|
48
|
+
}
|
|
49
|
+
type Discount = {
|
|
50
|
+
percent: string | number;
|
|
51
|
+
} | {
|
|
52
|
+
amount: string | number;
|
|
53
|
+
};
|
|
54
|
+
interface ItemTable {
|
|
55
|
+
id: any;
|
|
56
|
+
title: string;
|
|
57
|
+
taxes: TaxItem[];
|
|
58
|
+
taxIncluded: boolean;
|
|
59
|
+
quantity: string | number;
|
|
60
|
+
price: string | number;
|
|
61
|
+
/**
|
|
62
|
+
* Valor del descuento en porcentaje
|
|
63
|
+
*/
|
|
64
|
+
discount: Discount;
|
|
65
|
+
product?: any;
|
|
66
|
+
productId?: any;
|
|
67
|
+
variant?: any;
|
|
68
|
+
discountAllocations?: any[];
|
|
69
|
+
}
|
|
70
|
+
interface ItemDiscount {
|
|
71
|
+
amount: number;
|
|
72
|
+
percent?: number;
|
|
73
|
+
}
|
|
74
|
+
interface Amount {
|
|
75
|
+
price: number;
|
|
76
|
+
unitAmount: number;
|
|
77
|
+
unitBase: number;
|
|
78
|
+
discounts: {
|
|
79
|
+
itemDiscount: ItemDiscount;
|
|
80
|
+
parentDiscount: ItemDiscount;
|
|
81
|
+
};
|
|
82
|
+
total?: number;
|
|
83
|
+
}
|
|
84
|
+
interface Item {
|
|
85
|
+
id: any;
|
|
86
|
+
title: string;
|
|
87
|
+
taxIncluded: boolean;
|
|
88
|
+
item: Amount;
|
|
89
|
+
taxes: TaxItemFull[];
|
|
90
|
+
variant: {
|
|
91
|
+
id: any;
|
|
92
|
+
[key: string]: any;
|
|
93
|
+
};
|
|
94
|
+
product: {
|
|
95
|
+
id: any;
|
|
96
|
+
[key: string]: any;
|
|
97
|
+
};
|
|
98
|
+
discountAllocations?: any[];
|
|
99
|
+
quantity: number;
|
|
100
|
+
productId?: any;
|
|
101
|
+
category?: string;
|
|
102
|
+
}
|
|
103
|
+
interface Invoice {
|
|
104
|
+
total: number;
|
|
105
|
+
base: number;
|
|
106
|
+
items: Item[];
|
|
107
|
+
lineItemsSubtotalPrice: number;
|
|
108
|
+
shipping: number;
|
|
109
|
+
totalTax: number;
|
|
110
|
+
totalDiscount: number;
|
|
111
|
+
taxes: {
|
|
112
|
+
/**
|
|
113
|
+
* Codigo de tipo de impuesto
|
|
114
|
+
*/
|
|
115
|
+
type: string;
|
|
116
|
+
/**
|
|
117
|
+
* Codigo de impuesto
|
|
118
|
+
*/
|
|
119
|
+
code: string;
|
|
120
|
+
base: number;
|
|
121
|
+
value: number;
|
|
122
|
+
rate: number;
|
|
123
|
+
title?: string;
|
|
124
|
+
}[];
|
|
125
|
+
}
|
|
126
|
+
declare function calcAmount(_itemDiscount: Discount, _parentDiscount: Discount, taxIncluded?: boolean): (price: number) => Amount;
|
|
127
|
+
declare function calcSingleItem(parentDiscount?: Discount): (item: ItemTable) => Item;
|
|
128
|
+
declare function calcItems(items: ItemTable[], parentDiscount?: Discount): Item[];
|
|
129
|
+
declare function buildInvoice(items: Item[]): Invoice;
|
|
130
|
+
declare function calculateFormula(expression: string, variables: Record<string, number>): number;
|
|
131
|
+
export { Amount, Discount, Invoice, Item, ItemDiscount, ItemTable, TaxItem, TaxItemFull, TypeTax, buildInvoice, calcAmount, calcItems, calcSingleItem, calculateFormula };
|
package/package.json
CHANGED
|
@@ -3,24 +3,15 @@ import { ref } from 'vue';
|
|
|
3
3
|
import { useCartStore } from '../stores';
|
|
4
4
|
import ShoppinCartItem from './ShoppinCartItem.vue';
|
|
5
5
|
|
|
6
|
-
const toast = useToast();
|
|
6
|
+
// const toast = useToast();
|
|
7
7
|
const cart = useCartStore();
|
|
8
8
|
const loading = ref(false);
|
|
9
|
-
function onOpen() {
|
|
10
|
-
console.log('toast');
|
|
11
|
-
cart.drawer = !cart.drawer;
|
|
12
|
-
// toast.add({
|
|
13
|
-
// title: 'Event added to calendar',
|
|
14
|
-
// description: `This event is scheduled.`,
|
|
15
|
-
// icon: 'i-lucide-calendar-days',
|
|
16
|
-
// });
|
|
17
|
-
}
|
|
18
9
|
</script>
|
|
19
10
|
|
|
20
11
|
<template>
|
|
21
12
|
<!-- <UApp> -->
|
|
22
|
-
<UDrawer direction="right">
|
|
23
|
-
<UButton icon="i-bytesize-cart" size="xl" variant="ghost" class="mx-3"
|
|
13
|
+
<UDrawer v-model:open="cart.open" direction="right">
|
|
14
|
+
<UButton icon="i-bytesize-cart" size="xl" variant="ghost" class="mx-3" />
|
|
24
15
|
|
|
25
16
|
<template #content>
|
|
26
17
|
<div
|
|
@@ -55,17 +55,19 @@ function onUpdate(id: string): void {
|
|
|
55
55
|
{{ item?.variant?.price }}
|
|
56
56
|
</div>
|
|
57
57
|
</div>
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
58
|
+
<div v-if="!item?.noVariants">
|
|
59
|
+
<template
|
|
60
|
+
v-for="(option) in item?.variant?.selectedOptions || []"
|
|
61
|
+
:key="option.name"
|
|
62
|
+
>
|
|
63
|
+
<div v-show="option.name !== 'x'" class="text-gray-900">
|
|
64
|
+
<span class="font-bold"> {{ option.name }}: </span>
|
|
65
|
+
<span>
|
|
66
|
+
{{ option.value }}
|
|
67
|
+
</span>
|
|
68
|
+
</div>
|
|
69
|
+
</template>
|
|
70
|
+
</div>
|
|
69
71
|
<div class="flex-auto" />
|
|
70
72
|
<div class="text-gray-800 pt-1">
|
|
71
73
|
<div v-show="item.variant?.available">
|
|
@@ -15,23 +15,21 @@ const checkout = useCheckoutStore();
|
|
|
15
15
|
|
|
16
16
|
<template>
|
|
17
17
|
<div>
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
border-gray-200
|
|
24
|
-
overflow-hidden overflow-visible
|
|
25
|
-
"
|
|
26
|
-
>
|
|
18
|
+
<h3 class="text-xl font-bold mb-4 text-gray-800">
|
|
19
|
+
Resumen de tu pedido
|
|
20
|
+
</h3>
|
|
21
|
+
<div v-for="item in checkout.items" :key="item.id" class="flex items-center w-full gap-4">
|
|
22
|
+
<div>
|
|
27
23
|
<div class="relative">
|
|
28
|
-
<div
|
|
29
|
-
class=" h-[4.5rem] w-[4.5rem] min-w-[4.5rem] bg-gray-50 flex justify-center items-center"
|
|
30
|
-
>
|
|
24
|
+
<div class="w-16 h-16 rounded-md overflow-hidden bg-white border border-gray-200 flex-shrink-0">
|
|
31
25
|
<img
|
|
26
|
+
v-if="item?.image?.src"
|
|
32
27
|
:src="item?.image?.src"
|
|
33
28
|
alt="h-full w-full object-containt"
|
|
34
29
|
>
|
|
30
|
+
<div v-else class="w-full h-full flex items-center justify-center text-gray-400">
|
|
31
|
+
<i-mdi-image-outline class="text-2xl" />
|
|
32
|
+
</div>
|
|
35
33
|
</div>
|
|
36
34
|
<div
|
|
37
35
|
class="
|
|
@@ -50,20 +48,29 @@ const checkout = useCheckoutStore();
|
|
|
50
48
|
</div>
|
|
51
49
|
</div>
|
|
52
50
|
</div>
|
|
53
|
-
<div class="flex-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
51
|
+
<div class="flex-1 flex justify-between">
|
|
52
|
+
<div>
|
|
53
|
+
<div class="font-medium text-gray-800 text-sm leading-tight">
|
|
54
|
+
{{ item.title }}
|
|
55
|
+
</div>
|
|
56
|
+
<div
|
|
57
|
+
v-for="(option) in item.options"
|
|
58
|
+
:key="option.name"
|
|
59
|
+
class="text-xs text-gray-500 mt-1"
|
|
60
|
+
>
|
|
61
|
+
<span class="font-bold"> {{ option.name }}: </span>
|
|
62
|
+
<span>
|
|
63
|
+
{{ option.value }}
|
|
64
|
+
</span>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="text-xs text-gray-400 mt-1">
|
|
67
|
+
Cant: {{ item.quantity }}
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="font-bold text-gray-800 text-sm">
|
|
71
|
+
$ {{ item.total }}
|
|
64
72
|
</div>
|
|
65
73
|
</div>
|
|
66
|
-
<div>$ {{ item.total }}</div>
|
|
67
74
|
</div>
|
|
68
75
|
<div class="h-[2px] bg-gray-200 my-8" />
|
|
69
76
|
<AppCartDiscount />
|
|
@@ -11,7 +11,7 @@ const taxIncluded = ref(false);
|
|
|
11
11
|
<div class="pt-3 space-y-1">
|
|
12
12
|
<div v-if="checkout.invoice?.discount || checkout.invoice?.discounts?.length" class="border-b mb-2 pb-1">
|
|
13
13
|
<div class="flex">
|
|
14
|
-
<div>
|
|
14
|
+
<div class="text-gray-500">
|
|
15
15
|
Total
|
|
16
16
|
<span class="text-xs">(Sin descuentos)</span>
|
|
17
17
|
</div>
|
|
@@ -27,45 +27,49 @@ const taxIncluded = ref(false);
|
|
|
27
27
|
</div>
|
|
28
28
|
</div>
|
|
29
29
|
<div class="flex">
|
|
30
|
-
<div>
|
|
30
|
+
<div class="text-gray-500 text-sm">
|
|
31
|
+
Subtotal
|
|
32
|
+
</div>
|
|
31
33
|
<div class="flex-auto" />
|
|
32
|
-
<div>
|
|
33
|
-
{{ checkout.totals.base }}
|
|
34
|
+
<div class="font-medium">
|
|
35
|
+
${{ checkout.totals.base }}
|
|
34
36
|
</div>
|
|
35
37
|
</div>
|
|
36
38
|
<div class="flex">
|
|
37
|
-
<div>
|
|
39
|
+
<div class="text-gray-500 text-sm">
|
|
38
40
|
Impuestos
|
|
39
41
|
</div>
|
|
40
42
|
<div class="flex-auto" />
|
|
41
|
-
<div v-if="!taxIncluded">
|
|
42
|
-
{{ checkout.totals.totalTax }}
|
|
43
|
+
<div v-if="!taxIncluded" class="font-medium">
|
|
44
|
+
${{ checkout.totals.totalTax }}
|
|
43
45
|
</div>
|
|
44
46
|
<div v-else class="italic text-sm">
|
|
45
47
|
Ya incluidos
|
|
46
48
|
</div>
|
|
47
49
|
</div>
|
|
48
50
|
<div class="flex">
|
|
49
|
-
<div>
|
|
51
|
+
<div class="text-gray-500 text-sm">
|
|
52
|
+
Envio
|
|
53
|
+
</div>
|
|
50
54
|
<div class="flex-auto" />
|
|
51
|
-
<div>
|
|
52
|
-
{{ checkout.totals.shipping }}
|
|
55
|
+
<div class="font-medium">
|
|
56
|
+
${{ checkout.totals.shipping }}
|
|
53
57
|
</div>
|
|
54
58
|
</div>
|
|
55
59
|
</div>
|
|
56
60
|
<div class="border-b-1 border-black opacity-10 my-4 border" />
|
|
57
|
-
<div class="flex">
|
|
61
|
+
<div class="flex items-center font-bold">
|
|
58
62
|
<div>
|
|
59
|
-
<div class="
|
|
63
|
+
<div class=" text-gray-800">
|
|
60
64
|
Total
|
|
61
65
|
</div>
|
|
62
66
|
</div>
|
|
63
67
|
<div class="flex-auto" />
|
|
64
|
-
<div class="text-
|
|
65
|
-
<span class="text-sm">
|
|
68
|
+
<div class="text-xl text-gray-900">
|
|
69
|
+
<span class="text-sm text-gray-500">
|
|
66
70
|
USD
|
|
67
71
|
</span>
|
|
68
|
-
{{ checkout.totals.total }}
|
|
72
|
+
${{ checkout.totals.total }}
|
|
69
73
|
</div>
|
|
70
74
|
</div>
|
|
71
75
|
</div>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { reactive, ref } from 'vue';
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{
|
|
5
|
+
step: number;
|
|
6
|
+
isActive: boolean;
|
|
7
|
+
}>();
|
|
8
|
+
|
|
9
|
+
const emit = defineEmits(['next', 'edit']);
|
|
10
|
+
|
|
11
|
+
const requireBilling = ref(false);
|
|
12
|
+
|
|
13
|
+
const form = reactive({
|
|
14
|
+
rfc: '',
|
|
15
|
+
name: '',
|
|
16
|
+
address: ''
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const isComplete = ref(false);
|
|
20
|
+
|
|
21
|
+
function submit() {
|
|
22
|
+
if (!requireBilling.value) {
|
|
23
|
+
isComplete.value = true;
|
|
24
|
+
emit('next');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (form.rfc && form.name) {
|
|
28
|
+
isComplete.value = true;
|
|
29
|
+
emit('next');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<template>
|
|
35
|
+
<div class="bg-white p-6 rounded-xl border shadow-sm transition-colors duration-300" :class="isActive ? 'border-primary' : 'border-gray-100'">
|
|
36
|
+
<div class="flex items-center justify-between mb-2">
|
|
37
|
+
<div class="flex items-center space-x-3">
|
|
38
|
+
<div class="w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm transition-colors" :class="isActive ? 'bg-primary text-white' : (isComplete ? 'bg-green-500 text-white' : 'bg-slate-100 text-slate-600')">
|
|
39
|
+
<i-mdi-check v-if="!isActive && isComplete" class="text-lg" />
|
|
40
|
+
<span v-else>{{ step }}</span>
|
|
41
|
+
</div>
|
|
42
|
+
<h3 class="text-xl font-bold text-gray-800">
|
|
43
|
+
Datos de Facturación
|
|
44
|
+
</h3>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="flex items-center space-x-2" v-if="isActive">
|
|
47
|
+
<span class="text-sm text-gray-500 font-medium">Requerir factura</span>
|
|
48
|
+
<UToggle v-model="requireBilling" color="primary" />
|
|
49
|
+
</div>
|
|
50
|
+
<UButton v-else-if="isComplete" variant="ghost" color="gray" icon="i-heroicons-pencil-square" size="sm" @click="$emit('edit')">Editar</UButton>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="pl-11 pt-2 transition-all">
|
|
54
|
+
<div v-if="isActive" class="animate-fade-in">
|
|
55
|
+
<form @submit.prevent="submit" class="space-y-4">
|
|
56
|
+
<div v-if="requireBilling" class="animate-fade-in space-y-4 mt-2">
|
|
57
|
+
<UFormField label="RFC / Identificación" required>
|
|
58
|
+
<UInput v-model="form.rfc" placeholder="Ej. XAXX010101000" />
|
|
59
|
+
</UFormField>
|
|
60
|
+
<UFormField label="Razón Social / Nombre" required>
|
|
61
|
+
<UInput v-model="form.name" placeholder="Razón Social" />
|
|
62
|
+
</UFormField>
|
|
63
|
+
<UFormField label="Dirección Fiscal">
|
|
64
|
+
<UInput v-model="form.address" placeholder="Dirección completa" />
|
|
65
|
+
</UFormField>
|
|
66
|
+
</div>
|
|
67
|
+
<div v-else class="text-sm text-gray-500 italic mb-4 mt-2">
|
|
68
|
+
Has seleccionado que no requieres factura.
|
|
69
|
+
</div>
|
|
70
|
+
<div class="pt-4 flex justify-end">
|
|
71
|
+
<UButton type="submit" size="lg">Guardar y Continuar</UButton>
|
|
72
|
+
</div>
|
|
73
|
+
</form>
|
|
74
|
+
</div>
|
|
75
|
+
<div v-else-if="isComplete" class="text-sm text-gray-600 space-y-1 animate-fade-in">
|
|
76
|
+
<template v-if="requireBilling">
|
|
77
|
+
<p class="font-medium text-gray-900">{{ form.name }}</p>
|
|
78
|
+
<p>RFC: {{ form.rfc }}</p>
|
|
79
|
+
<p v-if="form.address">{{ form.address }}</p>
|
|
80
|
+
</template>
|
|
81
|
+
<p v-else class="italic">No se requiere factura</p>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</template>
|
|
86
|
+
|
|
87
|
+
<style scoped>
|
|
88
|
+
.animate-fade-in {
|
|
89
|
+
animation: fadeIn 0.3s ease-in-out;
|
|
90
|
+
}
|
|
91
|
+
@keyframes fadeIn {
|
|
92
|
+
from { opacity: 0; transform: translateY(-5px); }
|
|
93
|
+
to { opacity: 1; transform: translateY(0); }
|
|
94
|
+
}
|
|
95
|
+
</style>
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useAsyncState } from '@vueuse/core';
|
|
3
|
+
import { vMaska } from 'maska/vue';
|
|
4
|
+
import { ofetch } from 'ofetch';
|
|
5
|
+
import { computed, reactive, ref, watch } from 'vue';
|
|
6
|
+
|
|
7
|
+
defineProps<{
|
|
8
|
+
step: number
|
|
9
|
+
isActive: boolean
|
|
10
|
+
}>();
|
|
11
|
+
|
|
12
|
+
const emit = defineEmits(['next', 'edit']);
|
|
13
|
+
|
|
14
|
+
const form = reactive({
|
|
15
|
+
firstName: '',
|
|
16
|
+
lastName: '',
|
|
17
|
+
email: '',
|
|
18
|
+
phone: '',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
interface PhoneCode {
|
|
22
|
+
name: string
|
|
23
|
+
code: string
|
|
24
|
+
emoji: string
|
|
25
|
+
dialCode: string
|
|
26
|
+
mask: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const countryCode = ref('EC'); // Default country (Example: Mexico or US)
|
|
30
|
+
|
|
31
|
+
// Adapted from useLazyFetch to useAsyncState since we are in a Vue3/Vite env
|
|
32
|
+
const { state: phoneCodes, isLoading: statusPending, execute } = useAsyncState(async () => {
|
|
33
|
+
return await ofetch<PhoneCode[]>('/api/phone-codes.json').catch(() => {
|
|
34
|
+
// Fallback data in case the endpoint doesn't exist
|
|
35
|
+
return [
|
|
36
|
+
{ name: 'United States', code: 'US', emoji: '🇺🇸', dialCode: '+1', mask: '(###) ###-####' },
|
|
37
|
+
{ name: 'Mexico', code: 'MX', emoji: '🇲🇽', dialCode: '+52', mask: '## #### ####' },
|
|
38
|
+
{ name: 'Spain', code: 'ES', emoji: '🇪🇸', dialCode: '+34', mask: '### ### ###' },
|
|
39
|
+
{ name: 'Colombia', code: 'CO', emoji: '🇨🇴', dialCode: '+57', mask: '### ### ####' },
|
|
40
|
+
{ name: 'Argentina', code: 'AR', emoji: '🇦🇷', dialCode: '+54', mask: '## #### ####' },
|
|
41
|
+
{ name: 'Ecuador', code: 'EC', emoji: '🇪🇨', dialCode: '+593', mask: '### ### ###' },
|
|
42
|
+
];
|
|
43
|
+
});
|
|
44
|
+
}, [], {
|
|
45
|
+
immediate: false,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const country = computed(() => phoneCodes.value?.find(c => c.code === countryCode.value));
|
|
49
|
+
const dialCode = computed(() => country.value?.dialCode || '+52');
|
|
50
|
+
const mask = computed(() => country.value?.mask || '## #### ####');
|
|
51
|
+
|
|
52
|
+
function onOpen() {
|
|
53
|
+
if (!phoneCodes.value?.length) {
|
|
54
|
+
execute();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
watch(countryCode, () => {
|
|
59
|
+
form.phone = '';
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const isComplete = ref(false);
|
|
63
|
+
|
|
64
|
+
function submit() {
|
|
65
|
+
if (form.firstName && form.email) {
|
|
66
|
+
isComplete.value = true;
|
|
67
|
+
// Here you would normally commit this info to the store
|
|
68
|
+
emit('next');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
</script>
|
|
72
|
+
|
|
73
|
+
<template>
|
|
74
|
+
<div class="bg-white p-6 rounded-xl border shadow-sm transition-colors duration-300" :class="isActive ? 'border-primary' : 'border-gray-100'">
|
|
75
|
+
<div class="flex items-center justify-between mb-2">
|
|
76
|
+
<div class="flex items-center space-x-3">
|
|
77
|
+
<div class="w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm transition-colors" :class="isActive ? 'bg-primary text-white' : (isComplete ? 'bg-green-500 text-white' : 'bg-slate-100 text-slate-600')">
|
|
78
|
+
<i-mdi-check v-if="!isActive && isComplete" class="text-lg" />
|
|
79
|
+
<span v-else>{{ step }}</span>
|
|
80
|
+
</div>
|
|
81
|
+
<h3 class="text-xl font-bold text-gray-800">
|
|
82
|
+
Información de Contacto
|
|
83
|
+
</h3>
|
|
84
|
+
</div>
|
|
85
|
+
<UButton v-if="!isActive && isComplete" variant="ghost" color="gray" icon="i-heroicons-pencil-square" size="sm" @click="$emit('edit')">
|
|
86
|
+
Editar
|
|
87
|
+
</UButton>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div class="pl-11 pt-2 transition-all">
|
|
91
|
+
<div v-if="isActive" class="animate-fade-in">
|
|
92
|
+
<form class="space-y-4" @submit.prevent="submit">
|
|
93
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
94
|
+
<UFormField label="Nombre" required>
|
|
95
|
+
<UInput v-model="form.firstName" placeholder="Tu nombre" />
|
|
96
|
+
</UFormField>
|
|
97
|
+
<UFormField label="Apellido">
|
|
98
|
+
<UInput v-model="form.lastName" placeholder="Tu apellido" />
|
|
99
|
+
</UFormField>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
102
|
+
<UFormField label="Correo Electrónico" required>
|
|
103
|
+
<UInput v-model="form.email" type="email" placeholder="correo@ejemplo.com" />
|
|
104
|
+
</UFormField>
|
|
105
|
+
<UFormField label="Teléfono">
|
|
106
|
+
<div class="flex">
|
|
107
|
+
<USelectMenu
|
|
108
|
+
v-model="countryCode"
|
|
109
|
+
:items="phoneCodes"
|
|
110
|
+
value-key="code"
|
|
111
|
+
:search-input="{
|
|
112
|
+
placeholder: 'Search country...',
|
|
113
|
+
icon: 'i-lucide-search',
|
|
114
|
+
loading: statusPending,
|
|
115
|
+
}"
|
|
116
|
+
:filter-fields="['name', 'code', 'dialCode']"
|
|
117
|
+
:content="{ align: 'start' }"
|
|
118
|
+
:ui="{
|
|
119
|
+
base: 'pe-8 rounded-r-none border-r-0 focus:z-10 bg-gray-50',
|
|
120
|
+
content: 'w-48',
|
|
121
|
+
placeholder: 'hidden',
|
|
122
|
+
trailingIcon: 'size-4',
|
|
123
|
+
}"
|
|
124
|
+
trailing-icon="i-lucide-chevrons-up-down"
|
|
125
|
+
@update:open="onOpen"
|
|
126
|
+
>
|
|
127
|
+
<span class="size-5 flex items-center text-lg">
|
|
128
|
+
{{ country?.emoji || '\u{1F1FA}\u{1F1F8}' }}
|
|
129
|
+
</span>
|
|
130
|
+
|
|
131
|
+
<template #item-leading="{ item }">
|
|
132
|
+
<span class="size-5 flex items-center text-lg">
|
|
133
|
+
{{ item.emoji }}
|
|
134
|
+
</span>
|
|
135
|
+
</template>
|
|
136
|
+
|
|
137
|
+
<template #item-label="{ item }">
|
|
138
|
+
{{ item.name }} ({{ item.dialCode }})
|
|
139
|
+
</template>
|
|
140
|
+
</USelectMenu>
|
|
141
|
+
|
|
142
|
+
<UInput
|
|
143
|
+
v-model="form.phone"
|
|
144
|
+
v-maska="mask"
|
|
145
|
+
class="flex-1"
|
|
146
|
+
:placeholder="mask.replaceAll('#', '_')"
|
|
147
|
+
:style="{ '--dial-code-length': `${dialCode.length + 1.5}ch` }"
|
|
148
|
+
:ui="{
|
|
149
|
+
base: 'ps-[var(--dial-code-length)] rounded-l-none',
|
|
150
|
+
leading: 'pointer-events-none text-base md:text-sm text-gray-400 font-medium',
|
|
151
|
+
}"
|
|
152
|
+
>
|
|
153
|
+
<template #leading>
|
|
154
|
+
{{ dialCode }}
|
|
155
|
+
</template>
|
|
156
|
+
</UInput>
|
|
157
|
+
</div>
|
|
158
|
+
</UFormField>
|
|
159
|
+
</div>
|
|
160
|
+
<div class="pt-4 flex justify-end">
|
|
161
|
+
<UButton type="submit" size="lg">
|
|
162
|
+
Guardar y Continuar
|
|
163
|
+
</UButton>
|
|
164
|
+
</div>
|
|
165
|
+
</form>
|
|
166
|
+
</div>
|
|
167
|
+
<div v-else-if="isComplete" class="text-sm text-gray-600 space-y-1 animate-fade-in">
|
|
168
|
+
<p class="font-medium text-gray-900">
|
|
169
|
+
{{ form.firstName }} {{ form.lastName }}
|
|
170
|
+
</p>
|
|
171
|
+
<p>{{ form.email }}</p>
|
|
172
|
+
<p v-if="form.phone">
|
|
173
|
+
{{ form.phone }}
|
|
174
|
+
</p>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</template>
|
|
179
|
+
|
|
180
|
+
<style scoped>
|
|
181
|
+
.animate-fade-in {
|
|
182
|
+
animation: fadeIn 0.3s ease-in-out;
|
|
183
|
+
}
|
|
184
|
+
@keyframes fadeIn {
|
|
185
|
+
from { opacity: 0; transform: translateY(-5px); }
|
|
186
|
+
to { opacity: 1; transform: translateY(0); }
|
|
187
|
+
}
|
|
188
|
+
</style>
|