Activate documents module, reorganize sidebar, add cleaning plans to housekeeping
Sidebar restructure: - Основное: Шахматка, Бронирования, Номера, Категории номеров, Доступность - Модули: Уборка + all active addon modules (including Documents) - Управление: План этажей, Каналы, Модули, Настройки - Разработчикам: API ModulesContext: documents module now active by default (version bump to 3) HousekeepingPage: added «Планы уборки» tab with: - Уборка при выезде: auto task creation, priority (high/normal), inspection toggle - Ежедневная уборка: interval (every 1/2/3/7 days), start from day N of stay - Уборка по запросу: guest-initiated via QR, priority selection - Генеральная уборка: deep clean every N days toggle - Проверка после уборки: inspection task after any cleaning - Автоназначение горничной: assign based on shift schedule Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -44,9 +44,9 @@ export function Sidebar({ open, onClose }: SidebarProps) {
|
||||
const isModuleActive = (id: string) => statuses[id] === 'active' || statuses[id] === 'trial'
|
||||
|
||||
// Core modules with dedicated nav positions (not shown in generic module list)
|
||||
const CORE_MODULE_IDS = ['housekeeping', 'channel-manager']
|
||||
const CORE_MODULE_IDS = ['channel-manager']
|
||||
|
||||
// Extra module items for the "Модули" sidebar section (excluding core ones)
|
||||
// Module items for the "Модули" sidebar section
|
||||
const activeModuleItems = MODULES_DATA.filter(
|
||||
m => m.sidebarItem &&
|
||||
!m.sidebarItem.showForHousekeeper &&
|
||||
@@ -112,14 +112,26 @@ export function Sidebar({ open, onClose }: SidebarProps) {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* ── Основное ── */}
|
||||
<p className="px-3 pt-2 pb-1 text-xs font-semibold text-slate-400 uppercase tracking-wider">
|
||||
Основное
|
||||
</p>
|
||||
<NavItem to="/calendar" icon={CalendarDays} label="Шахматка" onClick={onClose} />
|
||||
{!isHousekeeper && (
|
||||
<NavItem to="/bookings" icon={BookOpen} label="Бронирования" onClick={onClose} />
|
||||
<>
|
||||
<NavItem to="/bookings" icon={BookOpen} label="Бронирования" onClick={onClose} />
|
||||
<NavItem to="/rooms" icon={BedDouble} label="Номера" onClick={onClose} />
|
||||
<NavItem to="/room-categories" icon={LayoutGrid} label="Категории номеров" onClick={onClose} />
|
||||
<NavItem to="/availability" icon={CalendarRange} label="Доступность" onClick={onClose} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ── Модули ── */}
|
||||
{(isModuleActive('housekeeping') || (!isHousekeeper && activeModuleItems.length > 0)) && (
|
||||
<p className="px-3 pt-3 pb-1 text-xs font-semibold text-slate-400 uppercase tracking-wider">
|
||||
Модули
|
||||
</p>
|
||||
)}
|
||||
{/* Housekeeping — controlled by module status, visible to all roles */}
|
||||
{isModuleActive('housekeeping') && (
|
||||
<NavItem
|
||||
to="/housekeeping"
|
||||
@@ -128,16 +140,23 @@ export function Sidebar({ open, onClose }: SidebarProps) {
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
{!isHousekeeper && activeModuleItems.map(m => (
|
||||
<NavItem
|
||||
key={m.id}
|
||||
to={m.sidebarItem!.path}
|
||||
icon={m.sidebarItem!.icon}
|
||||
label={m.sidebarItem!.label}
|
||||
onClick={onClose}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* ── Управление ── */}
|
||||
{!isHousekeeper && (
|
||||
<>
|
||||
<p className="px-3 pt-3 pb-1 text-xs font-semibold text-slate-400 uppercase tracking-wider">
|
||||
Управление
|
||||
</p>
|
||||
<NavItem to="/rooms" icon={BedDouble} label="Номера" onClick={onClose} />
|
||||
<NavItem to="/room-categories" icon={LayoutGrid} label="Категории номеров" onClick={onClose} />
|
||||
<NavItem to="/floor-map" icon={Map} label="План этажей" onClick={onClose} />
|
||||
<NavItem to="/availability" icon={CalendarRange} label="Доступность" onClick={onClose} />
|
||||
<NavItem to="/floor-map" icon={Map} label="План этажей" onClick={onClose} />
|
||||
{isModuleActive('channel-manager') && (
|
||||
<NavItem
|
||||
to="/channels"
|
||||
@@ -149,24 +168,6 @@ export function Sidebar({ open, onClose }: SidebarProps) {
|
||||
<NavItem to="/modules" icon={Puzzle} label="Модули" onClick={onClose} />
|
||||
<NavItem to="/settings" icon={Settings} label="Настройки" onClick={onClose} />
|
||||
|
||||
{/* Active addon modules */}
|
||||
{activeModuleItems.length > 0 && (
|
||||
<>
|
||||
<p className="px-3 pt-3 pb-1 text-xs font-semibold text-slate-400 uppercase tracking-wider">
|
||||
Модули
|
||||
</p>
|
||||
{activeModuleItems.map(m => (
|
||||
<NavItem
|
||||
key={m.id}
|
||||
to={m.sidebarItem!.path}
|
||||
icon={m.sidebarItem!.icon}
|
||||
label={m.sidebarItem!.label}
|
||||
onClick={onClose}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
<p className="px-3 pt-3 pb-1 text-xs font-semibold text-slate-400 uppercase tracking-wider">
|
||||
Разработчикам
|
||||
</p>
|
||||
|
||||
@@ -16,6 +16,7 @@ const DEFAULT_STATUSES: Record<string, ModuleStatus> = {
|
||||
'room-service': 'active',
|
||||
'rental': 'active',
|
||||
'migration': 'active',
|
||||
'documents': 'active',
|
||||
}
|
||||
|
||||
interface ModulesContextValue {
|
||||
@@ -26,7 +27,7 @@ interface ModulesContextValue {
|
||||
|
||||
const ModulesContext = createContext<ModulesContextValue | null>(null)
|
||||
|
||||
const MODULES_VERSION = '2'
|
||||
const MODULES_VERSION = '3'
|
||||
|
||||
export function ModulesProvider({ children }: { children: React.ReactNode }) {
|
||||
const [statuses, setStatuses] = useState<Record<string, ModuleStatus>>(() => {
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
import { useState } from 'react'
|
||||
import { CheckCircle2, Clock, AlertCircle, Sparkles, User } from 'lucide-react'
|
||||
import { CheckCircle2, Clock, Sparkles, User, ListChecks, Settings2, Save } from 'lucide-react'
|
||||
import { MOCK_HK_TASKS } from '../data/mockData'
|
||||
import type { HousekeepingTask } from '../types'
|
||||
import { cn } from '../lib/utils'
|
||||
import { Badge } from '../components/ui/Badge'
|
||||
|
||||
function Toggle({ on, onChange }: { on: boolean; onChange: () => void }) {
|
||||
return (
|
||||
<button
|
||||
onClick={onChange}
|
||||
className={cn('relative w-10 h-5.5 rounded-full transition-colors shrink-0 h-[22px]', on ? 'bg-brand-600' : 'bg-slate-200 dark:bg-slate-600')}
|
||||
>
|
||||
<div className={cn('absolute top-0.5 w-4.5 h-4.5 rounded-full bg-white shadow-sm transition-transform w-[18px] h-[18px]', on ? 'left-[20px]' : 'left-0.5')} />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
type Column = 'pending' | 'in_progress' | 'done'
|
||||
|
||||
const COLUMNS: { id: Column; label: string; icon: React.ElementType; color: string }[] = [
|
||||
@@ -35,6 +46,36 @@ const TYPE_LABELS = {
|
||||
|
||||
export function HousekeepingPage() {
|
||||
const [tasks, setTasks] = useState<HousekeepingTask[]>(MOCK_HK_TASKS)
|
||||
const [activeTab, setActiveTab] = useState<'tasks' | 'plans'>('tasks')
|
||||
const [planSaved, setPlanSaved] = useState(false)
|
||||
|
||||
// Cleaning plan settings
|
||||
const [plans, setPlans] = useState({
|
||||
checkoutAuto: true,
|
||||
checkoutPriority: 'high' as 'high' | 'normal',
|
||||
checkoutInspection: true,
|
||||
|
||||
dailyEnabled: true,
|
||||
dailyIntervalDays: 1,
|
||||
dailyStartFromDay: 1,
|
||||
|
||||
onDemandEnabled: true,
|
||||
onDemandPriority: 'normal' as 'high' | 'normal' | 'low',
|
||||
|
||||
deepCleanEnabled: false,
|
||||
deepCleanEveryDays: 7,
|
||||
|
||||
inspectionAfterClean: true,
|
||||
autoAssign: false,
|
||||
})
|
||||
|
||||
const setP = <K extends keyof typeof plans>(k: K, v: typeof plans[K]) =>
|
||||
setPlans(prev => ({ ...prev, [k]: v }))
|
||||
|
||||
const savePlans = () => {
|
||||
setPlanSaved(true)
|
||||
setTimeout(() => setPlanSaved(false), 2000)
|
||||
}
|
||||
|
||||
const updateStatus = (id: string, status: HousekeepingTask['status']) => {
|
||||
setTasks(prev => prev.map(t =>
|
||||
@@ -50,58 +91,272 @@ export function HousekeepingPage() {
|
||||
return (
|
||||
<div className="p-4 md:p-6 space-y-5">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-slate-900 dark:text-slate-100">Уборка и обслуживание</h1>
|
||||
<div className="flex items-center gap-3 mt-1.5">
|
||||
<div className="flex-1 bg-slate-200 dark:bg-slate-700 rounded-full h-2">
|
||||
<div
|
||||
className="h-2 rounded-full bg-emerald-500 transition-all duration-500"
|
||||
style={{ width: `${(done / total) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400 shrink-0">
|
||||
{done} / {total} завершено
|
||||
</span>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<h1 className="text-xl font-bold text-slate-900 dark:text-slate-100">Уборка и обслуживание</h1>
|
||||
{activeTab === 'tasks' && (
|
||||
<div className="flex items-center gap-3 mt-1.5">
|
||||
<div className="flex-1 bg-slate-200 dark:bg-slate-700 rounded-full h-2">
|
||||
<div
|
||||
className="h-2 rounded-full bg-emerald-500 transition-all duration-500"
|
||||
style={{ width: total ? `${(done / total) * 100}%` : '0%' }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400 shrink-0">
|
||||
{done} / {total} завершено
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Kanban columns */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{COLUMNS.map(col => {
|
||||
const colTasks = tasks.filter(t => t.status === col.id)
|
||||
const Icon = col.icon
|
||||
return (
|
||||
<div key={col.id} className="space-y-3">
|
||||
{/* Column header */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon size={16} className={col.color} />
|
||||
<h3 className="font-semibold text-slate-700 dark:text-slate-300">
|
||||
{col.label}
|
||||
</h3>
|
||||
<span className="ml-auto text-xs font-bold px-2 py-0.5 rounded-full bg-slate-200 dark:bg-slate-700 text-slate-600 dark:text-slate-400">
|
||||
{colTasks.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Cards */}
|
||||
<div className="space-y-2.5">
|
||||
{colTasks.map(task => (
|
||||
<TaskCard
|
||||
key={task.id}
|
||||
task={task}
|
||||
onStatusChange={updateStatus}
|
||||
/>
|
||||
))}
|
||||
{colTasks.length === 0 && (
|
||||
<div className="rounded-xl border-2 border-dashed border-slate-200 dark:border-slate-700 py-8 text-center">
|
||||
<p className="text-sm text-slate-400 dark:text-slate-500">Пусто</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{/* Tabs */}
|
||||
<div className="flex border-b border-slate-200 dark:border-slate-700">
|
||||
{([
|
||||
{ id: 'tasks' as const, label: 'Задачи на сегодня', icon: ListChecks },
|
||||
{ id: 'plans' as const, label: 'Планы уборки', icon: Settings2 },
|
||||
]).map(t => (
|
||||
<button
|
||||
key={t.id}
|
||||
onClick={() => setActiveTab(t.id)}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 px-4 py-2.5 text-sm font-medium border-b-2 -mb-px transition-colors',
|
||||
activeTab === t.id
|
||||
? 'border-brand-600 text-brand-600 dark:text-brand-400 dark:border-brand-400'
|
||||
: 'border-transparent text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300',
|
||||
)}
|
||||
>
|
||||
<t.icon size={14} />
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* ── Tasks tab ── */}
|
||||
{activeTab === 'tasks' && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{COLUMNS.map(col => {
|
||||
const colTasks = tasks.filter(t => t.status === col.id)
|
||||
const Icon = col.icon
|
||||
return (
|
||||
<div key={col.id} className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon size={16} className={col.color} />
|
||||
<h3 className="font-semibold text-slate-700 dark:text-slate-300">{col.label}</h3>
|
||||
<span className="ml-auto text-xs font-bold px-2 py-0.5 rounded-full bg-slate-200 dark:bg-slate-700 text-slate-600 dark:text-slate-400">
|
||||
{colTasks.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2.5">
|
||||
{colTasks.map(task => (
|
||||
<TaskCard key={task.id} task={task} onStatusChange={updateStatus} />
|
||||
))}
|
||||
{colTasks.length === 0 && (
|
||||
<div className="rounded-xl border-2 border-dashed border-slate-200 dark:border-slate-700 py-8 text-center">
|
||||
<p className="text-sm text-slate-400 dark:text-slate-500">Пусто</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Plans tab ── */}
|
||||
{activeTab === 'plans' && (
|
||||
<div className="max-w-2xl space-y-5">
|
||||
|
||||
{/* Checkout cleaning */}
|
||||
<div className="card p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<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>
|
||||
<Toggle on={plans.checkoutAuto} onChange={() => setP('checkoutAuto', !plans.checkoutAuto)} />
|
||||
</div>
|
||||
{plans.checkoutAuto && (
|
||||
<div className="pl-1 space-y-3 border-t border-slate-100 dark:border-slate-700 pt-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1.5">Приоритет задачи</label>
|
||||
<div className="flex gap-2">
|
||||
{([['high', 'Срочно'], ['normal', 'Обычный']] as const).map(([v, l]) => (
|
||||
<button
|
||||
key={v}
|
||||
onClick={() => setP('checkoutPriority', v)}
|
||||
className={cn(
|
||||
'px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors',
|
||||
plans.checkoutPriority === v
|
||||
? v === 'high' ? 'bg-red-500 border-red-500 text-white' : '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',
|
||||
)}
|
||||
>
|
||||
{l}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-slate-700 dark:text-slate-300">Инспекция после уборки</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">Создавать задачу проверки для старшей горничной</p>
|
||||
</div>
|
||||
<Toggle on={plans.checkoutInspection} onChange={() => setP('checkoutInspection', !plans.checkoutInspection)} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Daily cleaning */}
|
||||
<div className="card p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<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>
|
||||
<Toggle on={plans.dailyEnabled} onChange={() => setP('dailyEnabled', !plans.dailyEnabled)} />
|
||||
</div>
|
||||
{plans.dailyEnabled && (
|
||||
<div className="pl-1 space-y-3 border-t border-slate-100 dark:border-slate-700 pt-3">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1.5">
|
||||
Каждые N дней
|
||||
</label>
|
||||
<div className="flex gap-1.5 flex-wrap">
|
||||
{[1, 2, 3, 7].map(n => (
|
||||
<button
|
||||
key={n}
|
||||
onClick={() => setP('dailyIntervalDays', n)}
|
||||
className={cn(
|
||||
'px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors',
|
||||
plans.dailyIntervalDays === n
|
||||
? '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',
|
||||
)}
|
||||
>
|
||||
{n === 1 ? 'Ежедневно' : `${n} дня`}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1.5">
|
||||
Начинать с дня заезда №
|
||||
</label>
|
||||
<input
|
||||
type="number" min={1} max={10}
|
||||
className="input w-20 text-sm"
|
||||
value={plans.dailyStartFromDay}
|
||||
onChange={e => setP('dailyStartFromDay', Math.max(1, parseInt(e.target.value) || 1))}
|
||||
/>
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
Уборка начнётся на {plans.dailyStartFromDay}-й день проживания
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* On-demand cleaning */}
|
||||
<div className="card p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<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">
|
||||
Гость может запросить уборку через QR-код или мобильное приложение
|
||||
</p>
|
||||
</div>
|
||||
<Toggle on={plans.onDemandEnabled} onChange={() => setP('onDemandEnabled', !plans.onDemandEnabled)} />
|
||||
</div>
|
||||
{plans.onDemandEnabled && (
|
||||
<div className="pl-1 border-t border-slate-100 dark:border-slate-700 pt-3">
|
||||
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1.5">Приоритет</label>
|
||||
<div className="flex gap-2">
|
||||
{([['high', 'Срочно'], ['normal', 'Обычный'], ['low', 'Низкий']] as const).map(([v, l]) => (
|
||||
<button
|
||||
key={v}
|
||||
onClick={() => setP('onDemandPriority', v)}
|
||||
className={cn(
|
||||
'px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors',
|
||||
plans.onDemandPriority === v
|
||||
? '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',
|
||||
)}
|
||||
>
|
||||
{l}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Deep cleaning */}
|
||||
<div className="card p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<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>
|
||||
<Toggle on={plans.deepCleanEnabled} onChange={() => setP('deepCleanEnabled', !plans.deepCleanEnabled)} />
|
||||
</div>
|
||||
{plans.deepCleanEnabled && (
|
||||
<div className="pl-1 border-t border-slate-100 dark:border-slate-700 pt-3 flex items-center gap-3">
|
||||
<label className="text-xs font-medium text-slate-600 dark:text-slate-400 shrink-0">Проводить каждые</label>
|
||||
<input
|
||||
type="number" min={1} max={30}
|
||||
className="input w-20 text-sm"
|
||||
value={plans.deepCleanEveryDays}
|
||||
onChange={e => setP('deepCleanEveryDays', Math.max(1, parseInt(e.target.value) || 7))}
|
||||
/>
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400">дней</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Inspection after cleaning */}
|
||||
<div className="card p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<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>
|
||||
<Toggle on={plans.inspectionAfterClean} onChange={() => setP('inspectionAfterClean', !plans.inspectionAfterClean)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Auto assign */}
|
||||
<div className="card p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<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>
|
||||
<Toggle on={plans.autoAssign} onChange={() => setP('autoAssign', !plans.autoAssign)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button onClick={savePlans} className="btn-primary">
|
||||
<Save size={14} />
|
||||
{planSaved ? 'Сохранено!' : 'Сохранить планы'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user