# СИСТЕМА УПРАВЛЕНИЯ СОТРУДНИКАМИ ## 🎯 ОБЗОР СИСТЕМЫ Система управления персоналом SFERA включает полный цикл HR-процессов: от найма до ведения табелей учета рабочего времени. Система предназначена для **фулфилмент-центров** и обеспечивает управление командой сотрудников. ## 📊 МОДЕЛИ ДАННЫХ ### Модель Employee (Сотрудник) ```typescript // Prisma модель Employee model Employee { id String @id @default(cuid()) firstName String // Имя lastName String // Фамилия middleName String? // Отчество (опционально) birthDate DateTime? // Дата рождения avatar String? // Аватар сотрудника // Паспортные данные passportPhoto String? // Фото паспорта passportSeries String? // Серия паспорта passportNumber String? // Номер паспорта passportIssued String? // Кем выдан passportDate DateTime? // Дата выдачи // Рабочая информация position String // Должность (обязательно) department String? // Отдел hireDate DateTime // Дата найма (обязательно) salary Float? // Зарплата status EmployeeStatus @default(ACTIVE) // Контактная информация phone String // Телефон (обязательно) email String? // Email telegram String? // Telegram whatsapp String? // WhatsApp address String? // Адрес проживания emergencyContact String? // Контакт для экстренных случаев emergencyPhone String? // Телефон экстренного контакта // Связи organizationId String organization Organization @relation(fields: [organizationId], references: [id]) scheduleRecords EmployeeSchedule[] // Записи табеля supplyOrders SupplyOrder[] @relation("SupplyOrderResponsible") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } ``` ### Модель EmployeeSchedule (Табель) ```typescript // Система учета рабочего времени model EmployeeSchedule { id String @id @default(cuid()) date DateTime // Дата (уникальная для каждого сотрудника) status ScheduleStatus // Статус дня hoursWorked Float? // Отработанные часы overtimeHours Float? // Сверхурочные часы notes String? // Заметки к дню employeeId String // ID сотрудника employee Employee @relation(fields: [employeeId], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Уникальная связка: один сотрудник = одна запись на дату @@unique([employeeId, date]) } ``` ### Енумы статусов ```typescript // Статусы сотрудника enum EmployeeStatus { ACTIVE // Активен (работает) VACATION // В отпуске SICK // На больничном FIRED // Уволен } // Статусы дня в табеле enum ScheduleStatus { WORK // Рабочий день WEEKEND // Выходной VACATION // Отпуск SICK // Больничный ABSENT // Прогул/отсутствие } ``` ## 🏗️ АРХИТЕКТУРА КОМПОНЕНТОВ ### Главный дашборд ```typescript // EmployeesDashboard - центральная точка управления (50+ строк кода) const EmployeesDashboard = () => { const { data: employees, loading } = useQuery(GET_MY_EMPLOYEES) const [createEmployee] = useMutation(CREATE_EMPLOYEE) const [updateEmployee] = useMutation(UPDATE_EMPLOYEE) const [deleteEmployee] = useMutation(DELETE_EMPLOYEE) // Табы навигации const tabs = [ { id: 'list', label: 'Список сотрудников', icon: Users }, { id: 'calendar', label: 'Календарь', icon: Calendar }, { id: 'reports', label: 'Отчеты', icon: FileText } ] return (
{tabs.map(tab => ( {tab.label} ))}
) } ``` ### Модульная структура компонентов ``` src/components/employees/ ├── employees-dashboard.tsx # 🎯 Главный оркестратор ├── employees-list.tsx # 📋 Список сотрудников ├── employee-row.tsx # 📄 Строка сотрудника в списке ├── employee-card.tsx # 🃏 Карточка сотрудника ├── employee-search.tsx # 🔍 Поиск и фильтрация ├── employee-stats.tsx # 📊 Статистика по сотрудникам │ ├── employee-form.tsx # ➕ Форма создания/редактирования ├── employee-inline-form.tsx # ✏️ Быстрое редактирование ├── employee-compact-form.tsx # 📝 Компактная форма ├── employee-edit-inline-form.tsx # ✏️ Инлайн редактирование │ ├── employee-calendar.tsx # 📅 Календарь сотрудника ├── employee-schedule.tsx # ⏰ Расписание работы ├── day-edit-modal.tsx # 🪟 Модальное окно редактирования дня ├── bulk-edit-modal.tsx # 🪟 Массовое редактирование ├── month-navigation.tsx # 🗓️ Навигация по месяцам │ ├── employee-reports.tsx # 📈 Отчеты по сотрудникам ├── employee-legend.tsx # 🏷️ Легенда статусов ├── employee-header.tsx # 📋 Заголовок секции ├── employee-empty-state.tsx # 🚫 Пустое состояние └── employee-item.tsx # 📦 Элемент сотрудника ``` ## 📅 СИСТЕМА ТАБЕЛЬНОГО УЧЕТА ### Календарь сотрудника ```typescript // EmployeeCalendar - управление табелем рабочего времени const EmployeeCalendar = ({ employeeId, employeeSchedules, currentYear, currentMonth, onDayUpdate, employeeName }: EmployeeCalendarProps) => { const [selectedDate, setSelectedDate] = useState(null) const [bulkEditMode, setBulkEditMode] = useState(false) // Обработчик сохранения дня const handleDaySave = (data: { status: string hoursWorked?: number overtimeHours?: number notes?: string }) => { if (!selectedDate) return onDayUpdate(employeeId, selectedDate, data) setSelectedDate(null) } // Генерация календарной сетки const generateCalendarDays = () => { const daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate() const firstDay = new Date(currentYear, currentMonth, 1).getDay() const days = [] // Пустые ячейки в начале месяца for (let i = 0; i < firstDay; i++) { days.push(null) } // Дни месяца с данными табеля for (let day = 1; day <= daysInMonth; day++) { const scheduleRecord = getScheduleForDay(day) days.push({ date: day, status: scheduleRecord?.status || 'work', hoursWorked: scheduleRecord?.hoursWorked || 8, overtimeHours: scheduleRecord?.overtimeHours || 0 }) } return days } return (
{/* Заголовок календаря */}

