Обновлен стартовый экран
Переписаны запросы для статистики, отчетов Добавлена интеграция отчета сестры
This commit is contained in:
@@ -1,106 +1,127 @@
|
||||
<script setup>
|
||||
import {NButton, NFlex, NGi, NGrid, NH2, NForm, NFormItem, NInput, NTransfer, NSwitch} from "naive-ui";
|
||||
import AppLayout from "../../../Layouts/AppLayout.vue";
|
||||
import AppContainer from "../../../Components/AppContainer.vue";
|
||||
import AppPanel from "../../../Components/AppPanel.vue";
|
||||
import {useForm} from "@inertiajs/vue3";
|
||||
import {computed, ref} from "vue";
|
||||
import {
|
||||
NButton, NFlex, NForm, NFormItem, NInput,
|
||||
NTransfer, NSwitch, NSelect, NText, NEl, NIcon,
|
||||
} from 'naive-ui'
|
||||
import { TbUser, TbAt, TbLock, TbBuildingHospital, TbUserShield, TbLayoutDashboard, TbUsers } 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 } from '@inertiajs/vue3'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
departments: {
|
||||
type: Array,
|
||||
default: []
|
||||
},
|
||||
roles: {
|
||||
type: Array,
|
||||
default: []
|
||||
}
|
||||
departments: { type: Array, default: () => [] },
|
||||
roles: { type: Array, default: () => [] },
|
||||
})
|
||||
|
||||
const departmentOptions = computed(() => props.departments.map(itm => ({
|
||||
label: itm.name_full,
|
||||
value: itm.department_id
|
||||
})))
|
||||
const roleOptions = computed(() => props.roles.map(itm => ({
|
||||
label: itm.name,
|
||||
value: itm.role_id
|
||||
const departmentOptions = computed(() => props.departments.map(d => ({
|
||||
label: d.name_full,
|
||||
value: d.department_id,
|
||||
})))
|
||||
|
||||
const form = ref({
|
||||
'name': '',
|
||||
'login': '',
|
||||
'password': '',
|
||||
'is_active': true,
|
||||
const roleOptions = computed(() => props.roles.map(r => ({
|
||||
label: r.name,
|
||||
value: r.role_id,
|
||||
})))
|
||||
|
||||
const form = useForm({
|
||||
name: '',
|
||||
login: '',
|
||||
password: '',
|
||||
is_active: true,
|
||||
department_id: null,
|
||||
departments: [],
|
||||
roles: [],
|
||||
})
|
||||
|
||||
const submit = () => form.post('/admin/users/new')
|
||||
</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>
|
||||
</NFlex>
|
||||
</template>
|
||||
<AppContainer>
|
||||
<NGrid cols="2" x-gap="16">
|
||||
<NGi>
|
||||
<AppPanel header="Основная информация">
|
||||
<NForm v-model:model="form">
|
||||
<NFormItem label="Имя">
|
||||
<NInput v-model:value="form.name" />
|
||||
</NFormItem>
|
||||
<NFormItem label="Привязка к ресурсу МИС">
|
||||
<NInput v-model:value="form.password" />
|
||||
</NFormItem>
|
||||
<NFormItem label="Основное отделение">
|
||||
<NInput v-model:value="form.password" />
|
||||
</NFormItem>
|
||||
<NFormItem label-placement="left" label-width="auto" label-style="align-items: center;" label="Пользователь заблокирован">
|
||||
<NSwitch />
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
</AppPanel>
|
||||
</NGi>
|
||||
<NGi>
|
||||
<AppPanel header="Данные для входа">
|
||||
<NForm v-model:model="form">
|
||||
<NFormItem label="Логин">
|
||||
<NInput v-model:value="form.login" />
|
||||
</NFormItem>
|
||||
<NFormItem label="Пароль">
|
||||
<NInput type="password" v-model:value="form.password" show-password-toggle />
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
</AppPanel>
|
||||
</NGi>
|
||||
</NGrid>
|
||||
|
||||
<AppPanel no-padding header="Привязка к отделениям">
|
||||
<NTransfer
|
||||
v-model:value="form.departments"
|
||||
virtual-scroll
|
||||
:options="departmentOptions"
|
||||
source-filterable
|
||||
target-filterable
|
||||
/>
|
||||
</AppPanel>
|
||||
<PageBanner
|
||||
title="Новый пользователь"
|
||||
:icon="TbUser"
|
||||
color="default"
|
||||
:breadcrumbs="[
|
||||
{ label: 'Администратор', href: '/admin', icon: TbLayoutDashboard, tag: Link },
|
||||
{ label: 'Учётные записи', href: '/admin/users', icon: TbUsers, tag: Link },
|
||||
]"
|
||||
>
|
||||
<template #meta>
|
||||
<NText depth="3" style="font-size: 13px;">
|
||||
Заполните данные для создания учётной записи
|
||||
</NText>
|
||||
</template>
|
||||
<template #actions>
|
||||
<NButton :tag="Link" href="/admin/users">Отмена</NButton>
|
||||
<NButton type="primary" :loading="form.processing" @click="submit">
|
||||
Создать пользователя
|
||||
</NButton>
|
||||
</template>
|
||||
</PageBanner>
|
||||
|
||||
<NEl style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
|
||||
|
||||
<SectionCard title="Личные данные" :icon="TbUser">
|
||||
<NForm label-placement="top">
|
||||
<NFormItem label="Имя" :feedback="form.errors.name" :validation-status="form.errors.name ? 'error' : undefined">
|
||||
<NInput v-model:value="form.name" placeholder="Иванов Иван Иванович">
|
||||
<template #prefix><NIcon depth="3"><TbUser /></NIcon></template>
|
||||
</NInput>
|
||||
</NFormItem>
|
||||
<NFormItem label="Основное отделение" :feedback="form.errors.department_id" :validation-status="form.errors.department_id ? 'error' : undefined">
|
||||
<NSelect v-model:value="form.department_id" :options="departmentOptions" filterable placeholder="Выберите отделение" />
|
||||
</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_active ? 'Пользователь активен' : 'Пользователь заблокирован' }}
|
||||
</NText>
|
||||
<NText depth="3" style="font-size: 12px;">
|
||||
{{ form.is_active ? 'Может входить в систему' : 'Вход запрещён' }}
|
||||
</NText>
|
||||
</div>
|
||||
<NSwitch v-model:value="form.is_active" />
|
||||
</NFlex>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard title="Данные для входа" :icon="TbLock">
|
||||
<NForm label-placement="top">
|
||||
<NFormItem label="Логин" :feedback="form.errors.login" :validation-status="form.errors.login ? 'error' : undefined">
|
||||
<NInput v-model:value="form.login" placeholder="ivanov_i">
|
||||
<template #prefix><NIcon depth="3"><TbAt /></NIcon></template>
|
||||
</NInput>
|
||||
</NFormItem>
|
||||
<NFormItem label="Пароль" :feedback="form.errors.password" :validation-status="form.errors.password ? 'error' : undefined" style="margin-bottom: 0;">
|
||||
<NInput v-model:value="form.password" type="password" show-password-toggle placeholder="Минимум 6 символов">
|
||||
<template #prefix><NIcon depth="3"><TbLock /></NIcon></template>
|
||||
</NInput>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
</SectionCard>
|
||||
|
||||
</NEl>
|
||||
|
||||
<SectionCard title="Роли" :icon="TbUserShield" no-padding>
|
||||
<NTransfer v-model:value="form.roles" :options="roleOptions" source-filterable source-title="Доступные роли" target-title="Назначенные роли" />
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard title="Дополнительные отделения" :icon="TbBuildingHospital" no-padding>
|
||||
<NTransfer v-model:value="form.departments" virtual-scroll :options="departmentOptions" source-filterable target-filterable source-title="Все отделения" target-title="Доступные отделения" />
|
||||
</SectionCard>
|
||||
|
||||
<AppPanel no-padding header="Роли">
|
||||
<NTransfer
|
||||
v-model:value="form.roles"
|
||||
virtual-scroll
|
||||
:options="roleOptions"
|
||||
source-filterable
|
||||
target-filterable
|
||||
/>
|
||||
</AppPanel>
|
||||
</AppContainer>
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
:deep(.n-transfer) { border: none !important; border-radius: 0 !important; }
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,120 +1,282 @@
|
||||
<script setup>
|
||||
import {NButton, NDataTable, NScrollbar, NList, NListItem, NGrid, NGi, NFlex, NH2, NSpace} from "naive-ui";
|
||||
import AppLayout from "../../../Layouts/AppLayout.vue";
|
||||
import AppPanel from "../../../Components/AppPanel.vue";
|
||||
import AppContainer from "../../../Components/AppContainer.vue";
|
||||
import {
|
||||
NButton, NFlex, NForm, NFormItem, NInput, NTransfer,
|
||||
NSwitch, NSelect, NAlert, NTabs, NTabPane,
|
||||
NAvatar, NTag, NText, NEl, NGrid, NGi, NIcon,
|
||||
} from 'naive-ui'
|
||||
import {
|
||||
TbUser, TbAt, TbBuildingHospital, TbShieldLock,
|
||||
TbLock, TbAlertTriangle, TbCheck, TbUserShield,
|
||||
TbLayoutDashboard, TbUsers,
|
||||
} 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({
|
||||
userData: {
|
||||
type: Object,
|
||||
default: {}
|
||||
},
|
||||
roles: {
|
||||
type: Array,
|
||||
default: []
|
||||
},
|
||||
departments: {
|
||||
type: Array,
|
||||
default: []
|
||||
}
|
||||
userData: { type: Object, default: () => ({}) },
|
||||
userRoleIds: { type: Array, default: () => [] },
|
||||
userDepartmentIds: { type: Array, default: () => [] },
|
||||
allRoles: { type: Array, default: () => [] },
|
||||
allDepartments: { type: Array, default: () => [] },
|
||||
})
|
||||
|
||||
const page = usePage()
|
||||
const flash = computed(() => page.props.flash ?? {})
|
||||
|
||||
const departmentOptions = computed(() => props.allDepartments.map(d => ({
|
||||
label: d.name_full,
|
||||
value: d.department_id,
|
||||
})))
|
||||
|
||||
const roleOptions = computed(() => props.allRoles.map(r => ({
|
||||
label: r.name,
|
||||
value: r.role_id,
|
||||
})))
|
||||
|
||||
const initials = computed(() => {
|
||||
const parts = (props.userData.name ?? '').trim().split(/\s+/)
|
||||
return parts.slice(0, 2).map(p => p[0]?.toUpperCase() ?? '').join('')
|
||||
})
|
||||
|
||||
const form = useForm({
|
||||
name: props.userData.name,
|
||||
login: props.userData.login,
|
||||
is_active: props.userData.is_active,
|
||||
department_id: props.userData.department_id,
|
||||
departments: [...props.userDepartmentIds],
|
||||
roles: [...props.userRoleIds],
|
||||
})
|
||||
|
||||
const save = () => form.put(`/admin/users/${props.userData.id}`)
|
||||
|
||||
const passwordForm = useForm({
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
})
|
||||
|
||||
const resetPassword = () => {
|
||||
passwordForm.put(`/admin/users/${props.userData.id}/password`, {
|
||||
onSuccess: () => passwordForm.reset(),
|
||||
})
|
||||
}
|
||||
|
||||
const toggleBlock = () => {
|
||||
form.is_active = !form.is_active
|
||||
save()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout>
|
||||
<template #header>
|
||||
<NFlex align="center" justify="space-between" class="max-w-6xl mx-auto mt-6 mb-4 w-full">
|
||||
<NH2>
|
||||
{{ userData.name }}
|
||||
</NH2>
|
||||
<NSpace>
|
||||
<NButton type="primary">
|
||||
Создать учетную запись
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NFlex>
|
||||
</template>
|
||||
<AppContainer>
|
||||
<NGrid cols="2" x-gap="12" y-gap="12">
|
||||
<NGi>
|
||||
<AppPanel header="Информация о пользователе" min-h="148px" max-h="148px">
|
||||
<NSpace vertical>
|
||||
<NFlex align="center" justify="space-between">
|
||||
<div>
|
||||
Имя
|
||||
</div>
|
||||
<div>
|
||||
{{ userData.name }}
|
||||
</div>
|
||||
</NFlex>
|
||||
<NFlex align="center" justify="space-between">
|
||||
<div>
|
||||
Логин
|
||||
</div>
|
||||
<div>
|
||||
{{ userData.login }}
|
||||
</div>
|
||||
</NFlex>
|
||||
<NFlex align="center" justify="space-between">
|
||||
<div>
|
||||
Активен
|
||||
</div>
|
||||
<div>
|
||||
{{ userData.is_active ? 'Да' : 'Нет' }}
|
||||
</div>
|
||||
</NFlex>
|
||||
<NFlex align="center" justify="space-between">
|
||||
<div>
|
||||
Дата создания
|
||||
</div>
|
||||
<div>
|
||||
{{ userData.created_at }}
|
||||
</div>
|
||||
</NFlex>
|
||||
<NFlex align="center" justify="space-between">
|
||||
<div>
|
||||
Дата обновления
|
||||
</div>
|
||||
<div>
|
||||
{{ userData.updated_at }}
|
||||
</div>
|
||||
</NFlex>
|
||||
</NSpace>
|
||||
</AppPanel>
|
||||
</NGi>
|
||||
<NGi>
|
||||
|
||||
</NGi>
|
||||
<NGi>
|
||||
<AppPanel :header="`Доступные отделения (${departments.length})`" min-h="148px" max-h="148px">
|
||||
<NList>
|
||||
<NListItem v-for="department in departments">
|
||||
{{ department.name_full }}
|
||||
</NListItem>
|
||||
</NList>
|
||||
</AppPanel>
|
||||
</NGi>
|
||||
<NGi>
|
||||
<AppPanel :header="`Доступные роли (${roles.length})`" min-h="148px" max-h="148px">
|
||||
<NList>
|
||||
<NListItem v-for="role in roles">
|
||||
{{ role.name }}
|
||||
</NListItem>
|
||||
</NList>
|
||||
</AppPanel>
|
||||
</NGi>
|
||||
</NGrid>
|
||||
<NAlert v-if="flash.success" type="success" closable style="margin-bottom: 4px;">
|
||||
{{ flash.success }}
|
||||
</NAlert>
|
||||
|
||||
<PageBanner
|
||||
:breadcrumbs="[
|
||||
{ label: 'Администратор', href: '/admin', icon: TbLayoutDashboard, tag: Link },
|
||||
{ label: 'Учётные записи', href: '/admin/users', icon: TbUsers, tag: Link },
|
||||
]"
|
||||
>
|
||||
<template #icon>
|
||||
<NAvatar round :size="40"
|
||||
style="background: color-mix(in srgb, var(--primary-color) 22%, transparent); color: var(--primary-color); font-size: 15px; font-weight: 600;"
|
||||
>{{ initials }}</NAvatar>
|
||||
</template>
|
||||
<template #title>
|
||||
<NFlex align="center" :size="8" :wrap="false">
|
||||
<span style="font-size: 20px; font-weight: 700; line-height: 1.2;" class="truncate">
|
||||
{{ userData.name }}
|
||||
</span>
|
||||
<NTag :type="userData.is_active ? 'success' : 'error'" size="small" round :bordered="false">
|
||||
{{ userData.is_active ? 'Активен' : 'Заблокирован' }}
|
||||
</NTag>
|
||||
</NFlex>
|
||||
</template>
|
||||
<template #meta>
|
||||
<NFlex :size="12">
|
||||
<NText depth="3" style="font-size: 13px;">@{{ userData.login }}</NText>
|
||||
<NText depth="3" style="font-size: 13px;">Создан: {{ userData.created_at }}</NText>
|
||||
<NText depth="3" style="font-size: 13px;">Изменён: {{ userData.updated_at }}</NText>
|
||||
</NFlex>
|
||||
</template>
|
||||
<template #actions>
|
||||
<NButton :tag="Link" href="/admin/users">Назад</NButton>
|
||||
<NButton type="primary" :loading="form.processing" @click="save">Сохранить</NButton>
|
||||
</template>
|
||||
</PageBanner>
|
||||
|
||||
<!-- Табы -->
|
||||
<NTabs type="line" animated>
|
||||
|
||||
<!-- ── Основное ── -->
|
||||
<NTabPane name="general" tab="Основное">
|
||||
<NGrid :cols="2" :x-gap="16" :y-gap="16" style="margin-top: 4px;">
|
||||
|
||||
<NGi>
|
||||
<SectionCard title="Личные данные" :icon="TbUser">
|
||||
<NForm label-placement="top">
|
||||
<NFormItem label="Имя" :feedback="form.errors.name" :validation-status="form.errors.name ? 'error' : undefined">
|
||||
<NInput v-model:value="form.name">
|
||||
<template #prefix><NIcon depth="3"><TbUser /></NIcon></template>
|
||||
</NInput>
|
||||
</NFormItem>
|
||||
<NFormItem label="Логин" :feedback="form.errors.login" :validation-status="form.errors.login ? 'error' : undefined">
|
||||
<NInput v-model:value="form.login">
|
||||
<template #prefix><NIcon depth="3"><TbAt /></NIcon></template>
|
||||
</NInput>
|
||||
</NFormItem>
|
||||
<NFormItem label="Основное отделение" :feedback="form.errors.department_id" :validation-status="form.errors.department_id ? 'error' : undefined" style="margin-bottom: 0;">
|
||||
<NSelect v-model:value="form.department_id" :options="departmentOptions" filterable />
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
</SectionCard>
|
||||
</NGi>
|
||||
|
||||
<NGi>
|
||||
<NFlex vertical :size="12">
|
||||
|
||||
<SectionCard
|
||||
title="Статус аккаунта"
|
||||
:icon="TbShieldLock"
|
||||
:color="form.is_active ? 'success' : 'error'"
|
||||
>
|
||||
<NFlex justify="space-between" align="center">
|
||||
<div>
|
||||
<NText style="font-size: 14px; font-weight: 500; display: block;">
|
||||
{{ form.is_active ? 'Пользователь активен' : 'Пользователь заблокирован' }}
|
||||
</NText>
|
||||
<NText depth="3" style="font-size: 12px; margin-top: 2px; display: block;">
|
||||
{{ form.is_active ? 'Может входить в систему' : 'Вход в систему запрещён' }}
|
||||
</NText>
|
||||
</div>
|
||||
<NSwitch v-model:value="form.is_active" />
|
||||
</NFlex>
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard title="Роли" :icon="TbUserShield">
|
||||
<NFlex :size="6" wrap>
|
||||
<NTag
|
||||
v-for="roleId in form.roles" :key="roleId"
|
||||
type="info" :bordered="false" round size="small"
|
||||
>
|
||||
{{ allRoles.find(r => r.role_id === roleId)?.name ?? roleId }}
|
||||
</NTag>
|
||||
<NText v-if="form.roles.length === 0" depth="3" style="font-size: 13px;">
|
||||
Роли не назначены
|
||||
</NText>
|
||||
</NFlex>
|
||||
<NText depth="3" style="font-size: 11px; margin-top: 8px; display: block;">
|
||||
Изменить роли можно во вкладке «Доступ»
|
||||
</NText>
|
||||
</SectionCard>
|
||||
|
||||
</NFlex>
|
||||
</NGi>
|
||||
</NGrid>
|
||||
</NTabPane>
|
||||
|
||||
<!-- ── Безопасность ── -->
|
||||
<NTabPane name="security" tab="Безопасность">
|
||||
<NGrid :cols="2" :x-gap="16" :y-gap="16" style="margin-top: 4px;">
|
||||
|
||||
<NGi>
|
||||
<SectionCard title="Смена пароля" :icon="TbLock">
|
||||
<NText depth="3" style="font-size: 13px; display: block; margin-bottom: 14px; line-height: 1.5;">
|
||||
После смены пароля все активные сессии пользователя будут завершены.
|
||||
</NText>
|
||||
<NForm label-placement="top">
|
||||
<NFormItem label="Новый пароль" :feedback="passwordForm.errors.password" :validation-status="passwordForm.errors.password ? 'error' : undefined">
|
||||
<NInput v-model:value="passwordForm.password" type="password" show-password-toggle placeholder="Минимум 6 символов" />
|
||||
</NFormItem>
|
||||
<NFormItem label="Подтверждение" :feedback="passwordForm.errors.password_confirmation" :validation-status="passwordForm.errors.password_confirmation ? 'error' : undefined">
|
||||
<NInput v-model:value="passwordForm.password_confirmation" type="password" show-password-toggle placeholder="Повторите пароль" />
|
||||
</NFormItem>
|
||||
<NFormItem :show-label="false" :show-feedback="false" style="margin-bottom: 0;">
|
||||
<NButton
|
||||
type="primary"
|
||||
:loading="passwordForm.processing"
|
||||
:disabled="!passwordForm.password || !passwordForm.password_confirmation"
|
||||
@click="resetPassword"
|
||||
>
|
||||
<template #icon><NIcon><TbCheck /></NIcon></template>
|
||||
Изменить пароль
|
||||
</NButton>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
</SectionCard>
|
||||
</NGi>
|
||||
|
||||
<NGi>
|
||||
<SectionCard title="Опасная зона" :icon="TbAlertTriangle" color="error">
|
||||
<NFlex justify="space-between" align="center">
|
||||
<div>
|
||||
<NText style="font-size: 14px; font-weight: 500; display: block;">
|
||||
{{ form.is_active ? 'Заблокировать пользователя' : 'Разблокировать пользователя' }}
|
||||
</NText>
|
||||
<NText depth="3" style="font-size: 12px; margin-top: 2px; display: block; line-height: 1.4; max-width: 220px;">
|
||||
{{ form.is_active
|
||||
? 'Пользователь потеряет доступ ко всем разделам системы'
|
||||
: 'Пользователь снова сможет входить в систему' }}
|
||||
</NText>
|
||||
</div>
|
||||
<NButton
|
||||
:type="form.is_active ? 'error' : 'default'"
|
||||
size="small"
|
||||
:loading="form.processing"
|
||||
@click="toggleBlock"
|
||||
>
|
||||
{{ form.is_active ? 'Заблокировать' : 'Разблокировать' }}
|
||||
</NButton>
|
||||
</NFlex>
|
||||
</SectionCard>
|
||||
</NGi>
|
||||
|
||||
</NGrid>
|
||||
</NTabPane>
|
||||
|
||||
<!-- ── Доступ ── -->
|
||||
<NTabPane name="access" tab="Доступ">
|
||||
<NFlex vertical :size="16" style="margin-top: 4px;">
|
||||
|
||||
<SectionCard title="Роли" :icon="TbUserShield" no-padding>
|
||||
<NTransfer
|
||||
v-model:value="form.roles"
|
||||
:options="roleOptions"
|
||||
source-filterable
|
||||
source-title="Доступные роли"
|
||||
target-title="Назначенные роли"
|
||||
/>
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard title="Отделения" :icon="TbBuildingHospital" no-padding>
|
||||
<NTransfer
|
||||
v-model:value="form.departments"
|
||||
virtual-scroll
|
||||
:options="departmentOptions"
|
||||
source-filterable
|
||||
target-filterable
|
||||
source-title="Все отделения"
|
||||
target-title="Доступные отделения"
|
||||
/>
|
||||
</SectionCard>
|
||||
|
||||
</NFlex>
|
||||
</NTabPane>
|
||||
|
||||
</NTabs>
|
||||
|
||||
</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-tabs-pane-wrapper) { padding-top: 0; }
|
||||
:deep(.n-transfer) { border: none !important; border-radius: 0 !important; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user