feat: add table and filters

This commit is contained in:
Sergey Krylov 2025-02-07 06:39:23 +03:00
parent 41cc1e7c83
commit 6c99e47383
11 changed files with 202 additions and 42 deletions

View File

@ -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);
});

View 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>

View File

@ -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'])

View File

@ -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>

View 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>

View File

@ -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: ''

View File

@ -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({

View File

@ -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})

View File

@ -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
View File

@ -0,0 +1,4 @@
export const currencyFormatter = new Intl.NumberFormat('ru-RU', {
currency: 'RUB',
style: 'currency',
})

View File

@ -18,10 +18,12 @@
> >
<app-spinner /> <app-spinner />
</div> </div>
<div v-else>
<request-filter v-model="filters" />
<request-table <request-table
v-else
:requests="requests" :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;
const token = store.state.auth.token; await store.dispatch('request/loadRequests');
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 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>