{employeeName}

{/* Сетка календаря */}
{DAYS_OF_WEEK.map(day => (
{day}
))} {generateCalendarDays().map((dayData, index) => ( setSelectedDate(date)} className={getDayStatusClass(dayData?.status)} /> ))}
{/* Модальные окна */} {selectedDate && ( setSelectedDate(null)} /> )} {bulkEditMode && ( setBulkEditMode(false)} /> )}
) } ``` ### Статистика по табелю ```typescript // EmployeeStats - подсчет статистики рабочего времени const EmployeeStats = ({ currentYear, currentMonth }: EmployeeStatsProps) => { const daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate() // Расчет статистики на основе табеля const calculateMonthStats = () => { const stats = { workDays: 0, // Рабочие дни vacationDays: 0, // Отпускные дни sickDays: 0, // Больничные дни absentDays: 0, // Прогулы totalHours: 0, // Общие часы overtimeHours: 0 // Сверхурочные часы } for (let day = 1; day <= daysInMonth; day++) { const dayStatus = getDayStatus(day) const hoursWorked = getDayHours(day) const overtime = getOvertimeHours(day) switch (dayStatus) { case 'WORK': stats.workDays++ stats.totalHours += hoursWorked stats.overtimeHours += overtime break case 'VACATION': stats.vacationDays++ break case 'SICK': stats.sickDays++ break case 'ABSENT': stats.absentDays++ break } } return stats } const stats = calculateMonthStats() return (
) } ``` ## 🔧 GraphQL API ### Основные запросы ```graphql # Получение сотрудников организации query GetMyEmployees { myEmployees { id firstName lastName middleName position department status phone email avatar hireDate salary createdAt } } # Получение табеля сотрудника query GetEmployeeSchedule($employeeId: ID!, $month: Int!, $year: Int!) { employeeSchedule(employeeId: $employeeId, month: $month, year: $year) { id date status hoursWorked overtimeHours notes } } ``` ### Основные мутации ```graphql # Создание сотрудника mutation CreateEmployee($input: CreateEmployeeInput!) { createEmployee(input: $input) { success message employee { id firstName lastName position phone status } } } # Обновление табеля mutation UpdateEmployeeSchedule($input: UpdateScheduleInput!) { updateEmployeeSchedule(input: $input) } # Input типы input CreateEmployeeInput { firstName: String! lastName: String! middleName: String position: String! phone: String! email: String hireDate: DateTime! salary: Float birthDate: DateTime address: String } input UpdateScheduleInput { employeeId: ID! date: DateTime! status: ScheduleStatus! hoursWorked: Float overtimeHours: Float notes: String } ``` ## 📊 БИЗНЕС-ЛОГИКА И ПРАВИЛА ### Правила доступа ```typescript // Доступ к управлению сотрудниками - только для фулфилментов const validateEmployeeAccess = (user: User) => { if (user.organization.type !== 'FULFILLMENT') { throw new GraphQLError('Управление сотрудниками доступно только фулфилмент-центрам') } } // Изоляция данных - сотрудники видны только внутри организации const getMyEmployees = async (organizationId: string) => { return await prisma.employee.findMany({ where: { organizationId }, orderBy: { createdAt: 'desc' }, }) } ``` ### Автоматические вычисления ```typescript // Расчет полного имени const getEmployeeFullName = (employee: Employee) => { const parts = [employee.lastName, employee.firstName, employee.middleName] return parts.filter(Boolean).join(' ') } // Расчет стажа работы const calculateWorkExperience = (hireDate: Date) => { const now = new Date() const diffTime = Math.abs(now.getTime() - hireDate.getTime()) const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) const years = Math.floor(diffDays / 365) const months = Math.floor((diffDays % 365) / 30) return { years, months, totalDays: diffDays } } ``` ### Валидация табеля ```typescript // Бизнес-правила для табельного учета const validateScheduleEntry = (entry: ScheduleEntry) => { // Нельзя указать больше 24 часов в день if ((entry.hoursWorked || 0) + (entry.overtimeHours || 0) > 24) { throw new Error('Общее количество часов не может превышать 24 в день') } // Сверхурочные только при работе if (entry.status !== 'WORK' && entry.overtimeHours > 0) { throw new Error('Сверхурочные часы возможны только в рабочие дни') } // Больничный и отпуск исключают рабочие часы if (['SICK', 'VACATION'].includes(entry.status) && entry.hoursWorked > 0) { throw new Error('В отпуске и на больничном нельзя указывать рабочие часы') } } ``` ## 🔄 ИНТЕГРАЦИЯ С ПОСТАВКАМИ ### Ответственные за заказы ```typescript // Связь сотрудника с поставками (из SupplyOrder модели) model SupplyOrder { // ... другие поля responsibleEmployeeId String? responsibleEmployee Employee? @relation("SupplyOrderResponsible", fields: [responsibleEmployeeId], references: [id]) } // Назначение ответственного за поставку const assignEmployeeToSupplyOrder = async (supplyOrderId: string, employeeId: string) => { // Проверяем, что сотрудник активен const employee = await prisma.employee.findUnique({ where: { id: employeeId } }) if (employee.status !== 'ACTIVE') { throw new Error('Назначить можно только активного сотрудника') } return await prisma.supplyOrder.update({ where: { id: supplyOrderId }, data: { responsibleEmployeeId: employeeId } }) } ``` ## 📈 ОТЧЕТНОСТЬ ### Стандартные отчеты ```typescript // EmployeeReports - система отчетности const EmployeeReports = () => { const reportTypes = [ { title: 'Табель учета рабочего времени', description: 'Сводный табель по всем сотрудникам за месяц', generator: generateTimesheetReport }, { title: 'Отчет по отпускам', description: 'График отпусков и остатки отпускных дней', generator: generateVacationReport }, { title: 'Анализ производительности', description: 'Статистика по сверхурочным и прогулам', generator: generatePerformanceReport }, { title: 'Расчет зарплаты', description: 'Данные для расчета заработной платы', generator: generatePayrollReport } ] return (

