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>
|
<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>}
|
{room.name && !isMobile && <span className="text-xs text-slate-500 dark:text-slate-400">{room.name}</span>}
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ type ModalTab = 'main' | 'places' | 'description' | 'photos'
|
|||||||
interface RoomModalProps {
|
interface RoomModalProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
room?: Room
|
room?: Room
|
||||||
|
existingNumbers?: string[]
|
||||||
categories?: { id: string; name: string }[]
|
categories?: { id: string; name: string }[]
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onSave: (room: Room) => void
|
onSave: (room: Room) => void
|
||||||
@@ -53,7 +54,7 @@ interface RoomModalProps {
|
|||||||
error?: string | null
|
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 isEdit = !!room
|
||||||
const [tab, setTab] = useState<ModalTab>('main')
|
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))
|
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 = () => {
|
const handleSave = () => {
|
||||||
if (!form.number) return
|
if (!form.number || numberTaken) return
|
||||||
onSave({
|
onSave({
|
||||||
id: room?.id ?? `r-${Date.now()}`,
|
id: room?.id ?? `r-${Date.now()}`,
|
||||||
hotelId: room?.hotelId ?? 'hotel-1',
|
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>
|
<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={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 ? 'Сохранить' : 'Добавить номер'}
|
{isEdit ? 'Сохранить' : 'Добавить номер'}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
@@ -213,7 +230,16 @@ export function RoomModal({ open, room, categories = [], onClose, onSave, onDele
|
|||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Номер *</label>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Название</label>
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Название</label>
|
||||||
|
|||||||
@@ -208,6 +208,7 @@ export function RoomsPage() {
|
|||||||
key={editingRoom?.id ?? 'new'}
|
key={editingRoom?.id ?? 'new'}
|
||||||
open={modalOpen}
|
open={modalOpen}
|
||||||
room={editingRoom}
|
room={editingRoom}
|
||||||
|
existingNumbers={rooms.map(r => r.number)}
|
||||||
onClose={() => { setModalOpen(false); setSaveError(null) }}
|
onClose={() => { setModalOpen(false); setSaveError(null) }}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
categories={categories}
|
categories={categories}
|
||||||
|
|||||||
Reference in New Issue
Block a user