Files
onboard/resources/js/Pages/Admin/Users/User.vue
brusnitsyn 739168d427 Обновлен стартовый экран
Переписаны запросы для статистики, отчетов
Добавлена интеграция отчета сестры
2026-05-28 22:10:00 +09:00

283 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
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: () => ({}) },
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>
<AppContainer>
<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-tabs-pane-wrapper) { padding-top: 0; }
:deep(.n-transfer) { border: none !important; border-radius: 0 !important; }
</style>