MDM/mdm-front/src/pages/OrganisationPage/OrganisationPage.tsx

337 lines
11 KiB
TypeScript
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.

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>
)
}