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:
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user