fix: calendar room label dedup + room number validation

- BookingCalendar: remove type badge below room number (already shown in group header)
- RoomModal: validate number against existingNumbers before save
  - Red border + error message if number taken
  - Placeholder shows next free number (e.g. if 101,102 exist → placeholder 103)
  - Save button disabled while number is taken

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-23 13:44:33 +03:00
parent 5d17ad0579
commit 4ea6dcc368
3 changed files with 31 additions and 9 deletions

View File

@@ -437,11 +437,6 @@ export function BookingCalendar({ rooms, bookings, slug, onBookingCreate, onBook
<span className="text-sm font-bold text-slate-900 dark:text-slate-100">{room.number}</span>
{room.name && !isMobile && <span className="text-xs text-slate-500 dark:text-slate-400">{room.name}</span>}
</div>
{!compact && !isMobile && (
<span className={cn('text-xs px-1.5 py-0.5 rounded-md font-medium', getRoomTypeColor(room.type))}>
{room.type}
</span>
)}
</div>
</div>

View File

@@ -46,6 +46,7 @@ type ModalTab = 'main' | 'places' | 'description' | 'photos'
interface RoomModalProps {
open: boolean
room?: Room
existingNumbers?: string[]
categories?: { id: string; name: string }[]
onClose: () => void
onSave: (room: Room) => void
@@ -53,7 +54,7 @@ interface RoomModalProps {
error?: string | null
}
export function RoomModal({ open, room, categories = [], onClose, onSave, onDelete, error }: RoomModalProps) {
export function RoomModal({ open, room, existingNumbers = [], categories = [], onClose, onSave, onDelete, error }: RoomModalProps) {
const isEdit = !!room
const [tab, setTab] = useState<ModalTab>('main')
@@ -126,8 +127,24 @@ export function RoomModal({ open, room, categories = [], onClose, onSave, onDele
setPhotoIdx(p => Math.max(0, p - 1))
}
// Find next available room number (skips existing ones)
const nextFreeNumber = (() => {
if (isEdit) return null
const taken = new Set(existingNumbers)
// Try incrementing from the highest existing number
const nums = existingNumbers
.map(n => parseInt(n))
.filter(n => !isNaN(n))
.sort((a, b) => a - b)
let candidate = (nums[nums.length - 1] ?? 100) + 1
while (taken.has(String(candidate))) candidate++
return String(candidate)
})()
const numberTaken = !isEdit && !!form.number && existingNumbers.includes(form.number)
const handleSave = () => {
if (!form.number) return
if (!form.number || numberTaken) return
onSave({
id: room?.id ?? `r-${Date.now()}`,
hotelId: room?.hotelId ?? 'hotel-1',
@@ -183,7 +200,7 @@ export function RoomModal({ open, room, categories = [], onClose, onSave, onDele
<p className="text-sm text-red-600 dark:text-red-400 flex-1 text-left">{error}</p>
)}
<button onClick={onClose} className="btn-secondary">Отмена</button>
<button onClick={handleSave} className="btn-primary" disabled={!form.number}>
<button onClick={handleSave} className="btn-primary" disabled={!form.number || numberTaken}>
{isEdit ? 'Сохранить' : 'Добавить номер'}
</button>
</>
@@ -213,7 +230,16 @@ export function RoomModal({ open, room, categories = [], onClose, onSave, onDele
<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="101" value={form.number} onChange={e => set('number', e.target.value)} />
<input
type="text"
className={cn('input', numberTaken && 'border-red-400 focus:ring-red-400')}
placeholder={nextFreeNumber ?? '101'}
value={form.number}
onChange={e => set('number', e.target.value)}
/>
{numberTaken && (
<p className="text-xs text-red-500 mt-1">Номер {form.number} уже существует{nextFreeNumber ? `. Следующий свободный: ${nextFreeNumber}` : ''}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Название</label>