feat: add requests

This commit is contained in:
Sergey Krylov 2025-02-06 07:31:31 +03:00
parent 6501b1d020
commit 41cc1e7c83
11 changed files with 413 additions and 8 deletions

5
src/api/request.ts Normal file
View File

@ -0,0 +1,5 @@
import axios from 'axios';
export const requestApi = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
})

View File

@ -0,0 +1,123 @@
<template>
<form @submit.prevent="onSubmit">
<div
class="form-control"
:class="{invalid: fError}"
>
<label for="fio">ФИО</label>
<input
id="fio"
v-model="fio"
type="text"
:disabled="isSubmitting"
@blur="fBlur"
>
<small
v-if="fError"
class="text-danger"
>
{{ fError }}
</small>
</div>
<div
class="form-control"
:class="{invalid: pError}"
>
<label for="phone">Телефон</label>
<input
id="phone"
v-model="phone"
type="tel"
:disabled="isSubmitting"
@blur="pBlur"
>
<small
v-if="pError"
class="text-danger"
>
{{ pError }}
</small>
</div>
<div
class="form-control"
:class="{invalid: aError}"
>
<label for="amount">Сумма</label>
<input
id="amount"
v-model="amount"
type="number"
:disabled="isSubmitting"
min="0"
@blur="aBlur"
>
<small
v-if="aError"
class="text-danger"
>
{{ aError }}
</small>
</div>
<div class="form-control">
<label for="status">Статус</label>
<select
id="status"
v-model="status"
:disabled="isSubmitting"
>
<option value="done">
Завершено
</option>
<option value="cancelled">
Отменено
</option>
<option value="active">
Активен
</option>
<option value="pending">
Выполняется
</option>
</select>
</div>
<button
class="btn"
type="submit"
:disabled="isSubmitting"
>
Создать
</button>
</form>
</template>
<script setup lang="ts">
import useRequestForm from '@/hooks/useRequestForm.ts';
import { useStore } from 'vuex';
const emit = defineEmits(['created'])
const store = useStore();
const onSubmitHandler = async (values) => {
await store.dispatch('request/create', values);
emit('created', values);
}
const {
onSubmit,
isSubmitting,
status,
fio,
phone,
amount,
fError,
fBlur,
pError,
pBlur,
aError,
aBlur,
} = useRequestForm({ onSubmit: onSubmitHandler });
</script>

View File

@ -0,0 +1,45 @@
<template>
<h4
v-if="requests.length === 0"
class="text-center"
>
Заявок нет
</h4>
<table
v-else
class="table"
>
<thead>
<tr>
<th></th>
<th>ФИО</th>
<th>Телефон</th>
<th>Статус</th>
<th>Действие</th>
</tr>
</thead>
<tbody>
<tr
v-for="(request, index) in requests"
:key="request.id"
>
<td>{{ index + 1 }}</td>
<td>{{ request.fio }}</td>
<td>{{ request.phone }}</td>
<td>{{ request.status }}</td>
<td>Купить</td>
</tr>
</tbody>
</table>
</template>
<script setup>
defineProps({
requests: {
type: Array,
required: true
}
})
</script>

View File

@ -0,0 +1,24 @@
<template>
<div
class="modal-backdrop"
@click="$emit('close')"
/>
<div class="modal">
<h3 v-if="title">
{{ title }}
</h3>
<slot />
</div>
</template>
<script setup lang="ts">
defineProps({
title: {
type: String,
required: false
}
})
defineEmits(['close'])
</script>

View File

@ -0,0 +1,25 @@
<template>
<span class="loader" />
</template>
<style scoped>
.loader {
width: 48px;
height: 48px;
border: 5px solid transparent;
border-bottom-color: #42b983;
border-radius: 50%;
display: inline-block;
box-sizing: border-box;
animation: rotation 1s linear infinite;
}
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>

View File

@ -0,0 +1,65 @@
import { toTypedSchema } from '@vee-validate/zod';
import { type SubmissionHandler, useField, useForm } from 'vee-validate';
import * as z from 'zod';
const schema = z.object({
fio: z
.string()
.trim()
.min(1, 'Обязательное поле')
.default(''),
phone: z
.string()
.trim()
.min(1, 'Обязательное поле')
.default(''),
amount: z
.coerce
.string()
.trim()
.default('')
.transform(value => Number(value || 0)),
status: z
.enum(["done", "cancelled", "active", "pending"])
});
type Values = z.infer<typeof schema>;
type UseRequestFormProps = {
onSubmit: SubmissionHandler<Values>
}
const useRequestForm = (props: UseRequestFormProps) => {
const {onSubmit} = props;
const {isSubmitting, handleSubmit} = useForm<Values>({
validationSchema: toTypedSchema(schema),
initialValues: {
status: 'active',
amount: 0,
phone: '',
fio: ''
}
});
const { value: fio, errorMessage: fError, handleBlur: fBlur } = useField('fio');
const { value: phone, errorMessage: pError, handleBlur: pBlur } = useField('phone');
const { value: amount, errorMessage: aError, handleBlur: aBlur } = useField('amount');
const { value: status } = useField('status');
return {
status,
isSubmitting,
onSubmit: handleSubmit(onSubmit),
fio,
phone,
amount,
fError,
fBlur,
pError,
pBlur,
aError,
aBlur,
}
}
export default useRequestForm

