feat: emergency room closure for urgent maintenance tasks

- Backend housekeeping POST: when priority=urgent + category=maintenance + room_id
  → auto sets room status=maintenance with maintenance_from/to dates
  → closes room for emergency_close_days (default 1 day)
  → broadcasts room_updated WS event
- housekeeping-settings: add emergency_close_days column (migration 030, default 1)
- TechnicalPage: rename "Срочно" → "Экстренно!", show "🔒 Номер закрыт" badge for urgent active tasks
- HousekeepingPage plans tab: new "Экстренное закрытие номера" setting with day picker (1/2/3/5/7/14 days)
- api.ts HkSettings: add emergency_close_days field

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-24 11:51:42 +03:00
parent c83a9ca714
commit 0feec5c88f
6 changed files with 67 additions and 7 deletions

View File

@@ -114,6 +114,7 @@ export function HousekeepingPage() {
checkoutPriority: s.checkout_priority as 'high' | 'medium',
inspectionAfterClean: s.inspection_after_clean,
requireCompletionPhoto: s.require_completion_photo ?? false,
emergencyCloseDays: s.emergency_close_days ?? 1,
}))
}).catch(console.error)
.finally(() => setLoading(false))
@@ -136,6 +137,7 @@ export function HousekeepingPage() {
dailyStartFromDay: 1, onDemandEnabled: true, onDemandPriority: 'medium' as 'high' | 'medium' | 'low',
deepCleanEnabled: false, deepCleanEveryDays: 7,
inspectionAfterClean: true, autoAssign: false, requireCompletionPhoto: false,
emergencyCloseDays: 1,
})
const setP = <K extends keyof typeof plans>(k: K, v: typeof plans[K]) =>
@@ -150,6 +152,7 @@ export function HousekeepingPage() {
checkout_priority: plans.checkoutPriority,
inspection_after_clean: plans.inspectionAfterClean,
require_completion_photo: plans.requireCompletionPhoto,
emergency_close_days: plans.emergencyCloseDays,
}
await api.housekeeping.saveSettings(slug, settings)
setPlanSaved(true)
@@ -524,6 +527,34 @@ export function HousekeepingPage() {
</div>
</div>
<div className="card p-4 space-y-3">
<div>
<p className="font-semibold text-slate-900 dark:text-slate-100">Экстренное закрытие номера</p>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
При отчёте о критической поломке (статус «Экстренно!») номер автоматически закрывается для бронирования
</p>
</div>
<div className="flex items-center gap-3 border-t border-slate-100 dark:border-slate-700 pt-3">
<label className="text-sm text-slate-700 dark:text-slate-300 shrink-0">Закрывать на</label>
<div className="flex gap-1.5 flex-wrap">
{[1, 2, 3, 5, 7, 14].map(n => (
<button
key={n}
onClick={() => setP('emergencyCloseDays', n)}
className={cn(
'px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors',
plans.emergencyCloseDays === n
? 'bg-red-600 border-red-600 text-white'
: 'bg-white dark:bg-slate-700 border-slate-200 dark:border-slate-600 text-slate-600 dark:text-slate-300',
)}
>
{n === 1 ? '1 день' : n < 5 ? `${n} дня` : `${n} дней`}
</button>
))}
</div>
</div>
</div>
<div className="flex justify-end">
<button onClick={savePlans} disabled={planSaving} className="btn-primary">
{planSaving ? <Loader2 size={14} className="animate-spin" /> : <Save size={14} />}