Initial commit MKD fixes
This commit is contained in:
248
components/admin/PositionsSection.tsx
Executable file
248
components/admin/PositionsSection.tsx
Executable file
@@ -0,0 +1,248 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user