feat: create new order
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div class="app-container h-full">
|
||||
<FullscreenViewport v-if="platform === 'ios' || platform === 'android'"/>
|
||||
<router-view/>
|
||||
<CartButton/>
|
||||
|
||||
46
spa/src/components/Form/TgInput.vue
Normal file
46
spa/src/components/Form/TgInput.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<fieldset class="fieldset mb-0">
|
||||
<input
|
||||
:type="type"
|
||||
:inputmode="inputMode"
|
||||
class="input w-full"
|
||||
:class="error ? 'input-error' : ''"
|
||||
:placeholder="placeholder"
|
||||
v-model="model"
|
||||
@input="$emit('clearError')"
|
||||
/>
|
||||
<p v-if="error" class="label">{{ error }}</p>
|
||||
</fieldset>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed} from "vue";
|
||||
|
||||
const model = defineModel();
|
||||
const props = defineProps({
|
||||
error: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text',
|
||||
},
|
||||
});
|
||||
const emits = defineEmits(['clearError']);
|
||||
|
||||
const inputMode = computed(() => {
|
||||
switch (props.type) {
|
||||
case 'email': return 'email';
|
||||
case 'tel': return 'tel';
|
||||
case 'number': return 'numeric';
|
||||
default: return 'text';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
30
spa/src/components/Form/TgTextarea.vue
Normal file
30
spa/src/components/Form/TgTextarea.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<fieldset class="fieldset mb-0">
|
||||
<textarea
|
||||
class="input w-full h-30"
|
||||
:class="error ? 'input-error' : ''"
|
||||
:placeholder="placeholder"
|
||||
v-model="model"
|
||||
@input="$emit('clearError')"
|
||||
rows="5"
|
||||
/>
|
||||
<p v-if="error" class="label">{{ error }}</p>
|
||||
</fieldset>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
const model = defineModel();
|
||||
const props = defineProps({
|
||||
error: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
const emits = defineEmits(['clearError']);
|
||||
</script>
|
||||
@@ -19,7 +19,6 @@ const categoriesStore = useCategoriesStore();
|
||||
categoriesStore.fetchTopCategories();
|
||||
categoriesStore.fetchCategories();
|
||||
|
||||
|
||||
import {useCategoriesStore} from "@/stores/CategoriesStore.js";
|
||||
import {useSettingsStore} from "@/stores/SettingsStore.js";
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import CategoriesList from "./views/CategoriesList.vue";
|
||||
import Cart from "./views/Cart.vue";
|
||||
import Products from "@/views/Products.vue";
|
||||
import Checkout from "@/views/Checkout.vue";
|
||||
import OrderCreated from "@/views/OrderCreated.vue";
|
||||
|
||||
const routes = [
|
||||
{path: '/', name: 'home', component: Home},
|
||||
@@ -14,6 +15,7 @@ const routes = [
|
||||
{path: '/category/:id', name: 'category.show', component: CategoriesList},
|
||||
{path: '/cart', name: 'cart.show', component: Cart},
|
||||
{path: '/checkout', name: 'checkout', component: Checkout},
|
||||
{path: '/success', name: 'order_created', component: OrderCreated},
|
||||
];
|
||||
|
||||
export const router = createRouter({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {defineStore} from "pinia";
|
||||
import {isNotEmpty} from "@/helpers.js";
|
||||
import {apiFetch} from "@/utils/ftch.js";
|
||||
import {addToCart, cartEditItem, cartRemoveItem, getCart} from "@/utils/ftch.js";
|
||||
|
||||
export const useCartStore = defineStore('cart', {
|
||||
state: () => ({
|
||||
@@ -26,7 +26,7 @@ export const useCartStore = defineStore('cart', {
|
||||
async getProducts() {
|
||||
try {
|
||||
this.isLoading = true;
|
||||
const data = await apiFetch('/index.php?route=extension/tgshop/handle/cart');
|
||||
const {data} = await getCart();
|
||||
this.items = data.products;
|
||||
this.productsCount = data.total_products_count;
|
||||
this.totals = data.totals;
|
||||
@@ -62,10 +62,7 @@ export const useCartStore = defineStore('cart', {
|
||||
}
|
||||
})
|
||||
|
||||
const response = await apiFetch('/index.php?route=checkout/cart/add', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
const response = await addToCart(formData);
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(JSON.stringify(response.error));
|
||||
@@ -85,10 +82,7 @@ export const useCartStore = defineStore('cart', {
|
||||
this.isLoading = true;
|
||||
const formData = new FormData();
|
||||
formData.append('key', rowId);
|
||||
await apiFetch('/index.php?route=checkout/cart/remove', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
await cartRemoveItem(formData);
|
||||
await this.getProducts();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -102,11 +96,7 @@ export const useCartStore = defineStore('cart', {
|
||||
this.isLoading = true;
|
||||
const formData = new FormData();
|
||||
formData.append(`quantity[${cartId}]`, quantity);
|
||||
await apiFetch('/index.php?route=checkout/cart/edit', {
|
||||
redirect: 'manual',
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
await cartEditItem(formData);
|
||||
await this.getProducts();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
@@ -115,4 +105,4 @@ export const useCartStore = defineStore('cart', {
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
44
spa/src/stores/CheckoutStore.js
Normal file
44
spa/src/stores/CheckoutStore.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import {defineStore} from "pinia";
|
||||
import {isNotEmpty} from "@/helpers.js";
|
||||
import {storeOrder} from "@/utils/ftch.js";
|
||||
import {useCartStore} from "@/stores/CartStore.js";
|
||||
|
||||
export const useCheckoutStore = defineStore('checkout', {
|
||||
state: () => ({
|
||||
customer: {
|
||||
firstName: "Иван",
|
||||
lastName: "Васильевич",
|
||||
email: "ival_vasil@mail.ru",
|
||||
phone: "+79999999999",
|
||||
address: "Москва, Красная площадь, 1",
|
||||
comment: "Доставить срочно❗️",
|
||||
},
|
||||
|
||||
validationErrors: {},
|
||||
}),
|
||||
|
||||
getters: {
|
||||
hasError: (state) => {
|
||||
return (field) => isNotEmpty(state.validationErrors[field]);
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
async makeOrder() {
|
||||
await storeOrder(this.customer)
|
||||
.catch(error => {
|
||||
if (error.response?.status === 422) {
|
||||
this.validationErrors = error.response._data.data;
|
||||
} else {
|
||||
console.error('Unexpected error', error);
|
||||
}
|
||||
});
|
||||
|
||||
await useCartStore().getProducts();
|
||||
},
|
||||
|
||||
clearError(field) {
|
||||
this.validationErrors[field] = null;
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -3,9 +3,13 @@
|
||||
themes: all;
|
||||
}
|
||||
|
||||
|
||||
html, body, #app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#app {
|
||||
position: relative;
|
||||
padding-top: var(--tg-content-safe-area-inset-top);
|
||||
padding-bottom: var(--tg-content-safe-area-inset-bottom);
|
||||
padding-left: var(--tg-content-safe-area-inset-left);
|
||||
|
||||
@@ -3,7 +3,7 @@ import {ofetch} from "ofetch";
|
||||
const BASE_URL = '/';
|
||||
|
||||
export const apiFetch = ofetch.create({
|
||||
onRequest({ request, options }) {
|
||||
onRequest({request, options}) {
|
||||
const initData = window.Telegram?.WebApp?.initData
|
||||
|
||||
if (initData) {
|
||||
@@ -15,7 +15,7 @@ export const apiFetch = ofetch.create({
|
||||
},
|
||||
});
|
||||
|
||||
export default async function (action, query = null, json = null) {
|
||||
async function ftch(action, query = null, json = null) {
|
||||
const options = {
|
||||
method: json ? 'POST' : 'GET',
|
||||
}
|
||||
@@ -23,4 +23,39 @@ export default async function (action, query = null, json = null) {
|
||||
if (json) options.body = json;
|
||||
|
||||
return await apiFetch(`${BASE_URL}index.php?route=extension/tgshop/handle&api_action=${action}`, options);
|
||||
};
|
||||
}
|
||||
|
||||
export async function storeOrder(data) {
|
||||
return await apiFetch(`${BASE_URL}index.php?route=extension/tgshop/handle&api_action=storeOrder`, {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCart() {
|
||||
return await ftch('getCart');
|
||||
}
|
||||
|
||||
export async function addToCart(data) {
|
||||
return await apiFetch(`${BASE_URL}index.php?route=checkout/cart/add`, {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
});
|
||||
}
|
||||
|
||||
export async function cartRemoveItem(data) {
|
||||
return await apiFetch(`${BASE_URL}index.php?route=checkout/cart/remove`, {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
});
|
||||
}
|
||||
|
||||
export async function cartEditItem(data) {
|
||||
return await apiFetch(`${BASE_URL}index.php?route=checkout/cart/edit`, {
|
||||
redirect: 'manual',
|
||||
method: 'POST',
|
||||
body: data,
|
||||
});
|
||||
}
|
||||
|
||||
export default ftch;
|
||||
|
||||
@@ -86,7 +86,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" :disabled="cart.canCheckout === false">Перейти к оформлению</button>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
:disabled="cart.canCheckout === false"
|
||||
@click="goToCheckout"
|
||||
>Перейти к оформлению</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -108,8 +112,10 @@ import OptionRadio from "@/components/ProductOptions/Cart/OptionRadio.vue";
|
||||
import OptionCheckbox from "@/components/ProductOptions/Cart/OptionCheckbox.vue";
|
||||
import OptionText from "@/components/ProductOptions/Cart/OptionText.vue";
|
||||
import {computed} from "vue";
|
||||
import {useRouter} from "vue-router";
|
||||
|
||||
const cart = useCartStore();
|
||||
const router = useRouter();
|
||||
|
||||
// const componentMap = {
|
||||
// radio: OptionRadio,
|
||||
@@ -127,4 +133,8 @@ function removeItem(cartId) {
|
||||
cart.removeItem(cartId);
|
||||
window.Telegram.WebApp.HapticFeedback.notificationOccurred('error');
|
||||
}
|
||||
|
||||
function goToCheckout() {
|
||||
router.push({name: 'checkout'});
|
||||
}
|
||||
</script>
|
||||
@@ -1,13 +1,75 @@
|
||||
<template>
|
||||
<div class="max-w-3xl mx-auto p-4 space-y-6 pb-30">
|
||||
<h2 class="text-2xl">
|
||||
Корзина
|
||||
<span class="loading loading-spinner loading-md"></span>
|
||||
Оформление заказа
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="card card-border bg-base-100 w-full">
|
||||
<div class="card-body">
|
||||
<TgInput
|
||||
v-model="checkout.customer.firstName"
|
||||
placeholder="Введите имя"
|
||||
:error="checkout.validationErrors.firstName"
|
||||
@clearError="checkout.clearError('firstName')"
|
||||
/>
|
||||
|
||||
<TgInput
|
||||
v-model="checkout.customer.lastName"
|
||||
placeholder="Введите фамилию"
|
||||
:error="checkout.validationErrors.lastName"
|
||||
@clearError="checkout.clearError('lastName')"
|
||||
/>
|
||||
|
||||
<TgInput
|
||||
v-model="checkout.customer.email"
|
||||
type="email"
|
||||
placeholder="Введите email"
|
||||
:error="checkout.validationErrors.email"
|
||||
@clearError="checkout.clearError('email')"
|
||||
/>
|
||||
|
||||
<TgInput
|
||||
v-model="checkout.customer.phone"
|
||||
type="tel"
|
||||
placeholder="Введите телефон"
|
||||
:error="checkout.validationErrors.phone"
|
||||
@clearError="checkout.clearError('phone')"
|
||||
/>
|
||||
|
||||
<TgInput
|
||||
v-model="checkout.customer.address"
|
||||
placeholder="Адрес доставки"
|
||||
:error="checkout.validationErrors.address"
|
||||
@clearError="checkout.clearError('address')"
|
||||
/>
|
||||
|
||||
<TgTextarea
|
||||
v-model="checkout.customer.comment"
|
||||
placeholder="Комментарий"
|
||||
:error="checkout.validationErrors.comment"
|
||||
@clearError="checkout.clearError('comment')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="fixed px-4 pb-10 pt-4 bottom-0 left-0 w-full bg-base-200 z-50 flex justify-between items-center gap-2 border-t-1 border-t-base-300">
|
||||
<button class="btn btn-primary w-full" @click="onCreateBtnClick">Создать заказ</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useCheckoutStore} from "@/stores/CheckoutStore.js";
|
||||
import TgInput from "@/components/Form/TgInput.vue";
|
||||
import TgTextarea from "@/components/Form/TgTextarea.vue";
|
||||
import {useRouter} from "vue-router";
|
||||
|
||||
const checkout = useCheckoutStore();
|
||||
const router = useRouter();
|
||||
|
||||
async function onCreateBtnClick() {
|
||||
await checkout.makeOrder();
|
||||
router.push({name: 'order_created'});
|
||||
}
|
||||
</script>
|
||||
|
||||
16
spa/src/views/OrderCreated.vue
Normal file
16
spa/src/views/OrderCreated.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div class="max-w-3xl mx-auto p-4 space-y-6 pb-30 flex flex-col items-center h-full justify-center">
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-20 text-success">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12c0 1.268-.63 2.39-1.593 3.068a3.745 3.745 0 0 1-1.043 3.296 3.745 3.745 0 0 1-3.296 1.043A3.745 3.745 0 0 1 12 21c-1.268 0-2.39-.63-3.068-1.593a3.746 3.746 0 0 1-3.296-1.043 3.745 3.745 0 0 1-1.043-3.296A3.745 3.745 0 0 1 3 12c0-1.268.63-2.39 1.593-3.068a3.745 3.745 0 0 1 1.043-3.296 3.746 3.746 0 0 1 3.296-1.043A3.746 3.746 0 0 1 12 3c1.268 0 2.39.63 3.068 1.593a3.746 3.746 0 0 1 3.296 1.043 3.746 3.746 0 0 1 1.043 3.296A3.745 3.745 0 0 1 21 12Z" />
|
||||
</svg>
|
||||
|
||||
<p class="text-lg mb-3">Ваш заказ создан!</p>
|
||||
<RouterLink class="btn btn-primary" to="/">На главную</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
Reference in New Issue
Block a user