Обновлен стартовый экран
Переписаны запросы для статистики, отчетов Добавлена интеграция отчета сестры
This commit is contained in:
106
resources/js/Pages/Admin/Metrics/Group.vue
Normal file
106
resources/js/Pages/Admin/Metrics/Group.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<script setup>
|
||||
import {
|
||||
NButton, NFlex, NForm, NFormItem, NInput, NTransfer,
|
||||
NText, NIcon, NAlert,
|
||||
} from 'naive-ui'
|
||||
import {
|
||||
TbStack2, TbLayoutDashboard, TbChartBar, TbAdjustments,
|
||||
} from 'vue-icons-plus/tb'
|
||||
import AppLayout from '../../../Layouts/AppLayout.vue'
|
||||
import AppContainer from '../../../Components/AppContainer.vue'
|
||||
import SectionCard from '../../../Components/SectionCard.vue'
|
||||
import PageBanner from '../../../Components/PageBanner.vue'
|
||||
import { useForm, Link, usePage } from '@inertiajs/vue3'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
group: { type: Object, default: null },
|
||||
itemIds: { type: Array, default: () => [] },
|
||||
allItems: { type: Array, default: () => [] },
|
||||
})
|
||||
|
||||
const page = usePage()
|
||||
const flash = computed(() => page.props.flash ?? {})
|
||||
|
||||
const isEdit = computed(() => !!props.group)
|
||||
|
||||
const transferOptions = computed(() => props.allItems.map(i => ({
|
||||
label: i.name,
|
||||
value: i.metrika_item_id,
|
||||
})))
|
||||
|
||||
const form = useForm({
|
||||
name: props.group?.name ?? '',
|
||||
description: props.group?.description ?? '',
|
||||
items: [...props.itemIds],
|
||||
})
|
||||
|
||||
const submit = () => {
|
||||
if (isEdit.value) {
|
||||
form.put(`/admin/metrics/groups/${props.group.id}`)
|
||||
} else {
|
||||
form.post('/admin/metrics/groups/new')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout>
|
||||
<AppContainer>
|
||||
|
||||
<NAlert v-if="flash.success" type="success" closable style="margin-bottom: 4px;">
|
||||
{{ flash.success }}
|
||||
</NAlert>
|
||||
|
||||
<PageBanner
|
||||
:title="isEdit ? group.name : 'Новая группа'"
|
||||
:icon="TbStack2"
|
||||
:color="isEdit ? null : 'default'"
|
||||
:breadcrumbs="[
|
||||
{ label: 'Администратор', href: '/admin', icon: TbLayoutDashboard, tag: Link },
|
||||
{ label: 'Метрики', href: '/admin/metrics', icon: TbChartBar, tag: Link },
|
||||
]"
|
||||
>
|
||||
<template #meta>
|
||||
<NText depth="3" style="font-size: 13px;">
|
||||
{{ isEdit ? 'Редактирование группы показателей' : 'Создание новой группы' }}
|
||||
</NText>
|
||||
</template>
|
||||
<template #actions>
|
||||
<NButton :tag="Link" href="/admin/metrics">Отмена</NButton>
|
||||
<NButton type="primary" :loading="form.processing" @click="submit">
|
||||
{{ isEdit ? 'Сохранить' : 'Создать группу' }}
|
||||
</NButton>
|
||||
</template>
|
||||
</PageBanner>
|
||||
|
||||
<SectionCard title="Основные данные" :icon="TbStack2">
|
||||
<NForm label-placement="top">
|
||||
<NFormItem label="Название" :feedback="form.errors.name" :validation-status="form.errors.name ? 'error' : undefined">
|
||||
<NInput v-model:value="form.name" placeholder="Название группы" />
|
||||
</NFormItem>
|
||||
<NFormItem label="Описание" :feedback="form.errors.description" :validation-status="form.errors.description ? 'error' : undefined" style="margin-bottom: 0;">
|
||||
<NInput v-model:value="form.description" type="textarea" placeholder="Краткое описание группы" :rows="3" />
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard title="Показатели" :icon="TbAdjustments" no-padding>
|
||||
<NTransfer
|
||||
v-model:value="form.items"
|
||||
:options="transferOptions"
|
||||
source-filterable
|
||||
target-filterable
|
||||
source-title="Доступные показатели"
|
||||
target-title="Показатели группы"
|
||||
style="height: calc(100vh - 520px); min-height: 200px;"
|
||||
/>
|
||||
</SectionCard>
|
||||
|
||||
</AppContainer>
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.n-transfer) { border: none !important; border-radius: 0 !important; }
|
||||
</style>
|
||||
205
resources/js/Pages/Admin/Metrics/Index.vue
Normal file
205
resources/js/Pages/Admin/Metrics/Index.vue
Normal file
@@ -0,0 +1,205 @@
|
||||
<script setup>
|
||||
import {
|
||||
NFlex, NButton, NDataTable, NTag,
|
||||
NText, NEl, NIcon, NInput, NTabs, NTabPane,
|
||||
} from 'naive-ui'
|
||||
import {
|
||||
TbChartBar, TbPlus, TbPencil, TbSearch,
|
||||
TbLayoutDashboard, TbStack2, TbAdjustments,
|
||||
} from 'vue-icons-plus/tb'
|
||||
import AppLayout from '../../../Layouts/AppLayout.vue'
|
||||
import AppContainer from '../../../Components/AppContainer.vue'
|
||||
import SectionCard from '../../../Components/SectionCard.vue'
|
||||
import PageBanner from '../../../Components/PageBanner.vue'
|
||||
import { Link } from '@inertiajs/vue3'
|
||||
import { computed, h, ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
groups: { type: Array, default: () => [] },
|
||||
items: { type: Array, default: () => [] },
|
||||
})
|
||||
|
||||
const searchGroups = ref('')
|
||||
const searchItems = ref('')
|
||||
|
||||
const filteredGroups = computed(() => {
|
||||
const q = searchGroups.value.toLowerCase().trim()
|
||||
if (!q) return props.groups
|
||||
return props.groups.filter(g => g.name.toLowerCase().includes(q))
|
||||
})
|
||||
|
||||
const filteredItems = computed(() => {
|
||||
const q = searchItems.value.toLowerCase().trim()
|
||||
if (!q) return props.items
|
||||
return props.items.filter(i => i.name.toLowerCase().includes(q) || i.code.toLowerCase().includes(q))
|
||||
})
|
||||
|
||||
const dataTypeLabel = (type) => ({
|
||||
integer: 'Целое',
|
||||
float: 'Дробное',
|
||||
string: 'Строка',
|
||||
text: 'Текст',
|
||||
boolean: 'Да/Нет',
|
||||
select: 'Список',
|
||||
}[type] ?? type)
|
||||
|
||||
const dataTypeColor = (type) => ({
|
||||
integer: 'info',
|
||||
float: 'info',
|
||||
string: 'default',
|
||||
text: 'default',
|
||||
boolean: 'warning',
|
||||
select: 'success',
|
||||
}[type] ?? 'default')
|
||||
|
||||
const groupColumns = computed(() => [
|
||||
{
|
||||
key: 'name',
|
||||
title: 'Название',
|
||||
render: (row) => h(NFlex, { vertical: true, size: 2 }, () => [
|
||||
h(NText, { style: 'font-weight: 500; font-size: 13px;' }, () => row.name),
|
||||
row.description ? h(NText, { depth: 3, style: 'font-size: 12px;' }, () => row.description) : null,
|
||||
])
|
||||
},
|
||||
{
|
||||
key: 'items_count',
|
||||
title: 'Показателей',
|
||||
width: 130,
|
||||
render: (row) => h(NTag, { size: 'small', round: true, bordered: false }, () => `${row.items_count}`)
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
title: '',
|
||||
width: 60,
|
||||
align: 'center',
|
||||
render: (row) => h(NButton, {
|
||||
text: true, size: 'small', tag: Link,
|
||||
href: `/admin/metrics/groups/${row.id}`,
|
||||
title: 'Редактировать',
|
||||
}, { icon: () => h(NIcon, { size: 18 }, () => h(TbPencil)) })
|
||||
},
|
||||
])
|
||||
|
||||
const itemColumns = computed(() => [
|
||||
{
|
||||
key: 'name',
|
||||
title: 'Название',
|
||||
render: (row) => h(NFlex, { vertical: true, size: 2 }, () => [
|
||||
h(NText, { style: 'font-weight: 500; font-size: 13px;' }, () => row.name),
|
||||
h(NText, { depth: 3, style: 'font-size: 12px; font-family: monospace;' }, () => row.code),
|
||||
])
|
||||
},
|
||||
{
|
||||
key: 'data_type',
|
||||
title: 'Тип',
|
||||
width: 120,
|
||||
render: (row) => h(NTag, {
|
||||
type: dataTypeColor(row.data_type),
|
||||
size: 'small', round: true, bordered: false,
|
||||
}, () => dataTypeLabel(row.data_type))
|
||||
},
|
||||
{
|
||||
key: 'is_active',
|
||||
title: 'Статус',
|
||||
width: 110,
|
||||
render: (row) => h(NTag, {
|
||||
type: row.is_active ? 'success' : 'error',
|
||||
size: 'small', round: true, bordered: false,
|
||||
}, () => row.is_active ? 'Активен' : 'Отключён')
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
title: '',
|
||||
width: 60,
|
||||
align: 'center',
|
||||
render: (row) => h(NButton, {
|
||||
text: true, size: 'small', tag: Link,
|
||||
href: `/admin/metrics/items/${row.id}`,
|
||||
title: 'Редактировать',
|
||||
}, { icon: () => h(NIcon, { size: 18 }, () => h(TbPencil)) })
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout>
|
||||
<AppContainer>
|
||||
|
||||
<PageBanner
|
||||
title="Метрики"
|
||||
:icon="TbChartBar"
|
||||
:breadcrumbs="[{ label: 'Администратор', href: '/admin', icon: TbLayoutDashboard, tag: Link }]"
|
||||
>
|
||||
<template #meta>
|
||||
<NFlex align="center" :size="8">
|
||||
<NText depth="3" style="font-size: 13px;">
|
||||
{{ groups.length }} {{ groups.length === 1 ? 'группа' : 'групп' }}
|
||||
</NText>
|
||||
<NEl style="width: 3px; height: 3px; border-radius: 50%; background: currentColor; opacity: .3;" />
|
||||
<NText depth="3" style="font-size: 13px;">{{ items.length }} показателей</NText>
|
||||
</NFlex>
|
||||
</template>
|
||||
<template #actions>
|
||||
<NButton :tag="Link" href="/admin/metrics/groups/new">
|
||||
<template #icon><NIcon><TbPlus /></NIcon></template>
|
||||
Новая группа
|
||||
</NButton>
|
||||
<NButton type="primary" :tag="Link" href="/admin/metrics/items/new">
|
||||
<template #icon><NIcon><TbPlus /></NIcon></template>
|
||||
Новый показатель
|
||||
</NButton>
|
||||
</template>
|
||||
</PageBanner>
|
||||
|
||||
<NTabs type="line" animated>
|
||||
|
||||
<NTabPane name="groups" tab="Группы">
|
||||
<SectionCard :icon="TbStack2" title="Группы показателей" no-padding style="margin-top: 4px;">
|
||||
<template #header-extra>
|
||||
<NInput v-model:value="searchGroups" size="small" placeholder="Поиск..." clearable style="width: 200px;">
|
||||
<template #prefix><NIcon depth="3"><TbSearch /></NIcon></template>
|
||||
</NInput>
|
||||
</template>
|
||||
<NDataTable
|
||||
:columns="groupColumns"
|
||||
:data="filteredGroups"
|
||||
:bordered="false"
|
||||
size="small"
|
||||
flex-height
|
||||
style="height: calc(100vh - 356px); min-height: 200px;"
|
||||
/>
|
||||
</SectionCard>
|
||||
</NTabPane>
|
||||
|
||||
<NTabPane name="items" tab="Показатели">
|
||||
<SectionCard :icon="TbAdjustments" title="Показатели" no-padding style="margin-top: 4px;">
|
||||
<template #header-extra>
|
||||
<NInput v-model:value="searchItems" size="small" placeholder="Поиск..." clearable style="width: 200px;">
|
||||
<template #prefix><NIcon depth="3"><TbSearch /></NIcon></template>
|
||||
</NInput>
|
||||
</template>
|
||||
<NDataTable
|
||||
:columns="itemColumns"
|
||||
:data="filteredItems"
|
||||
:bordered="false"
|
||||
size="small"
|
||||
flex-height
|
||||
style="height: calc(100vh - 356px); min-height: 200px;"
|
||||
/>
|
||||
</SectionCard>
|
||||
</NTabPane>
|
||||
|
||||
</NTabs>
|
||||
|
||||
</AppContainer>
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.n-data-table-th) { background: transparent !important; }
|
||||
:deep(.n-data-table) { background: transparent; }
|
||||
:deep(.n-data-table-wrapper) { border-radius: 0; }
|
||||
:deep(.n-data-table-th .n-data-table-th__title) { font-size: 12px; }
|
||||
:deep(.n-data-table-td) { font-size: 13px; }
|
||||
:deep(.n-tabs-pane-wrapper) { padding-top: 0; }
|
||||
</style>
|
||||
160
resources/js/Pages/Admin/Metrics/Item.vue
Normal file
160
resources/js/Pages/Admin/Metrics/Item.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<script setup>
|
||||
import {
|
||||
NButton, NFlex, NForm, NFormItem, NInput, NSelect,
|
||||
NSwitch, NText, NIcon, NAlert, NGrid, NGi,
|
||||
} from 'naive-ui'
|
||||
import {
|
||||
TbAdjustments, TbLayoutDashboard, TbChartBar,
|
||||
} from 'vue-icons-plus/tb'
|
||||
import AppLayout from '../../../Layouts/AppLayout.vue'
|
||||
import AppContainer from '../../../Components/AppContainer.vue'
|
||||
import SectionCard from '../../../Components/SectionCard.vue'
|
||||
import PageBanner from '../../../Components/PageBanner.vue'
|
||||
import { useForm, Link, usePage } from '@inertiajs/vue3'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
item: { type: Object, default: null },
|
||||
})
|
||||
|
||||
const page = usePage()
|
||||
const flash = computed(() => page.props.flash ?? {})
|
||||
|
||||
const isEdit = computed(() => !!props.item)
|
||||
|
||||
const dataTypeOptions = [
|
||||
{ label: 'Целое число (integer)', value: 'integer' },
|
||||
{ label: 'Дробное число (float)', value: 'float' },
|
||||
{ label: 'Строка (string)', value: 'string' },
|
||||
{ label: 'Текст (text)', value: 'text' },
|
||||
{ label: 'Да / Нет (boolean)', value: 'boolean' },
|
||||
{ label: 'Список значений (select)', value: 'select' },
|
||||
]
|
||||
|
||||
const form = useForm({
|
||||
name: props.item?.name ?? '',
|
||||
description: props.item?.description ?? '',
|
||||
data_type: props.item?.data_type ?? 'string',
|
||||
is_active: props.item?.is_active ?? true,
|
||||
is_required: props.item?.is_required ?? false,
|
||||
default_value: props.item?.default_value ?? '',
|
||||
placeholder: props.item?.placeholder ?? '',
|
||||
})
|
||||
|
||||
const submit = () => {
|
||||
if (isEdit.value) {
|
||||
form.put(`/admin/metrics/items/${props.item.id}`)
|
||||
} else {
|
||||
form.post('/admin/metrics/items/new')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout>
|
||||
<AppContainer>
|
||||
|
||||
<NAlert v-if="flash.success" type="success" closable style="margin-bottom: 4px;">
|
||||
{{ flash.success }}
|
||||
</NAlert>
|
||||
|
||||
<PageBanner
|
||||
:title="isEdit ? item.name : 'Новый показатель'"
|
||||
:icon="TbAdjustments"
|
||||
:color="isEdit ? null : 'default'"
|
||||
:breadcrumbs="[
|
||||
{ label: 'Администратор', href: '/admin', icon: TbLayoutDashboard, tag: Link },
|
||||
{ label: 'Метрики', href: '/admin/metrics', icon: TbChartBar, tag: Link },
|
||||
]"
|
||||
>
|
||||
<template #meta>
|
||||
<NFlex align="center" :size="8">
|
||||
<NText depth="3" style="font-size: 13px;">
|
||||
{{ isEdit ? 'Редактирование показателя' : 'Создание нового показателя' }}
|
||||
</NText>
|
||||
<template v-if="isEdit">
|
||||
<span style="opacity: .3; font-size: 11px;">•</span>
|
||||
<NText depth="3" style="font-size: 12px; font-family: monospace;">{{ item.code }}</NText>
|
||||
</template>
|
||||
</NFlex>
|
||||
</template>
|
||||
<template #actions>
|
||||
<NButton :tag="Link" href="/admin/metrics">Отмена</NButton>
|
||||
<NButton type="primary" :loading="form.processing" @click="submit">
|
||||
{{ isEdit ? 'Сохранить' : 'Создать показатель' }}
|
||||
</NButton>
|
||||
</template>
|
||||
</PageBanner>
|
||||
|
||||
<NGrid :cols="2" :x-gap="16" :y-gap="16">
|
||||
|
||||
<NGi>
|
||||
<SectionCard title="Основные данные" :icon="TbAdjustments">
|
||||
<NForm label-placement="top">
|
||||
<NFormItem label="Название" :feedback="form.errors.name" :validation-status="form.errors.name ? 'error' : undefined">
|
||||
<NInput v-model:value="form.name" placeholder="Название показателя" />
|
||||
</NFormItem>
|
||||
<NFormItem label="Описание" :feedback="form.errors.description" :validation-status="form.errors.description ? 'error' : undefined">
|
||||
<NInput v-model:value="form.description" type="textarea" placeholder="Краткое описание" :rows="3" />
|
||||
</NFormItem>
|
||||
<NFormItem label="Тип данных" :feedback="form.errors.data_type" :validation-status="form.errors.data_type ? 'error' : undefined" style="margin-bottom: 0;">
|
||||
<NSelect v-model:value="form.data_type" :options="dataTypeOptions" />
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
</SectionCard>
|
||||
</NGi>
|
||||
|
||||
<NGi>
|
||||
<NFlex vertical :size="12">
|
||||
|
||||
<SectionCard
|
||||
title="Параметры"
|
||||
:icon="TbAdjustments"
|
||||
:color="form.is_active ? 'success' : null"
|
||||
>
|
||||
<NForm label-placement="top">
|
||||
<NFormItem label="Статус" style="margin-bottom: 12px;">
|
||||
<NFlex align="center" justify="space-between" style="width: 100%;">
|
||||
<div>
|
||||
<NText style="font-size: 14px; display: block;">
|
||||
{{ form.is_active ? 'Показатель активен' : 'Показатель отключён' }}
|
||||
</NText>
|
||||
<NText depth="3" style="font-size: 12px;">
|
||||
{{ form.is_active ? 'Доступен для использования в группах' : 'Не отображается в группах' }}
|
||||
</NText>
|
||||
</div>
|
||||
<NSwitch v-model:value="form.is_active" />
|
||||
</NFlex>
|
||||
</NFormItem>
|
||||
<NFormItem label="Обязательный" style="margin-bottom: 0;">
|
||||
<NFlex align="center" justify="space-between" style="width: 100%;">
|
||||
<div>
|
||||
<NText style="font-size: 14px; display: block;">
|
||||
{{ form.is_required ? 'Обязательно для заполнения' : 'Необязательный' }}
|
||||
</NText>
|
||||
</div>
|
||||
<NSwitch v-model:value="form.is_required" />
|
||||
</NFlex>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard title="Ввод" :icon="TbAdjustments">
|
||||
<NForm label-placement="top">
|
||||
<NFormItem label="Placeholder" :feedback="form.errors.placeholder" :validation-status="form.errors.placeholder ? 'error' : undefined">
|
||||
<NInput v-model:value="form.placeholder" placeholder="Текст-подсказка в поле" />
|
||||
</NFormItem>
|
||||
<NFormItem label="Значение по умолчанию" :feedback="form.errors.default_value" :validation-status="form.errors.default_value ? 'error' : undefined" style="margin-bottom: 0;">
|
||||
<NInput v-model:value="form.default_value" placeholder="Оставьте пустым если не нужно" />
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
</SectionCard>
|
||||
|
||||
</NFlex>
|
||||
</NGi>
|
||||
|
||||
</NGrid>
|
||||
|
||||
</AppContainer>
|
||||
</AppLayout>
|
||||
</template>
|
||||
Reference in New Issue
Block a user