Обновлен стартовый экран

Переписаны запросы для статистики, отчетов
Добавлена интеграция отчета сестры
This commit is contained in:
brusnitsyn
2026-05-28 22:10:00 +09:00
parent 90e0d04dfd
commit 739168d427
96 changed files with 6663 additions and 1465 deletions

View File

@@ -1,81 +1,154 @@
<script setup>
import AppLayout from "../../../Layouts/AppLayout.vue";
import {NFlex, NH2, NSpace, NButton, NDataTable} from 'naive-ui'
import {h, ref} from "vue";
import {Link} from "@inertiajs/vue3";
import {
NFlex, NButton, NDataTable, NAvatar, NTag,
NText, NEl, NIcon, NInput,
} from 'naive-ui'
import { TbUsers, TbUserCheck, TbUserOff, TbSearch, TbPlus, TbPencil, TbLayoutDashboard } 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'
import { useThemeVars } from 'naive-ui'
const props = defineProps({
users: {
type: Array,
default: []
}
users: { type: Array, default: () => [] }
})
const columns = ref([
const themeVars = useThemeVars()
const search = ref('')
const filteredUsers = computed(() => {
const q = search.value.toLowerCase().trim()
if (!q) return props.users
return props.users.filter(u =>
u.name.toLowerCase().includes(q) || u.login.toLowerCase().includes(q)
)
})
const activeCount = computed(() => props.users.filter(u => u.is_active).length)
const blockedCount = computed(() => props.users.filter(u => !u.is_active).length)
const initials = (name) =>
(name ?? '').trim().split(/\s+/).slice(0, 2).map(p => p[0]?.toUpperCase() ?? '').join('')
const columns = computed(() => [
{
title: 'Имя',
key: 'name'
key: 'name',
title: 'Пользователь',
render: (row) => h(NFlex, { align: 'center', size: 10 }, () => [
h(NAvatar, {
round: true,
size: 32,
style: `background: color-mix(in srgb, ${themeVars.value.primaryColor} 20%, transparent); color: ${themeVars.value.primaryColor}; font-size: 12px; font-weight: 600; flex-shrink: 0;`
}, () => initials(row.name)),
h(NFlex, { vertical: true, size: 2 }, () => [
h(NText, { style: 'font-weight: 500; font-size: 14px; line-height: 1.2;' }, () => row.name),
h(NText, { depth: 3, style: 'font-size: 12px;' }, () => `@${row.login}`),
]),
])
},
{
title: 'Логин',
key: 'login'
},
{
title: 'Активен',
key: 'is_active',
render: (row) => {
return row.is_active ? 'Да' : 'Нет'
}
title: 'Статус',
width: 140,
render: (row) => h(NTag, {
type: row.is_active ? 'success' : 'error',
size: 'small',
round: true,
bordered: false,
}, () => row.is_active ? 'Активен' : 'Заблокирован')
},
{
title: 'Дата создания',
key: 'created_at'
key: 'created_at',
title: 'Создан',
width: 160,
render: (row) => h(NText, { depth: 3, style: 'font-size: 13px;' }, () => row.created_at)
},
{
title: 'Дата изменения',
key: 'updated_at'
},
{
title: 'Действия',
key: 'actions',
render: (row) => {
return h(NButton, {
text: true,
size: 'small',
tag: Link,
href: `/admin/users/${row.id}`
}, 'Редактировать')
}
title: '',
width: 60,
align: 'center',
render: (row) => h(NButton, {
text: true,
size: 'small',
tag: Link,
href: `/admin/users/${row.id}`,
title: 'Редактировать',
}, { icon: () => h(NIcon, { size: 18 }, () => h(TbPencil)) })
},
])
</script>
<template>
<AppLayout>
<template #header>
<NFlex align="center" justify="space-between" class="max-w-6xl mx-auto mt-6 mb-4 w-full">
<NH2>
Учетные записи
</NH2>
<NSpace>
<NButton :tag="Link" href="/admin/users/new" type="primary">
Создать учетную запись
<AppContainer>
<PageBanner
title="Учётные записи"
:icon="TbUsers"
:breadcrumbs="[{ label: 'Администратор', href: '/admin', icon: TbLayoutDashboard, tag: Link }]"
>
<template #meta>
<NFlex align="center" :size="8">
<NText depth="3" style="font-size: 13px;">
{{ props.users.length }} {{ props.users.length === 1 ? 'пользователь' : 'пользователей' }}
</NText>
<NEl style="width: 3px; height: 3px; border-radius: 50%; background: currentColor; opacity: .3;" />
<NFlex align="center" :size="4">
<NIcon size="12" style="color: var(--success-color);"><TbUserCheck /></NIcon>
<NText depth="3" style="font-size: 13px;">{{ activeCount }} активных</NText>
</NFlex>
<template v-if="blockedCount > 0">
<NEl style="width: 3px; height: 3px; border-radius: 50%; background: currentColor; opacity: .3;" />
<NFlex align="center" :size="4">
<NIcon size="12" style="color: var(--error-color);"><TbUserOff /></NIcon>
<NText style="font-size: 13px; color: var(--error-color);">{{ blockedCount }} заблокировано</NText>
</NFlex>
</template>
</NFlex>
</template>
<template #actions>
<NButton type="primary" :tag="Link" href="/admin/users/new">
<template #icon><NIcon><TbPlus /></NIcon></template>
Создать пользователя
</NButton>
</NSpace>
</NFlex>
</template>
<NDataTable class="max-w-6xl mx-auto mb-4 w-full" :columns="columns" :data="users" />
</template>
</PageBanner>
<!-- Таблица -->
<SectionCard :icon="TbUsers" title="Список пользователей" no-padding>
<template #header-extra>
<NInput
v-model:value="search"
size="small"
placeholder="Поиск..."
clearable
style="width: 200px;"
>
<template #prefix><NIcon depth="3"><TbSearch /></NIcon></template>
</NInput>
</template>
<NDataTable
:columns="columns"
:data="filteredUsers"
:bordered="false"
size="small"
flex-height
style="height: calc(100vh - 300px); min-height: 200px;"
/>
</SectionCard>
</AppContainer>
</AppLayout>
</template>
<style scoped>
:deep(.n-h) {
margin: 0;
}
:deep(.n-data-table-th),
:deep(.n-data-table-td) {
font-size: var(--n-font-size);
}
: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; }
</style>