- PosPage: rooms list becomes horizontal scroll row on mobile instead of w-56 sidebar - AvailabilityPage: edit panel stacks below grid on mobile (was fixed w-80 side panel) - UsersPage roles tab: sidebar stacks above permissions on mobile - All page containers: p-6 → p-4 md:p-6 (TariffsPage, DiscountsPage, MaintenancePage, ReportsPage, DynamicPricingPage, GuestsPage, LoyaltyPage) - Stats grids: lg:grid-cols-4 → md:grid-cols-4 (GuestsPage, TariffsPage, DiscountsPage, MaintenancePage, ReportsPage, LoyaltyPage) - Modal form grids: grid-cols-3 → grid-cols-1 sm:grid-cols-3, grid-cols-5 → grid-cols-3 sm:grid-cols-5, grid-cols-4 → grid-cols-2 sm:grid-cols-4 - PosPage modals: fixed w-80/w-96 → responsive w-[calc(100vw-2rem)] sm:w-80/96 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
552 lines
24 KiB
TypeScript
552 lines
24 KiB
TypeScript
import { useState } from 'react'
|
||
import {
|
||
Plus, Edit2, Trash2, Wrench, AlertTriangle, CheckCircle2,
|
||
Clock, BedDouble, Dumbbell, Waves, UtensilsCrossed, Wifi,
|
||
Car, Sparkles, Package, X, CalendarClock,
|
||
} from 'lucide-react'
|
||
import { Modal } from '../components/ui/Modal'
|
||
import { cn } from '../lib/utils'
|
||
import { format } from 'date-fns'
|
||
import { ru } from 'date-fns/locale'
|
||
import { RENTAL_OBJECTS } from '../data/rentalData'
|
||
|
||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||
|
||
type MaintenanceTarget = 'room' | 'service' | 'rental'
|
||
type MaintenanceStatus = 'scheduled' | 'in_progress' | 'done' | 'cancelled'
|
||
|
||
interface MaintenanceRecord {
|
||
id: string
|
||
type: MaintenanceTarget
|
||
targetId: string // roomId or serviceId
|
||
targetLabel: string // "Номер 205" or "Бассейн"
|
||
startDate: string
|
||
endDate: string
|
||
startTime?: string // HH:MM
|
||
endTime?: string
|
||
reason: string
|
||
notes: string
|
||
status: MaintenanceStatus
|
||
assignedTo?: string
|
||
}
|
||
|
||
interface ServiceDef {
|
||
id: string
|
||
label: string
|
||
icon: React.ElementType
|
||
}
|
||
|
||
// ─── Mock Data ────────────────────────────────────────────────────────────────
|
||
|
||
const MOCK_ROOMS = [
|
||
{ id: 'r101', label: 'Номер 101 — Стандарт' },
|
||
{ id: 'r102', label: 'Номер 102 — Стандарт' },
|
||
{ id: 'r205', label: 'Номер 205 — Делюкс' },
|
||
{ id: 'r301', label: 'Номер 301 — Сьют' },
|
||
{ id: 'r401', label: 'Номер 401 — Люкс' },
|
||
]
|
||
|
||
const SERVICES: ServiceDef[] = [
|
||
{ id: 'pool', label: 'Бассейн', icon: Waves },
|
||
{ id: 'gym', label: 'Тренажёрный зал', icon: Dumbbell },
|
||
{ id: 'restaurant', label: 'Ресторан', icon: UtensilsCrossed },
|
||
{ id: 'spa', label: 'СПА', icon: Sparkles },
|
||
{ id: 'wifi', label: 'Wi-Fi', icon: Wifi },
|
||
{ id: 'parking', label: 'Парковка', icon: Car },
|
||
{ id: 'laundry', label: 'Прачечная', icon: Package },
|
||
]
|
||
|
||
const MOCK_RECORDS: MaintenanceRecord[] = [
|
||
{
|
||
id: 'm-1',
|
||
type: 'room',
|
||
targetId: 'r205',
|
||
targetLabel: 'Номер 205 — Делюкс',
|
||
startDate: '2026-03-16',
|
||
endDate: '2026-03-18',
|
||
reason: 'Замена сантехники',
|
||
notes: 'Полная замена смесителей, ремонт душевой кабины. Подрядчик: ООО ТехСервис',
|
||
status: 'scheduled',
|
||
assignedTo: 'Иванов А.А.',
|
||
},
|
||
{
|
||
id: 'm-2',
|
||
type: 'service',
|
||
targetId: 'pool',
|
||
targetLabel: 'Бассейн',
|
||
startDate: '2026-03-20',
|
||
endDate: '2026-03-22',
|
||
startTime: '08:00',
|
||
endTime: '20:00',
|
||
reason: 'Плановая чистка и обработка воды',
|
||
notes: 'Проводится раз в квартал. Замена фильтров.',
|
||
status: 'scheduled',
|
||
assignedTo: 'Технический отдел',
|
||
},
|
||
{
|
||
id: 'm-3',
|
||
type: 'room',
|
||
targetId: 'r101',
|
||
targetLabel: 'Номер 101 — Стандарт',
|
||
startDate: '2026-03-10',
|
||
endDate: '2026-03-14',
|
||
reason: 'Косметический ремонт: покраска стен',
|
||
notes: 'Перекраска стен, замена штор',
|
||
status: 'done',
|
||
assignedTo: 'Строительная бригада',
|
||
},
|
||
{
|
||
id: 'm-4',
|
||
type: 'service',
|
||
targetId: 'restaurant',
|
||
targetLabel: 'Ресторан',
|
||
startDate: '2026-03-15',
|
||
endDate: '2026-03-15',
|
||
startTime: '14:00',
|
||
endTime: '18:00',
|
||
reason: 'Санитарная обработка',
|
||
notes: 'Плановая дезинфекция кухни',
|
||
status: 'in_progress',
|
||
},
|
||
{
|
||
id: 'm-5',
|
||
type: 'room',
|
||
targetId: 'r401',
|
||
targetLabel: 'Номер 401 — Люкс',
|
||
startDate: '2026-03-25',
|
||
endDate: '2026-04-05',
|
||
reason: 'Капитальный ремонт',
|
||
notes: 'Полный ремонт: полы, потолки, мебель',
|
||
status: 'scheduled',
|
||
},
|
||
{
|
||
id: 'm-6',
|
||
type: 'rental',
|
||
targetId: 'sauna',
|
||
targetLabel: '🛁 Баня',
|
||
startDate: '2026-03-16',
|
||
endDate: '2026-03-16',
|
||
startTime: '14:00',
|
||
endTime: '16:00',
|
||
reason: 'Подготовка между гостями — проветривание и уборка',
|
||
notes: 'Плановый тех. перерыв 2 часа после каждого заезда. Температурное охлаждение + мытьё полков.',
|
||
status: 'scheduled',
|
||
assignedTo: 'Горничная',
|
||
},
|
||
{
|
||
id: 'm-7',
|
||
type: 'rental',
|
||
targetId: 'court',
|
||
targetLabel: '🎾 Теннисный корт',
|
||
startDate: '2026-03-18',
|
||
endDate: '2026-03-19',
|
||
reason: 'Замена покрытия',
|
||
notes: 'Полная замена резинового покрытия. Корт закрыт на 2 дня.',
|
||
status: 'scheduled',
|
||
assignedTo: 'Хозяйственная служба',
|
||
},
|
||
]
|
||
|
||
// ─── Status helpers ───────────────────────────────────────────────────────────
|
||
|
||
const STATUS_META: Record<MaintenanceStatus, { label: string; cls: string; icon: React.ElementType }> = {
|
||
scheduled: { label: 'Запланировано', cls: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400', icon: Clock },
|
||
in_progress: { label: 'Выполняется', cls: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400', icon: Wrench },
|
||
done: { label: 'Завершено', cls: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400', icon: CheckCircle2 },
|
||
cancelled: { label: 'Отменено', cls: 'bg-slate-100 text-slate-500 dark:bg-slate-700 dark:text-slate-400', icon: X },
|
||
}
|
||
|
||
function formatDateRange(from: string, to: string, startTime?: string, endTime?: string) {
|
||
const f = new Date(from), t = new Date(to)
|
||
const sameDay = from === to
|
||
const fmt = (d: Date) => format(d, 'd MMM', { locale: ru })
|
||
if (sameDay) {
|
||
return `${fmt(f)}${startTime ? ` ${startTime}–${endTime ?? '...'}` : ''}`
|
||
}
|
||
return `${fmt(f)} — ${fmt(t)}`
|
||
}
|
||
|
||
function daysCount(from: string, to: string) {
|
||
const n = Math.round((new Date(to).getTime() - new Date(from).getTime()) / 86400000) + 1
|
||
return `${n} ${n === 1 ? 'день' : n < 5 ? 'дня' : 'дней'}`
|
||
}
|
||
|
||
// ─── Modal ────────────────────────────────────────────────────────────────────
|
||
|
||
const EMPTY: Omit<MaintenanceRecord, 'id'> = {
|
||
type: 'room',
|
||
targetId: MOCK_ROOMS[0].id,
|
||
targetLabel: MOCK_ROOMS[0].label,
|
||
startDate: format(new Date(), 'yyyy-MM-dd'),
|
||
endDate: format(new Date(), 'yyyy-MM-dd'),
|
||
reason: '',
|
||
notes: '',
|
||
status: 'scheduled',
|
||
}
|
||
|
||
function MaintenanceModal({
|
||
record, onSave, onClose,
|
||
}: {
|
||
record?: MaintenanceRecord
|
||
onSave: (r: Omit<MaintenanceRecord, 'id'>) => void
|
||
onClose: () => void
|
||
}) {
|
||
const [form, setForm] = useState<Omit<MaintenanceRecord, 'id'>>(
|
||
record ? { ...record } : { ...EMPTY }
|
||
)
|
||
const set = <K extends keyof typeof form>(k: K, v: typeof form[K]) =>
|
||
setForm(prev => ({ ...prev, [k]: v }))
|
||
|
||
const targets = form.type === 'room'
|
||
? MOCK_ROOMS
|
||
: form.type === 'rental'
|
||
? RENTAL_OBJECTS.map(o => ({ id: o.id, label: `${o.icon} ${o.name}` }))
|
||
: SERVICES.map(s => ({ id: s.id, label: s.label }))
|
||
|
||
const handleTargetChange = (id: string) => {
|
||
const t = targets.find(t => t.id === id)
|
||
if (t) { set('targetId', id); set('targetLabel', t.label) }
|
||
}
|
||
|
||
return (
|
||
<Modal
|
||
open
|
||
onClose={onClose}
|
||
title={record ? 'Редактировать перерыв' : 'Добавить тех. перерыв'}
|
||
size="lg"
|
||
footer={
|
||
<>
|
||
<button onClick={onClose} className="btn-secondary">Отмена</button>
|
||
<button
|
||
onClick={() => { if (form.reason) onSave(form) }}
|
||
className="btn-primary"
|
||
disabled={!form.reason}
|
||
>
|
||
{record ? 'Сохранить' : 'Добавить'}
|
||
</button>
|
||
</>
|
||
}
|
||
>
|
||
<div className="space-y-4">
|
||
{/* Type selector */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Тип</label>
|
||
<div className="grid grid-cols-3 sm:grid-cols-3 gap-2">
|
||
{([
|
||
{ id: 'room' as const, label: 'Номер', icon: BedDouble },
|
||
{ id: 'service' as const, label: 'Услуга', icon: Wrench },
|
||
{ id: 'rental' as const, label: 'Аренда', icon: CalendarClock },
|
||
]).map(t => (
|
||
<button
|
||
key={t.id}
|
||
type="button"
|
||
onClick={() => {
|
||
set('type', t.id)
|
||
let def: { id: string; label: string }
|
||
if (t.id === 'room') def = MOCK_ROOMS[0]
|
||
else if (t.id === 'rental') def = { id: RENTAL_OBJECTS[0].id, label: `${RENTAL_OBJECTS[0].icon} ${RENTAL_OBJECTS[0].name}` }
|
||
else def = { id: SERVICES[0].id, label: SERVICES[0].label }
|
||
set('targetId', def.id); set('targetLabel', def.label)
|
||
}}
|
||
className={cn(
|
||
'flex items-center gap-2 justify-center py-2.5 rounded-xl border text-sm font-medium transition-colors',
|
||
form.type === t.id
|
||
? 'bg-brand-600 border-brand-600 text-white'
|
||
: 'bg-white dark:bg-slate-700 border-slate-200 dark:border-slate-600 text-slate-600 dark:text-slate-300 hover:border-brand-400',
|
||
)}
|
||
>
|
||
<t.icon size={16} /> {t.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Target */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
|
||
{form.type === 'room' ? 'Номер' : 'Услуга / объект'}
|
||
</label>
|
||
<select className="input" value={form.targetId} onChange={e => handleTargetChange(e.target.value)}>
|
||
{targets.map(t => <option key={t.id} value={t.id}>{t.label}</option>)}
|
||
</select>
|
||
</div>
|
||
|
||
{/* Dates */}
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Начало</label>
|
||
<input type="date" className="input" value={form.startDate}
|
||
onChange={e => set('startDate', e.target.value)} />
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Конец</label>
|
||
<input type="date" className="input" value={form.endDate}
|
||
onChange={e => set('endDate', e.target.value)} />
|
||
</div>
|
||
</div>
|
||
|
||
{/* Time (for services and rental) */}
|
||
{(form.type === 'service' || form.type === 'rental') && (
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Начало (время)</label>
|
||
<input type="time" className="input" value={form.startTime ?? ''}
|
||
onChange={e => set('startTime', e.target.value)} />
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Конец (время)</label>
|
||
<input type="time" className="input" value={form.endTime ?? ''}
|
||
onChange={e => set('endTime', e.target.value)} />
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Reason */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Причина *</label>
|
||
<input type="text" className="input" placeholder="Плановый ремонт, санитарная обработка..."
|
||
value={form.reason} onChange={e => set('reason', e.target.value)} />
|
||
</div>
|
||
|
||
{/* Notes */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Примечания</label>
|
||
<textarea className="input resize-none" rows={2}
|
||
placeholder="Подрядчик, детали работ, контактное лицо..."
|
||
value={form.notes} onChange={e => set('notes', e.target.value)} />
|
||
</div>
|
||
|
||
{/* Assigned + Status */}
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Ответственный</label>
|
||
<input type="text" className="input" placeholder="ФИО или отдел"
|
||
value={form.assignedTo ?? ''} onChange={e => set('assignedTo', e.target.value)} />
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Статус</label>
|
||
<select className="input" value={form.status}
|
||
onChange={e => set('status', e.target.value as MaintenanceStatus)}>
|
||
{(Object.entries(STATUS_META) as [MaintenanceStatus, { label: string }][]).map(([k, v]) => (
|
||
<option key={k} value={k}>{v.label}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
)
|
||
}
|
||
|
||
// ─── Main Page ────────────────────────────────────────────────────────────────
|
||
|
||
export function MaintenancePage() {
|
||
const [records, setRecords] = useState<MaintenanceRecord[]>(MOCK_RECORDS)
|
||
const [modal, setModal] = useState<'create' | MaintenanceRecord | null>(null)
|
||
const [delTarget, setDel] = useState<MaintenanceRecord | null>(null)
|
||
const [activeTab, setActiveTab] = useState<'all' | 'room' | 'service' | 'rental'>('all')
|
||
|
||
const save = (data: Omit<MaintenanceRecord, 'id'>) => {
|
||
if (typeof modal === 'object' && modal !== null) {
|
||
setRecords(prev => prev.map(r => r.id === modal.id ? { ...data, id: modal.id } : r))
|
||
} else {
|
||
setRecords(prev => [...prev, { ...data, id: `m-${Date.now()}` }])
|
||
}
|
||
setModal(null)
|
||
}
|
||
|
||
const updateStatus = (id: string, status: MaintenanceStatus) =>
|
||
setRecords(prev => prev.map(r => r.id === id ? { ...r, status } : r))
|
||
|
||
const filtered = records.filter(r => activeTab === 'all' || r.type === activeTab)
|
||
const active = records.filter(r => r.status !== 'done' && r.status !== 'cancelled')
|
||
|
||
return (
|
||
<div className="p-4 md:p-6 space-y-6">
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">Технические перерывы</h1>
|
||
<p className="text-sm text-slate-500 dark:text-slate-400 mt-0.5">
|
||
Плановые и внеплановые перерывы в обслуживании номеров и услуг
|
||
</p>
|
||
</div>
|
||
<button onClick={() => setModal('create')} className="btn-primary flex items-center gap-2">
|
||
<Plus size={16} />
|
||
Добавить перерыв
|
||
</button>
|
||
</div>
|
||
|
||
{/* Stats */}
|
||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||
{[
|
||
{ label: 'Всего записей', value: records.length, cls: '' },
|
||
{ label: 'Активных', value: active.length, cls: 'text-amber-600 dark:text-amber-400' },
|
||
{ label: 'Номеров на ремонте', value: records.filter(r => r.type === 'room' && r.status !== 'done' && r.status !== 'cancelled').length, cls: 'text-red-600 dark:text-red-400' },
|
||
{ label: 'Услуг / аренды', value: records.filter(r => r.type !== 'room' && r.status !== 'done' && r.status !== 'cancelled').length, cls: 'text-blue-600 dark:text-blue-400' },
|
||
].map(s => (
|
||
<div key={s.label} className="card p-4 text-center">
|
||
<p className="text-xs text-slate-500 dark:text-slate-400">{s.label}</p>
|
||
<p className={cn('text-3xl font-bold mt-1 text-slate-900 dark:text-slate-100', s.cls)}>{s.value}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* Warning about current issues */}
|
||
{active.length > 0 && (
|
||
<div className="flex items-start gap-3 p-4 rounded-xl bg-amber-50 dark:bg-amber-900/10 border border-amber-200 dark:border-amber-800">
|
||
<AlertTriangle size={16} className="text-amber-600 dark:text-amber-400 mt-0.5 shrink-0" />
|
||
<div>
|
||
<p className="text-sm font-medium text-amber-700 dark:text-amber-400">
|
||
{active.length} активных перерыва
|
||
</p>
|
||
<p className="text-xs text-amber-600 dark:text-amber-500 mt-0.5">
|
||
Недоступные объекты автоматически блокируются в календаре бронирований и на странице доступности.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Filter tabs */}
|
||
<div className="card p-1 inline-flex gap-1">
|
||
{([
|
||
{ id: 'all' as const, label: `Все (${records.length})` },
|
||
{ id: 'room' as const, label: `Номера (${records.filter(r => r.type === 'room').length})` },
|
||
{ id: 'service' as const, label: `Услуги (${records.filter(r => r.type === 'service').length})` },
|
||
{ id: 'rental' as const, label: `Аренда (${records.filter(r => r.type === 'rental').length})` },
|
||
]).map(t => (
|
||
<button
|
||
key={t.id}
|
||
onClick={() => setActiveTab(t.id)}
|
||
className={cn(
|
||
'px-4 py-2 rounded-xl text-sm font-medium transition-colors',
|
||
activeTab === t.id
|
||
? 'bg-brand-600 text-white'
|
||
: 'text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700',
|
||
)}
|
||
>
|
||
{t.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* Records list */}
|
||
<div className="space-y-3">
|
||
{filtered.length === 0 && (
|
||
<div className="card p-10 text-center text-slate-400">Записей нет</div>
|
||
)}
|
||
{filtered.map(record => {
|
||
const smeta = STATUS_META[record.status]
|
||
const svc = record.type === 'service' ? SERVICES.find(s => s.id === record.targetId) : null
|
||
|
||
return (
|
||
<div
|
||
key={record.id}
|
||
className={cn(
|
||
'card p-4 flex items-start gap-4',
|
||
record.status === 'done' && 'opacity-60',
|
||
)}
|
||
>
|
||
{/* Icon */}
|
||
<div className={cn(
|
||
'w-10 h-10 rounded-xl flex items-center justify-center shrink-0',
|
||
record.type === 'room'
|
||
? 'bg-slate-100 dark:bg-slate-700'
|
||
: record.type === 'rental'
|
||
? 'bg-emerald-50 dark:bg-emerald-900/20'
|
||
: 'bg-blue-50 dark:bg-blue-900/20',
|
||
)}>
|
||
{record.type === 'room'
|
||
? <BedDouble size={18} className="text-slate-500 dark:text-slate-400" />
|
||
: record.type === 'rental'
|
||
? <CalendarClock size={18} className="text-emerald-500" />
|
||
: svc ? <svc.icon size={18} className="text-blue-500" /> : <Wrench size={18} className="text-blue-500" />
|
||
}
|
||
</div>
|
||
|
||
{/* Info */}
|
||
<div className="flex-1 min-w-0">
|
||
<div className="flex items-center gap-2 flex-wrap mb-0.5">
|
||
<span className="text-sm font-semibold text-slate-900 dark:text-slate-100">{record.targetLabel}</span>
|
||
<span className={cn('inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium', smeta.cls)}>
|
||
<smeta.icon size={10} />
|
||
{smeta.label}
|
||
</span>
|
||
</div>
|
||
<p className="text-sm text-slate-700 dark:text-slate-300 mb-0.5">{record.reason}</p>
|
||
{record.notes && (
|
||
<p className="text-xs text-slate-500 dark:text-slate-400">{record.notes}</p>
|
||
)}
|
||
<div className="flex items-center gap-3 mt-1">
|
||
<span className="text-xs text-slate-500 dark:text-slate-400">
|
||
📅 {formatDateRange(record.startDate, record.endDate, record.startTime, record.endTime)}
|
||
{' · '}{daysCount(record.startDate, record.endDate)}
|
||
</span>
|
||
{record.assignedTo && (
|
||
<span className="text-xs text-slate-500 dark:text-slate-400">
|
||
👤 {record.assignedTo}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Quick status change */}
|
||
{record.status !== 'done' && record.status !== 'cancelled' && (
|
||
<select
|
||
className="input text-xs w-36 shrink-0"
|
||
value={record.status}
|
||
onChange={e => updateStatus(record.id, e.target.value as MaintenanceStatus)}
|
||
>
|
||
{(Object.entries(STATUS_META) as [MaintenanceStatus, { label: string }][]).map(([k, v]) => (
|
||
<option key={k} value={k}>{v.label}</option>
|
||
))}
|
||
</select>
|
||
)}
|
||
|
||
{/* Actions */}
|
||
<div className="flex items-center gap-1 shrink-0">
|
||
<button
|
||
onClick={() => setModal(record)}
|
||
className="p-1.5 rounded-lg text-slate-400 hover:text-brand-600 hover:bg-brand-50 dark:hover:bg-brand-900/20 transition-colors"
|
||
>
|
||
<Edit2 size={15} />
|
||
</button>
|
||
<button
|
||
onClick={() => setDel(record)}
|
||
className="p-1.5 rounded-lg text-slate-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
|
||
>
|
||
<Trash2 size={15} />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
|
||
{modal && (
|
||
<MaintenanceModal
|
||
record={typeof modal === 'object' ? modal : undefined}
|
||
onSave={save}
|
||
onClose={() => setModal(null)}
|
||
/>
|
||
)}
|
||
|
||
{delTarget && (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
|
||
<div className="bg-white dark:bg-slate-800 rounded-2xl shadow-xl p-6 w-full max-w-sm">
|
||
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-2">Удалить запись?</h3>
|
||
<p className="text-sm text-slate-500 mb-4">«{delTarget.targetLabel} — {delTarget.reason}» будет удалена.</p>
|
||
<div className="flex justify-end gap-2">
|
||
<button onClick={() => setDel(null)} className="btn-secondary">Отмена</button>
|
||
<button
|
||
onClick={() => { setRecords(prev => prev.filter(r => r.id !== delTarget.id)); setDel(null) }}
|
||
className="px-4 py-2 rounded-xl bg-red-600 text-white text-sm font-medium hover:bg-red-700"
|
||
>Удалить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|