Обновлен стартовый экран
Переписаны запросы для статистики, отчетов Добавлена интеграция отчета сестры
This commit is contained in:
@@ -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