Отчеты по сотрудникам

{reportTypes.map(report => (

{report.title}

{report.description}

))}
) } ``` ## 🔐 БЕЗОПАСНОСТЬ И ПРИВАТНОСТЬ ### Защита персональных данных ```typescript // Ограниченный доступ к паспортным данным const getEmployeePublicInfo = (employee: Employee) => { return { id: employee.id, fullName: getEmployeeFullName(employee), position: employee.position, department: employee.department, avatar: employee.avatar, status: employee.status, // Паспортные данные и зарплата скрыты } } // Логирование доступа к персональным данным const logPersonalDataAccess = async (userId: string, employeeId: string, action: string) => { console.log(`Personal data access: User ${userId} performed ${action} on employee ${employeeId}`) // Сохранение в audit log await prisma.auditLog.create({ data: { userId, entityType: 'EMPLOYEE', entityId: employeeId, action, timestamp: new Date(), }, }) } ``` ### Права доступа по ролям ```typescript // Разграничение прав внутри фулфилмента const checkEmployeePermissions = (user: User, operation: string) => { const permissions = { view_employees: ['ADMIN', 'HR_MANAGER', 'SUPERVISOR'], create_employee: ['ADMIN', 'HR_MANAGER'], edit_employee: ['ADMIN', 'HR_MANAGER'], delete_employee: ['ADMIN'], view_salary: ['ADMIN', 'HR_MANAGER'], manage_schedule: ['ADMIN', 'HR_MANAGER', 'SUPERVISOR'], } if (!permissions[operation]?.includes(user.role)) { throw new GraphQLError(`Недостаточно прав для операции: ${operation}`) } } ``` ## 🎨 UI/UX ОСОБЕННОСТИ ### Адаптивный дизайн - **Desktop**: Полная функциональность с табличным отображением - **Tablet**: Карточный режим просмотра сотрудников - **Mobile**: Компактные формы и вертикальная навигация ### Интерактивные элементы - **Drag & Drop**: Перенос сотрудников между отделами - **Inline editing**: Быстрое редактирование прямо в списке - **Bulk operations**: Массовые операции с несколькими сотрудниками - **Real-time updates**: Автообновление при изменениях табеля ### Цветовая индикация статусов ```css /* Статусы сотрудников */ .employee-active { @apply bg-green-100 text-green-800; } .employee-vacation { @apply bg-blue-100 text-blue-800; } .employee-sick { @apply bg-yellow-100 text-yellow-800; } .employee-fired { @apply bg-red-100 text-red-800; } /* Статусы дней в календаре */ .schedule-work { @apply bg-green-200; } .schedule-weekend { @apply bg-gray-200; } .schedule-vacation { @apply bg-blue-200; } .schedule-sick { @apply bg-yellow-200; } .schedule-absent { @apply bg-red-200; } ``` --- _Извлечено из анализа: 19 компонентов системы управления сотрудниками_ _Источники: src/components/employees/, prisma/schema.prisma, src/graphql/_ _Создано: 2025-08-21_