feat: add table and filters
This commit is contained in:
parent
41cc1e7c83
commit
6c99e47383
@ -1,5 +1,15 @@
|
|||||||
|
import router, { PAGE, PAGE_URL } from '@/router';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
export const requestApi = axios.create({
|
export const requestApi = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
requestApi.interceptors.response.use(null, async (error) => {
|
||||||
|
if (error.response.status === 401) {
|
||||||
|
const url = PAGE_URL[PAGE.AUTH] + '?message=auth-required';
|
||||||
|
await router.push(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
|
});
|
||||||
|
|||||||
66
src/components/request/RequestFilter.vue
Normal file
66
src/components/request/RequestFilter.vue
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<div class="filter">
|
||||||
|
<div class="form-control">
|
||||||
|
<input
|
||||||
|
v-model="name"
|
||||||
|
type="text"
|
||||||
|
placeholder="Имя"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<select v-model="status">
|
||||||
|
<option disabled>
|
||||||
|
Выберите статус
|
||||||
|
</option>
|
||||||
|
<option :value="STATUS.DONE">
|
||||||
|
Завершен
|
||||||
|
</option>
|
||||||
|
<option :value="STATUS.CANCELLED">
|
||||||
|
Отменен
|
||||||
|
</option>
|
||||||
|
<option :value="STATUS.ACTIVE">
|
||||||
|
Активен
|
||||||
|
</option>
|
||||||
|
<option :value="STATUS.PENDING">
|
||||||
|
Выполняется
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="isActive"
|
||||||
|
class="btn warning"
|
||||||
|
@click="reset"
|
||||||
|
>
|
||||||
|
Очистить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import { STATUS } from '@/utils/constants';
|
||||||
|
import { computed, ref, watch, Ref } from 'vue';
|
||||||
|
|
||||||
|
const name = ref();
|
||||||
|
const status = ref();
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
defineProps<{modelValue: Ref<string>}>()
|
||||||
|
|
||||||
|
watch([name, status], () => {
|
||||||
|
emit('update:modelValue', {name: name.value, status: status.value})
|
||||||
|
})
|
||||||
|
|
||||||
|
const isActive = computed(() => name.value || status.value);
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
name.value = undefined;
|
||||||
|
status.value = undefined;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -68,16 +68,16 @@
|
|||||||
v-model="status"
|
v-model="status"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
>
|
>
|
||||||
<option value="done">
|
<option :value="STATUS.DONE">
|
||||||
Завершено
|
Завершено
|
||||||
</option>
|
</option>
|
||||||
<option value="cancelled">
|
<option :value="STATUS.CANCELLED">
|
||||||
Отменено
|
Отменено
|
||||||
</option>
|
</option>
|
||||||
<option value="active">
|
<option :value="STATUS.ACTIVE">
|
||||||
Активен
|
Активен
|
||||||
</option>
|
</option>
|
||||||
<option value="pending">
|
<option :value="STATUS.PENDING">
|
||||||
Выполняется
|
Выполняется
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
@ -95,6 +95,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import useRequestForm from '@/hooks/useRequestForm.ts';
|
import useRequestForm from '@/hooks/useRequestForm.ts';
|
||||||
|
import { STATUS } from '@/utils/constants.ts';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
|
|
||||||
const emit = defineEmits(['created'])
|
const emit = defineEmits(['created'])
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>№</th>
|
<th>№</th>
|
||||||
<th>ФИО</th>
|
<th>ФИО</th>
|
||||||
|
<th>Сумма</th>
|
||||||
<th>Телефон</th>
|
<th>Телефон</th>
|
||||||
<th>Статус</th>
|
<th>Статус</th>
|
||||||
<th>Действие</th>
|
<th>Действие</th>
|
||||||
@ -27,19 +28,35 @@
|
|||||||
>
|
>
|
||||||
<td>{{ index + 1 }}</td>
|
<td>{{ index + 1 }}</td>
|
||||||
<td>{{ request.fio }}</td>
|
<td>{{ request.fio }}</td>
|
||||||
|
<td>{{ currencyFormatter.format(request.amount) }}</td>
|
||||||
<td>{{ request.phone }}</td>
|
<td>{{ request.phone }}</td>
|
||||||
<td>{{ request.status }}</td>
|
<td>
|
||||||
<td>Купить</td>
|
<app-status :status="request.status" />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<router-link
|
||||||
|
v-slot="{navigate}"
|
||||||
|
custom
|
||||||
|
:to="{name: PAGE.REQUEST, params: {id: request.id}}"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn primary"
|
||||||
|
@click="navigate"
|
||||||
|
>
|
||||||
|
Открыть
|
||||||
|
</button>
|
||||||
|
</router-link>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
defineProps({
|
import { currencyFormatter } from '@/utils/helpers';
|
||||||
requests: {
|
import type { AppRequest } from '@/utils/constants';
|
||||||
type: Array,
|
import AppStatus from '@/components/ui/AppStatus.vue';
|
||||||
required: true
|
import { PAGE } from '@/router';
|
||||||
}
|
|
||||||
})
|
defineProps<{requests: AppRequest[]}>()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
24
src/components/ui/AppStatus.vue
Normal file
24
src/components/ui/AppStatus.vue
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<span class="badge" :class="classesMap[status]">
|
||||||
|
{{textMap[status]}}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { STATUS } from '@/utils/constants';
|
||||||
|
|
||||||
|
const classesMap: Record<STATUS, string> = {
|
||||||
|
[STATUS.ACTIVE]: 'primary',
|
||||||
|
[STATUS.CANCELLED]: 'danger',
|
||||||
|
[STATUS.DONE]: 'primary',
|
||||||
|
[STATUS.PENDING]: 'warning',
|
||||||
|
}
|
||||||
|
const textMap: Record<STATUS, string> = {
|
||||||
|
[STATUS.ACTIVE]: 'Активен',
|
||||||
|
[STATUS.CANCELLED]: 'Отменен',
|
||||||
|
[STATUS.DONE]: 'Завершен',
|
||||||
|
[STATUS.PENDING]: 'Выполняется',
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<{status: STATUS}>();
|
||||||
|
</script>
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { STATUS } from '@/utils/constants.ts';
|
||||||
import { toTypedSchema } from '@vee-validate/zod';
|
import { toTypedSchema } from '@vee-validate/zod';
|
||||||
import { type SubmissionHandler, useField, useForm } from 'vee-validate';
|
import { type SubmissionHandler, useField, useForm } from 'vee-validate';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
@ -21,7 +22,7 @@ const schema = z.object({
|
|||||||
.transform(value => Number(value || 0)),
|
.transform(value => Number(value || 0)),
|
||||||
|
|
||||||
status: z
|
status: z
|
||||||
.enum(["done", "cancelled", "active", "pending"])
|
.nativeEnum(STATUS)
|
||||||
});
|
});
|
||||||
|
|
||||||
type Values = z.infer<typeof schema>;
|
type Values = z.infer<typeof schema>;
|
||||||
@ -35,7 +36,7 @@ const useRequestForm = (props: UseRequestFormProps) => {
|
|||||||
const {isSubmitting, handleSubmit} = useForm<Values>({
|
const {isSubmitting, handleSubmit} = useForm<Values>({
|
||||||
validationSchema: toTypedSchema(schema),
|
validationSchema: toTypedSchema(schema),
|
||||||
initialValues: {
|
initialValues: {
|
||||||
status: 'active',
|
status: STATUS.ACTIVE,
|
||||||
amount: 0,
|
amount: 0,
|
||||||
phone: '',
|
phone: '',
|
||||||
fio: ''
|
fio: ''
|
||||||
|
|||||||
@ -4,11 +4,13 @@ import store from '../store';
|
|||||||
export enum PAGE {
|
export enum PAGE {
|
||||||
HOME= 'Home',
|
HOME= 'Home',
|
||||||
AUTH = 'Auth',
|
AUTH = 'Auth',
|
||||||
HELP = 'Help'
|
HELP = 'Help',
|
||||||
|
REQUEST = 'Request'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PAGE_URL: Record<PAGE, string> = {
|
export const PAGE_URL: Record<PAGE, string> = {
|
||||||
[PAGE.HOME]: '/',
|
[PAGE.HOME]: '/',
|
||||||
|
[PAGE.REQUEST]: '/:id',
|
||||||
[PAGE.AUTH]: '/auth',
|
[PAGE.AUTH]: '/auth',
|
||||||
[PAGE.HELP]: '/help'
|
[PAGE.HELP]: '/help'
|
||||||
}
|
}
|
||||||
@ -40,6 +42,14 @@ const routes = [
|
|||||||
layout: 'auth',
|
layout: 'auth',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: PAGE_URL[PAGE.REQUEST],
|
||||||
|
name: PAGE.REQUEST,
|
||||||
|
component: () => import('../views/AuthPage.vue'),
|
||||||
|
meta: {
|
||||||
|
layout: 'main',
|
||||||
|
}
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
|
|||||||
@ -1,21 +1,22 @@
|
|||||||
import { requestApi } from '@/api/request.ts';
|
import { requestApi } from '@/api/request.ts';
|
||||||
import type { Module } from 'vuex';
|
import type { Module } from 'vuex';
|
||||||
import authModule from './auth.module.ts';
|
import authModule from './auth.module.ts';
|
||||||
|
import type {AppRequest} from '@/utils/constants'
|
||||||
type Request = string;
|
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
requests: Request[];
|
requests: AppRequest[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type SetRequestsPayload = {
|
type SetRequestsPayload = {
|
||||||
requests: State['requests']
|
requests: AppRequest[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type AddRequestsPayload = {
|
type AddRequestsPayload = {
|
||||||
request: Request
|
request: AppRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CreateRequestPayload = Omit<AppRequest, 'id'>
|
||||||
|
|
||||||
const requestModule: Module<State, any> = {
|
const requestModule: Module<State, any> = {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {
|
state: {
|
||||||
@ -33,13 +34,25 @@ const requestModule: Module<State, any> = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions:{
|
actions:{
|
||||||
create: async (injectee, payload) => {
|
loadRequests: async (injectee) => {
|
||||||
|
const {commit, dispatch} = injectee;
|
||||||
|
try {
|
||||||
|
const token = authModule.state.token;
|
||||||
|
const { data } = await requestApi.get<Record<AppRequest['id'], Omit<AppRequest, 'id'>>>(`/requests.json?auth=${token}`);
|
||||||
|
const requests = Object.entries(data).map(([id, value]) => ({id, ...value}))
|
||||||
|
commit('setRequests', { requests })
|
||||||
|
} catch (e) {
|
||||||
|
await dispatch('alert/setAlert', {message: 'Ошибка при попытке получить список заявок', type: 'danger', title: 'Ошибка', time: 5000}, {root: true})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
create: async (injectee, payload: CreateRequestPayload) => {
|
||||||
const { dispatch, commit } = injectee;
|
const { dispatch, commit } = injectee;
|
||||||
|
|
||||||
const token = authModule.state.token;
|
const token = authModule.state.token;
|
||||||
try {
|
try {
|
||||||
const { data } = await requestApi.post(`/requests.json?auth=${token}`, payload)
|
const { data } = await requestApi.post<Record<'name', AppRequest['id']>>(`/requests.json?auth=${token}`, payload)
|
||||||
commit('addRequest', {...payload, id: data.name})
|
const request = {...payload, id: data.name }
|
||||||
|
commit('addRequest', { request })
|
||||||
await dispatch('alert/setAlert', {message: 'Заявка успешно создана', type: 'primary', title: 'Успех', time: 5000}, {root: true})
|
await dispatch('alert/setAlert', {message: 'Заявка успешно создана', type: 'primary', title: 'Успех', time: 5000}, {root: true})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await dispatch('alert/setAlert', {message: 'Ошибка при попытке создать заявку', type: 'danger', title: 'Ошибка', time: 5000}, {root: true})
|
await dispatch('alert/setAlert', {message: 'Ошибка при попытке создать заявку', type: 'danger', title: 'Ошибка', time: 5000}, {root: true})
|
||||||
|
|||||||
@ -2,3 +2,18 @@ export const ERROR_MESSAGES = {
|
|||||||
'INVALID_LOGIN_CREDENTIALS': 'Неправильный логин или пароль',
|
'INVALID_LOGIN_CREDENTIALS': 'Неправильный логин или пароль',
|
||||||
'auth-required': 'Требуется авторизация',
|
'auth-required': 'Требуется авторизация',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum STATUS {
|
||||||
|
ACTIVE = 'active',
|
||||||
|
CANCELLED = 'cancelled',
|
||||||
|
DONE = 'done',
|
||||||
|
PENDING = 'pending',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AppRequest = {
|
||||||
|
id: string;
|
||||||
|
amount: number;
|
||||||
|
fio: string;
|
||||||
|
phone: string;
|
||||||
|
status: STATUS;
|
||||||
|
};
|
||||||
|
|||||||
4
src/utils/helpers.ts
Normal file
4
src/utils/helpers.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const currencyFormatter = new Intl.NumberFormat('ru-RU', {
|
||||||
|
currency: 'RUB',
|
||||||
|
style: 'currency',
|
||||||
|
})
|
||||||
@ -18,10 +18,12 @@
|
|||||||
>
|
>
|
||||||
<app-spinner />
|
<app-spinner />
|
||||||
</div>
|
</div>
|
||||||
<request-table
|
<div v-else>
|
||||||
v-else
|
<request-filter v-model="filters" />
|
||||||
:requests="requests"
|
<request-table
|
||||||
/>
|
:requests="requests"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<teleport to="body">
|
<teleport to="body">
|
||||||
<app-modal
|
<app-modal
|
||||||
@ -36,7 +38,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
import { requestApi } from '@/api/request.ts';
|
import RequestFilter from '@/components/request/RequestFilter.vue';
|
||||||
import RequestForm from '@/components/request/RequestForm.vue';
|
import RequestForm from '@/components/request/RequestForm.vue';
|
||||||
import RequestTable from '@/components/request/RequestTable.vue';
|
import RequestTable from '@/components/request/RequestTable.vue';
|
||||||
import AppModal from '@/components/ui/AppModal.vue';
|
import AppModal from '@/components/ui/AppModal.vue';
|
||||||
@ -49,23 +51,20 @@ const store = useStore();
|
|||||||
const closeModal = () => modal.value = false;
|
const closeModal = () => modal.value = false;
|
||||||
const openModal = () => modal.value = true;
|
const openModal = () => modal.value = true;
|
||||||
|
|
||||||
const loading = ref(true);
|
const loading = ref(false);
|
||||||
|
const filters = ref({});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
loading.value = true;
|
||||||
loading.value = true;
|
await store.dispatch('request/loadRequests');
|
||||||
const token = store.state.auth.token;
|
loading.value = false
|
||||||
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);
|
const requests = computed(() => {
|
||||||
|
return store.state.request.requests
|
||||||
|
.filter(item => filters.value.status ? item.status === filters.value.status : true)
|
||||||
|
.filter(item => filters.value.name ? item.fio.includes(filters.value.name) : true);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user