337 lines
11 KiB
TypeScript
337 lines
11 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||
import { useApolloClient, useQuery } from '@apollo/client/react'
|
||
import { Building2, Pencil, Trash2 } from 'lucide-react'
|
||
|
||
import {
|
||
GET_ORGANISATION_QUERY,
|
||
GET_USERS_PAGE_QUERY,
|
||
} from '../../entities/employee/api/employee.graphql'
|
||
import type {
|
||
Employee,
|
||
GetOrganisationData,
|
||
GetOrganisationVariables,
|
||
GetUsersPageData,
|
||
GetUsersPageVariables,
|
||
Organisation,
|
||
} from '../../entities/employee/model/types'
|
||
import { ConfirmDangerDialog } from '../../widgets/ConfirmDangerDialog/ConfirmDangerDialog'
|
||
import { AddOrganisationModal } from '../EmployeesPage/components/AddOrganisationModal/AddOrganisationModal'
|
||
import { OrganisationPolicyCard } from './components/OrganisationPolicyCard/OrganisationPolicyCard'
|
||
|
||
import './OrganisationPage.scss'
|
||
|
||
type LoadStatus = 'idle' | 'loading' | 'success' | 'error'
|
||
|
||
function getEmployeeFullName(employee: Employee) {
|
||
return [employee.lastName, employee.firstName, employee.middleName]
|
||
.filter(Boolean)
|
||
.join(' ')
|
||
}
|
||
|
||
function getEmployeeRoleLabel(role: string) {
|
||
if (role === 'Admin') return 'Администратор'
|
||
if (role === 'User') return 'Пользователь'
|
||
|
||
return role
|
||
}
|
||
|
||
function getOrganisationInitials(name: string) {
|
||
const words = name.trim().split(/\s+/).filter(Boolean)
|
||
|
||
if (words.length === 0) return 'ОР'
|
||
|
||
return words
|
||
.slice(0, 2)
|
||
.map((word) => word[0])
|
||
.join('')
|
||
.toUpperCase()
|
||
}
|
||
|
||
function formatCreationDate(timestamp: number) {
|
||
if (!timestamp) return 'Нет данных'
|
||
|
||
return new Intl.DateTimeFormat('ru-RU', {
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
year: 'numeric',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
}).format(new Date(timestamp))
|
||
}
|
||
|
||
export function OrganisationPage() {
|
||
const client = useApolloClient()
|
||
const navigate = useNavigate()
|
||
const { organisationId } = useParams()
|
||
|
||
const [employees, setEmployees] = useState<Employee[]>([])
|
||
const [employeesStatus, setEmployeesStatus] = useState<LoadStatus>('idle')
|
||
const [editingOrganisation, setEditingOrganisation] =
|
||
useState<Organisation | null>(null)
|
||
const [deletingOrganisation, setDeletingOrganisation] =
|
||
useState<Organisation | null>(null)
|
||
|
||
const {
|
||
data: organisationData,
|
||
loading: organisationLoading,
|
||
error: organisationError,
|
||
} = useQuery<GetOrganisationData, GetOrganisationVariables>(
|
||
GET_ORGANISATION_QUERY,
|
||
{
|
||
variables: {
|
||
id: organisationId ?? '',
|
||
},
|
||
skip: !organisationId,
|
||
fetchPolicy: 'network-only',
|
||
},
|
||
)
|
||
|
||
const organisation = organisationData?.getOrganisation ?? null
|
||
|
||
const organisationInitials = useMemo(() => {
|
||
if (!organisation) return 'ОР'
|
||
|
||
return getOrganisationInitials(organisation.name)
|
||
}, [organisation])
|
||
|
||
const loadEmployees = useCallback(async () => {
|
||
if (!organisationId) {
|
||
setEmployeesStatus('error')
|
||
return
|
||
}
|
||
|
||
setEmployeesStatus('loading')
|
||
|
||
try {
|
||
let nextKey: string | null = null
|
||
let pageCounter = 0
|
||
const allEmployees: Employee[] = []
|
||
|
||
do {
|
||
const result = await client.query<
|
||
GetUsersPageData,
|
||
GetUsersPageVariables
|
||
>({
|
||
query: GET_USERS_PAGE_QUERY,
|
||
variables: nextKey ? { key: nextKey } : {},
|
||
fetchPolicy: 'network-only',
|
||
})
|
||
|
||
const pageData = result.data?.getUsersPage
|
||
|
||
if (!pageData) break
|
||
|
||
allEmployees.push(
|
||
...pageData.page.filter(
|
||
(employee) => String(employee.orgId) === organisationId,
|
||
),
|
||
)
|
||
|
||
nextKey = pageData.nextKey
|
||
pageCounter += 1
|
||
} while (nextKey && pageCounter < 50)
|
||
|
||
setEmployees(allEmployees)
|
||
setEmployeesStatus('success')
|
||
} catch {
|
||
setEmployeesStatus('error')
|
||
}
|
||
}, [client, organisationId])
|
||
|
||
useEffect(() => {
|
||
void loadEmployees()
|
||
}, [loadEmployees])
|
||
|
||
function handleConfirmDeleteOrganisation() {
|
||
if (!deletingOrganisation) return
|
||
|
||
console.log('Удаление организации пока без мутации', deletingOrganisation)
|
||
|
||
setDeletingOrganisation(null)
|
||
navigate('/employees')
|
||
}
|
||
|
||
if (!organisationId) {
|
||
return (
|
||
<section className="organisation-page">
|
||
<div className="organisation-page__empty">
|
||
<h2>Некорректный ID организации</h2>
|
||
<Link to="/employees">Вернуться к списку</Link>
|
||
</div>
|
||
</section>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<section className="organisation-page">
|
||
<div className="organisation-breadcrumbs">
|
||
<Link to="/employees">Сотрудники</Link>
|
||
<span>/</span>
|
||
<span>{organisation?.name ?? 'Организация'}</span>
|
||
</div>
|
||
|
||
{organisationLoading && !organisation && (
|
||
<div className="organisation-page__empty">Загрузка организации...</div>
|
||
)}
|
||
|
||
{organisationError && (
|
||
<div className="organisation-page__empty organisation-page__empty--error">
|
||
Не удалось загрузить организацию
|
||
</div>
|
||
)}
|
||
|
||
{!organisationLoading && !organisationError && !organisation && (
|
||
<div className="organisation-page__empty">
|
||
<h2>Организация не найдена</h2>
|
||
<Link to="/employees">Вернуться к списку</Link>
|
||
</div>
|
||
)}
|
||
|
||
{organisation && (
|
||
<>
|
||
<div className="organisation-page__top">
|
||
<div>
|
||
<div className="organisation-card">
|
||
<div className="organisation-card__main">
|
||
<div className="organisation-card__avatar" aria-hidden="true">
|
||
{organisationInitials}
|
||
</div>
|
||
|
||
<div className="organisation-card__info">
|
||
<h2>
|
||
{organisation.name}
|
||
<span>ID: {organisation.id}</span>
|
||
</h2>
|
||
<p>Создана {formatCreationDate(organisation.creationDate)}</p>
|
||
<div className="organisation-card__actions">
|
||
<button
|
||
className="organisation-action-btn organisation-action-btn--edit"
|
||
type="button"
|
||
onClick={() => setEditingOrganisation(organisation)}
|
||
>
|
||
<Pencil size={16} />
|
||
Редактировать
|
||
</button>
|
||
|
||
<button
|
||
className="organisation-action-btn organisation-action-btn--delete"
|
||
type="button"
|
||
onClick={() => setDeletingOrganisation(organisation)}
|
||
>
|
||
<Trash2 size={16} />
|
||
Удалить
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<OrganisationPolicyCard organisation={organisation} />
|
||
</div>
|
||
<div className="organisation-employees-card">
|
||
|
||
<div className="organisation-employees-table-card">
|
||
{employeesStatus === 'loading' && employees.length === 0 && (
|
||
<div className="organisation-page__state">
|
||
Загрузка сотрудников...
|
||
</div>
|
||
)}
|
||
|
||
{employeesStatus === 'error' && (
|
||
<div className="organisation-page__state organisation-page__state--error">
|
||
Не удалось загрузить сотрудников
|
||
</div>
|
||
)}
|
||
|
||
{employeesStatus === 'success' && employees.length === 0 && (
|
||
<div className="organisation-page__state">
|
||
В этой организации пока нет сотрудников
|
||
</div>
|
||
)}
|
||
|
||
{employees.length > 0 && (
|
||
<table className="organisation-employees-table">
|
||
<thead>
|
||
<tr>
|
||
<th>ID</th>
|
||
<th>ФИО</th>
|
||
<th>Организация</th>
|
||
<th>Роль</th>
|
||
</tr>
|
||
</thead>
|
||
|
||
<tbody>
|
||
{employees.map((employee) => (
|
||
<tr key={employee.id}>
|
||
<td>{employee.id}</td>
|
||
|
||
<td>
|
||
<div className="organisation-employee-person">
|
||
<span>
|
||
{getEmployeeFullName(employee) || 'ФИО не указано'}
|
||
</span>
|
||
</div>
|
||
</td>
|
||
|
||
<td>
|
||
<span className="organisation-employee-org">
|
||
{employee.org?.name || organisation.name}
|
||
</span>
|
||
</td>
|
||
|
||
<td>
|
||
<span
|
||
className={
|
||
employee.role === 'Admin'
|
||
? 'organisation-employee-role organisation-employee-role--admin'
|
||
: 'organisation-employee-role'
|
||
}
|
||
>
|
||
{getEmployeeRoleLabel(employee.role)}
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<AddOrganisationModal
|
||
open={Boolean(editingOrganisation)}
|
||
mode="edit"
|
||
organisation={editingOrganisation}
|
||
onOpenChange={(open) => {
|
||
if (!open) {
|
||
setEditingOrganisation(null)
|
||
}
|
||
}}
|
||
/>
|
||
|
||
<ConfirmDangerDialog
|
||
open={Boolean(deletingOrganisation)}
|
||
title="Удалить организацию?"
|
||
description={
|
||
deletingOrganisation
|
||
? `Организация «${deletingOrganisation.name}» будет удалена из системы. Это действие нельзя будет отменить.`
|
||
: ''
|
||
}
|
||
confirmText="Удалить"
|
||
onOpenChange={(open) => {
|
||
if (!open) {
|
||
setDeletingOrganisation(null)
|
||
}
|
||
}}
|
||
onConfirm={handleConfirmDeleteOrganisation}
|
||
/>
|
||
</>
|
||
)}
|
||
</section>
|
||
)
|
||
} |