Files
mkd/components/admin/PositionsSection.tsx
2026-02-04 00:17:04 +05:00

249 lines
10 KiB
TypeScript
Executable File
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 React, { useCallback, useEffect, useState } from 'react';
import { backendApi } from '../../services/apiClient';
import { Position } from '../../types';
import { Briefcase, Loader2, Plus, Pencil, Trash2, X } from 'lucide-react';
export const PositionsSection: React.FC = () => {
const [positions, setPositions] = useState<Position[]>([]);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [isCreateOpen, setIsCreateOpen] = useState(false);
const [createName, setCreateName] = useState('');
const [createIsManagerial, setCreateIsManagerial] = useState(false);
const [editName, setEditName] = useState('');
const [editIsManagerial, setEditIsManagerial] = useState(false);
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null);
const load = useCallback(async () => {
try {
setLoading(true);
const list = await backendApi.getPositions();
setPositions(list);
} catch (err) {
console.error('Error loading positions:', err);
setPositions([]);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
load();
}, [load]);
const handleCreate = async (e: React.FormEvent) => {
e.preventDefault();
if (!createName.trim()) return;
try {
setSaving(true);
await backendApi.createPosition({ name: createName.trim(), isManagerial: createIsManagerial });
setIsCreateOpen(false);
setCreateName('');
setCreateIsManagerial(false);
await load();
} catch (err: any) {
console.error(err);
alert(err?.message || 'Ошибка создания должности');
} finally {
setSaving(false);
}
};
const startEdit = (p: Position) => {
setEditingId(p.id);
setEditName(p.name);
setEditIsManagerial(p.isManagerial ?? false);
};
const cancelEdit = () => {
setEditingId(null);
};
const handleUpdate = async (id: string) => {
if (!editName.trim()) return;
try {
setSaving(true);
await backendApi.updatePosition(id, { name: editName.trim(), isManagerial: editIsManagerial });
setEditingId(null);
await load();
} catch (err: any) {
console.error(err);
alert(err?.message || 'Ошибка сохранения');
} finally {
setSaving(false);
}
};
const handleDelete = async (id: string) => {
try {
setSaving(true);
await backendApi.deletePosition(id);
setDeleteConfirmId(null);
await load();
} catch (err: any) {
console.error(err);
alert(err?.message || 'Ошибка удаления');
} finally {
setSaving(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-primary-600" />
</div>
);
}
return (
<div>
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 rounded-xl bg-primary-50 flex items-center justify-center">
<Briefcase className="w-5 h-5 text-primary-600" />
</div>
<div>
<h3 className="text-lg font-bold text-slate-800">Справочник должностей</h3>
<p className="text-xs text-slate-500">
Должности для назначения сотрудникам. Отметьте «Руководящая должность» для должностей руководителей (мастер, начальник участка и т.д.)
</p>
</div>
</div>
<div className="flex justify-end mb-4">
<button
type="button"
onClick={() => setIsCreateOpen(true)}
className="flex items-center gap-2 px-4 py-2 rounded-xl bg-primary-600 text-white text-sm font-bold hover:bg-primary-700"
>
<Plus className="w-4 h-4" /> Добавить должность
</button>
</div>
{isCreateOpen && (
<form
onSubmit={handleCreate}
className="mb-6 p-4 bg-slate-50 rounded-2xl border border-slate-200 flex flex-wrap items-end gap-4"
>
<div className="flex-1 min-w-[200px]">
<label className="block text-[10px] font-black uppercase text-slate-500 mb-1">Название должности</label>
<input
type="text"
value={createName}
onChange={(e) => setCreateName(e.target.value)}
placeholder="Например: Мастер участка"
className="w-full p-2.5 border border-slate-200 rounded-xl text-sm"
required
/>
</div>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={createIsManagerial}
onChange={(e) => setCreateIsManagerial(e.target.checked)}
className="rounded border-slate-300"
/>
<span className="text-sm font-medium text-slate-700">Руководящая должность</span>
</label>
<div className="flex gap-2">
<button type="button" onClick={() => { setIsCreateOpen(false); setCreateName(''); setCreateIsManagerial(false); }} className="px-4 py-2.5 rounded-xl border border-slate-200 text-slate-600 text-sm font-bold">
Отмена
</button>
<button type="submit" disabled={saving} className="px-4 py-2.5 rounded-xl bg-primary-600 text-white text-sm font-bold disabled:opacity-50">
{saving ? 'Сохранение...' : 'Сохранить'}
</button>
</div>
</form>
)}
<div className="border border-slate-200 rounded-2xl overflow-hidden">
<table className="w-full text-left">
<thead className="bg-slate-50 border-b border-slate-200">
<tr>
<th className="p-3 text-[10px] font-black uppercase text-slate-500">Должность</th>
<th className="p-3 text-[10px] font-black uppercase text-slate-500 w-48">Руководящая должность</th>
<th className="p-3 text-[10px] font-black uppercase text-slate-500 w-28">Действия</th>
</tr>
</thead>
<tbody>
{positions.length === 0 ? (
<tr>
<td colSpan={3} className="p-6 text-center text-slate-500 text-sm">
Нет должностей. Добавьте первую.
</td>
</tr>
) : (
positions.map((p) => (
<tr key={p.id} className="border-b border-slate-100 last:border-0 hover:bg-slate-50/50">
<td className="p-3">
{editingId === p.id ? (
<input
type="text"
value={editName}
onChange={(e) => setEditName(e.target.value)}
className="w-full p-2 border border-slate-200 rounded-lg text-sm"
autoFocus
/>
) : (
<span className="font-medium text-slate-800">{p.name}</span>
)}
</td>
<td className="p-3">
{editingId === p.id ? (
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={editIsManagerial}
onChange={(e) => setEditIsManagerial(e.target.checked)}
className="rounded border-slate-300"
/>
<span className="text-sm text-slate-600">Руководящая</span>
</label>
) : (
<span className={p.isManagerial ? 'text-emerald-600 font-medium' : 'text-slate-400'}>
{p.isManagerial ? 'Да' : 'Нет'}
</span>
)}
</td>
<td className="p-3">
{editingId === p.id ? (
<div className="flex items-center gap-2">
<button type="button" onClick={() => handleUpdate(p.id)} disabled={saving} className="px-3 py-1.5 rounded-lg bg-primary-600 text-white text-xs font-bold hover:bg-primary-700 disabled:opacity-50">
Сохранить
</button>
<button type="button" onClick={cancelEdit} className="p-2 rounded-lg border border-slate-200 text-slate-500 hover:bg-slate-100" title="Отмена">
<X className="w-4 h-4" />
</button>
</div>
) : deleteConfirmId === p.id ? (
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500">Удалить?</span>
<button type="button" onClick={() => handleDelete(p.id)} disabled={saving} className="text-xs font-bold text-red-600 hover:underline">
Да
</button>
<button type="button" onClick={() => setDeleteConfirmId(null)} className="text-xs font-bold text-slate-500 hover:underline">
Нет
</button>
</div>
) : (
<div className="flex items-center gap-1">
<button type="button" onClick={() => startEdit(p)} className="p-2 rounded-lg text-slate-400 hover:bg-slate-100 hover:text-primary-600" title="Редактировать">
<Pencil className="w-4 h-4" />
</button>
<button type="button" onClick={() => setDeleteConfirmId(p.id)} className="p-2 rounded-lg text-slate-400 hover:bg-red-50 hover:text-red-500" title="Удалить">
<Trash2 className="w-4 h-4" />
</button>
</div>
)}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
);
};