View File

@ -1,6 +1,7 @@
<template>
<the-navbar />
<div class="container with-nav">
<app-alert />
<div class="card">
<router-view />
</div>
@ -9,8 +10,9 @@
<script>
import TheNavbar from '../components/TheNavbar.vue'
import AppAlert from '@/components/ui/AppAlert.vue';
export default {
components: { TheNavbar }
components: {AppAlert, TheNavbar }
}
</script>

View File

@ -45,6 +45,8 @@ const routes = [
const router = createRouter({
history: createWebHistory(),
routes,
linkActiveClass: 'active',
linkExactActiveClass: 'active',
});
router.beforeEach((to, from, next) => {

View File

@ -1,6 +1,7 @@
import { createLogger, createStore } from 'vuex'
import authModule from './modules/auth.module.ts';
import alertModule from './modules/alert.module.ts';
import requestModule from './modules/request.module.ts';
const isDev = process.env.NODE_ENV === 'development';
@ -11,6 +12,7 @@ const store = createStore({
modules: {
auth: authModule,
alert: alertModule,
request: requestModule
},
})

View File

@ -0,0 +1,51 @@
import { requestApi } from '@/api/request.ts';
import type { Module } from 'vuex';
import authModule from './auth.module.ts';
type Request = string;
type State = {
requests: Request[];
};
type SetRequestsPayload = {
requests: State['requests']
}
type AddRequestsPayload = {
request: Request
}
const requestModule: Module<State, any> = {
namespaced: true,
state: {
requests: []
},
mutations: {
setRequests: (state, payload: SetRequestsPayload) => {
const { requests } = payload;
state.requests = requests
},
addRequest: (state, payload: AddRequestsPayload) => {
const { request } = payload;
state.requests.push(request)
}
},
actions:{
create: async (injectee, payload) => {
const { dispatch, commit } = injectee;
const token = authModule.state.token;
try {
const { data } = await requestApi.post(`/requests.json?auth=${token}`, payload)
commit('addRequest', {...payload, id: data.name})
await dispatch('alert/setAlert', {message: 'Заявка успешно создана', type: 'primary', title: 'Успех', time: 5000}, {root: true})
} catch (e) {
await dispatch('alert/setAlert', {message: 'Ошибка при попытке создать заявку', type: 'danger', title: 'Ошибка', time: 5000}, {root: true})
}
}
}
}
export default requestModule;

View File

@ -1,16 +1,77 @@
<template>
<div>
<h1>Home</h1>
<div class="header">
<h1 class="card-title">
Список заявок
</h1>
<button
class="btn primary"
@click="openModal"
>
Создать
</button>
</div>
<div
v-if="loading"
style="display: flex;"
>
<app-spinner />
</div>
<request-table
v-else
:requests="requests"
/>
<teleport to="body">
<app-modal
v-if="modal"
title="Создать заявку"
@close="closeModal"
>
<request-form @created="closeModal" />
</app-modal>
</teleport>
</template>
<script>
<script setup lang="ts">
export default {
import { requestApi } from '@/api/request.ts';
import RequestForm from '@/components/request/RequestForm.vue';
import RequestTable from '@/components/request/RequestTable.vue';
import AppModal from '@/components/ui/AppModal.vue';
import AppSpinner from '@/components/ui/AppSpinner.vue';
import { computed, onMounted, ref } from 'vue';
import { useStore } from 'vuex';
const modal = ref(false);
const store = useStore();
const closeModal = () => modal.value = false;
const openModal = () => modal.value = true;
const loading = ref(true);
onMounted(async () => {
try {
loading.value = true;
const token = store.state.auth.token;
const { data } = await requestApi.get(`/requests.json?auth=${token}`);
const requests = Object.entries(data).map(([id, value]) => ({id, ...value}))
store.commit('request/setRequests', { requests })
} catch (e) {
await store.dispatch('alert/setAlert', {message: 'Ошибка при попытке получить список заявок', type: 'danger', title: 'Ошибка', time: 5000})
} finally {
loading.value = false
}
})
const requests = computed(() => store.state.request.requests);
</script>
<style lang="scss" scoped>
<style scoped>
.header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>