Fix mobile responsiveness across all pages

- PosPage: rooms list becomes horizontal scroll row on mobile instead of w-56 sidebar
- AvailabilityPage: edit panel stacks below grid on mobile (was fixed w-80 side panel)
- UsersPage roles tab: sidebar stacks above permissions on mobile
- All page containers: p-6 → p-4 md:p-6 (TariffsPage, DiscountsPage, MaintenancePage, ReportsPage, DynamicPricingPage, GuestsPage, LoyaltyPage)
- Stats grids: lg:grid-cols-4 → md:grid-cols-4 (GuestsPage, TariffsPage, DiscountsPage, MaintenancePage, ReportsPage, LoyaltyPage)
- Modal form grids: grid-cols-3 → grid-cols-1 sm:grid-cols-3, grid-cols-5 → grid-cols-3 sm:grid-cols-5, grid-cols-4 → grid-cols-2 sm:grid-cols-4
- PosPage modals: fixed w-80/w-96 → responsive w-[calc(100vw-2rem)] sm:w-80/96

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-17 00:27:43 +03:00
parent 91e58ff994
commit 2fc66d1e84
10 changed files with 62 additions and 35 deletions

View File

@@ -265,7 +265,7 @@ function EditPanel({
}, [startDate, endDate]) }, [startDate, endDate])
return ( return (
<div className="w-80 shrink-0 border-l border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 flex flex-col overflow-y-auto"> <div className="w-full sm:w-80 shrink-0 border-t sm:border-t-0 sm:border-l border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 flex flex-col overflow-y-auto max-h-72 sm:max-h-none">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-slate-200 dark:border-slate-700"> <div className="flex items-center justify-between px-4 py-3 border-b border-slate-200 dark:border-slate-700">
<div> <div>
@@ -922,7 +922,7 @@ export function AvailabilityPage() {
</div> </div>
{/* ── Content ── */} {/* ── Content ── */}
<div className="flex-1 overflow-hidden flex"> <div className="flex-1 overflow-hidden flex flex-col sm:flex-row">
{/* Grid tab */} {/* Grid tab */}
{tab === 'grid' && ( {tab === 'grid' && (

View File

@@ -140,8 +140,8 @@ function DiscountModal({
} }
> >
<div className="space-y-4"> <div className="space-y-4">
<div className="grid grid-cols-3 gap-3"> <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div className="col-span-2"> <div className="sm:col-span-2">
<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="Корпоративная скидка" <input type="text" className="input" placeholder="Корпоративная скидка"
value={form.name} onChange={e => set('name', e.target.value)} /> value={form.name} onChange={e => set('name', e.target.value)} />
@@ -285,7 +285,7 @@ export function DiscountsPage() {
setDiscounts(prev => prev.map(d => d.id === id ? { ...d, isActive: !d.isActive } : d)) setDiscounts(prev => prev.map(d => d.id === id ? { ...d, isActive: !d.isActive } : d))
return ( return (
<div className="p-6 space-y-6"> <div className="p-4 md:p-6 space-y-6">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
@@ -301,7 +301,7 @@ export function DiscountsPage() {
</div> </div>
{/* Stats */} {/* Stats */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3"> <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{[ {[
{ label: 'Всего скидок', value: discounts.length }, { label: 'Всего скидок', value: discounts.length },
{ label: 'Активных', value: discounts.filter(d => d.isActive).length }, { label: 'Активных', value: discounts.filter(d => d.isActive).length },

View File

@@ -772,7 +772,7 @@ export function DynamicPricingPage() {
setRules(prev => prev.map(r => r.id === id ? { ...r, isActive: !r.isActive } : r)) setRules(prev => prev.map(r => r.id === id ? { ...r, isActive: !r.isActive } : r))
return ( return (
<div className="p-6 space-y-6"> <div className="p-4 md:p-6 space-y-6">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>

View File

@@ -287,7 +287,7 @@ function GuestModal({ guest, onClose }: { guest: Guest; onClose: () => void }) {
</div> </div>
{/* Body */} {/* Body */}
<div className="flex-1 overflow-y-auto p-6"> <div className="flex-1 overflow-y-auto p-4 sm:p-6">
{tab === 'overview' && ( {tab === 'overview' && (
<div className="space-y-5"> <div className="space-y-5">
{/* Tags */} {/* Tags */}
@@ -556,7 +556,7 @@ export function GuestsPage() {
const avgRating = (MOCK_GUESTS.reduce((s, g) => s + g.rating, 0) / MOCK_GUESTS.length).toFixed(1) const avgRating = (MOCK_GUESTS.reduce((s, g) => s + g.rating, 0) / MOCK_GUESTS.length).toFixed(1)
return ( return (
<div className="p-6 space-y-6"> <div className="p-4 md:p-6 space-y-6">
{/* Header */} {/* Header */}
<div> <div>
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">Гости</h1> <h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">Гости</h1>
@@ -588,7 +588,7 @@ export function GuestsPage() {
{pageTab === 'list' && <> {pageTab === 'list' && <>
{/* Stats */} {/* Stats */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3"> <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{[ {[
{ icon: Users, label: 'Всего гостей', value: MOCK_GUESTS.length, color: 'text-brand-600 dark:text-brand-400', bg: 'bg-brand-50 dark:bg-brand-900/20' }, { icon: Users, label: 'Всего гостей', value: MOCK_GUESTS.length, color: 'text-brand-600 dark:text-brand-400', bg: 'bg-brand-50 dark:bg-brand-900/20' },
{ icon: Award, label: 'VIP гостей', value: vipCount, color: 'text-amber-600 dark:text-amber-400', bg: 'bg-amber-50 dark:bg-amber-900/20' }, { icon: Award, label: 'VIP гостей', value: vipCount, color: 'text-amber-600 dark:text-amber-400', bg: 'bg-amber-50 dark:bg-amber-900/20' },

View File

@@ -815,7 +815,7 @@ export function LoyaltyPage() {
const totalPoints = MOCK_GUESTS.reduce((s, g) => s + g.points, 0) const totalPoints = MOCK_GUESTS.reduce((s, g) => s + g.points, 0)
return ( return (
<div className="p-6 space-y-5"> <div className="p-4 md:p-6 space-y-5">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
@@ -837,7 +837,7 @@ export function LoyaltyPage() {
</div> </div>
{/* Stats */} {/* Stats */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3"> <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{[ {[
{ label: 'Участников программы', value: activeGuests, icon: User, color: 'text-brand-600 dark:text-brand-400', bg: 'bg-brand-50 dark:bg-brand-900/20' }, { label: 'Участников программы', value: activeGuests, icon: User, color: 'text-brand-600 dark:text-brand-400', bg: 'bg-brand-50 dark:bg-brand-900/20' },
{ label: 'Всего баллов в обороте', value: totalPoints.toLocaleString('ru-RU'), icon: TrendingUp, color: 'text-amber-600 dark:text-amber-400', bg: 'bg-amber-50 dark:bg-amber-900/20' }, { label: 'Всего баллов в обороте', value: totalPoints.toLocaleString('ru-RU'), icon: TrendingUp, color: 'text-amber-600 dark:text-amber-400', bg: 'bg-amber-50 dark:bg-amber-900/20' },
@@ -885,7 +885,7 @@ export function LoyaltyPage() {
{editSettings ? <><Save size={13} /> Сохранить</> : <><Edit2 size={13} /> Изменить</>} {editSettings ? <><Save size={13} /> Сохранить</> : <><Edit2 size={13} /> Изменить</>}
</button> </button>
</div> </div>
<div className="grid grid-cols-3 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{[ {[
{ label: 'Начисление', desc: `${settings.pointsPerRuble} балл за 1 ₽`, field: 'pointsPerRuble' as const, unit: 'балл/₽' }, { label: 'Начисление', desc: `${settings.pointsPerRuble} балл за 1 ₽`, field: 'pointsPerRuble' as const, unit: 'балл/₽' },
{ label: 'Ценность', desc: `1 балл = ${settings.pointValue}`, field: 'pointValue' as const, unit: '₽' }, { label: 'Ценность', desc: `1 балл = ${settings.pointValue}`, field: 'pointValue' as const, unit: '₽' },

View File

@@ -231,7 +231,7 @@ function MaintenanceModal({
{/* Type selector */} {/* Type selector */}
<div> <div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Тип</label> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Тип</label>
<div className="grid grid-cols-3 gap-2"> <div className="grid grid-cols-3 sm:grid-cols-3 gap-2">
{([ {([
{ id: 'room' as const, label: 'Номер', icon: BedDouble }, { id: 'room' as const, label: 'Номер', icon: BedDouble },
{ id: 'service' as const, label: 'Услуга', icon: Wrench }, { id: 'service' as const, label: 'Услуга', icon: Wrench },
@@ -362,7 +362,7 @@ export function MaintenancePage() {
const active = records.filter(r => r.status !== 'done' && r.status !== 'cancelled') const active = records.filter(r => r.status !== 'done' && r.status !== 'cancelled')
return ( return (
<div className="p-6 space-y-6"> <div className="p-4 md:p-6 space-y-6">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
@@ -378,7 +378,7 @@ export function MaintenancePage() {
</div> </div>
{/* Stats */} {/* Stats */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3"> <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{[ {[
{ label: 'Всего записей', value: records.length, cls: '' }, { label: 'Всего записей', value: records.length, cls: '' },
{ label: 'Активных', value: active.length, cls: 'text-amber-600 dark:text-amber-400' }, { label: 'Активных', value: active.length, cls: 'text-amber-600 dark:text-amber-400' },

View File

@@ -270,14 +270,41 @@ export function PosPage() {
<> <>
{/* ══════════════════════ TAB: main ══════════════════════ */} {/* ══════════════════════ TAB: main ══════════════════════ */}
{activeTab === 'main' && ( {activeTab === 'main' && (
<div className="flex flex-1 overflow-hidden"> <div className="flex flex-col sm:flex-row flex-1 overflow-hidden">
{/* ── Left sidebar: Заезды (w-56) ── */} {/* ── Заезды: horizontal scroll on mobile, sidebar on desktop ── */}
<div className="w-56 flex flex-col border-r border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/50 shrink-0 overflow-hidden"> <div className="sm:w-56 flex-shrink-0 border-b sm:border-b-0 sm:border-r border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/50">
<div className="px-3 py-2 border-b border-slate-200 dark:border-slate-700"> <div className="px-3 py-2 border-b border-slate-200 dark:border-slate-700 hidden sm:block">
<p className="text-[11px] font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide">Заезды</p> <p className="text-[11px] font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide">Заезды</p>
</div> </div>
<div className="flex-1 overflow-y-auto p-2 space-y-1"> {/* Mobile: horizontal scroll row */}
<div className="flex sm:hidden gap-2 p-2 overflow-x-auto">
{MOCK_BALANCES.map(rb => {
const debt = Math.max(0, rb.totalAmount - rb.paidAmount)
const paid = debt === 0
const sel = selectedRoom?.roomId === rb.roomId
return (
<button
key={rb.roomId}
onClick={() => setSelectedRoom(rb)}
className={cn(
'shrink-0 rounded-lg px-3 py-1.5 border transition-all flex items-center gap-1.5',
sel
? 'border-brand-400 bg-brand-50 dark:bg-brand-900/20 dark:border-brand-600'
: 'border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-800',
)}
>
<span className="text-xs font-bold text-slate-900 dark:text-slate-100">{rb.roomNumber}</span>
{paid
? <span className="text-[9px] font-semibold px-1 py-0.5 rounded-full bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300"></span>
: <span className="text-[9px] font-semibold px-1 py-0.5 rounded-full bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300 whitespace-nowrap">{formatCurrency(debt)}</span>
}
</button>
)
})}
</div>
{/* Desktop: vertical list */}
<div className="hidden sm:block flex-1 overflow-y-auto p-2 space-y-1">
{MOCK_BALANCES.map(rb => { {MOCK_BALANCES.map(rb => {
const debt = Math.max(0, rb.totalAmount - rb.paidAmount) const debt = Math.max(0, rb.totalAmount - rb.paidAmount)
const paid = debt === 0 const paid = debt === 0
@@ -313,7 +340,7 @@ export function PosPage() {
</div> </div>
{/* ── Center: payment form ── */} {/* ── Center: payment form ── */}
<div className="flex-1 flex flex-col overflow-y-auto p-5 gap-5"> <div className="flex-1 flex flex-col overflow-y-auto p-4 sm:p-5 gap-5">
{selectedRoom ? ( {selectedRoom ? (
<> <>
{/* Guest info */} {/* Guest info */}
@@ -781,7 +808,7 @@ export function PosPage() {
{/* ── Receipt success overlay ── */} {/* ── Receipt success overlay ── */}
{lastReceipt && ( {lastReceipt && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="bg-white dark:bg-slate-800 rounded-2xl shadow-2xl p-8 w-80 text-center space-y-4"> <div className="bg-white dark:bg-slate-800 rounded-2xl shadow-2xl p-6 sm:p-8 w-[calc(100vw-2rem)] sm:w-80 text-center space-y-4">
<CheckCircle2 size={56} className="text-emerald-500 mx-auto" /> <CheckCircle2 size={56} className="text-emerald-500 mx-auto" />
<div> <div>
<p className="text-xl font-bold text-slate-900 dark:text-slate-100">Оплата принята</p> <p className="text-xl font-bold text-slate-900 dark:text-slate-100">Оплата принята</p>
@@ -815,7 +842,7 @@ export function PosPage() {
{/* ── Close shift confirmation modal ── */} {/* ── Close shift confirmation modal ── */}
{closeConfirm && ( {closeConfirm && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="bg-white dark:bg-slate-800 rounded-2xl shadow-2xl p-6 w-96 space-y-4"> <div className="bg-white dark:bg-slate-800 rounded-2xl shadow-2xl p-6 w-[calc(100vw-2rem)] sm:w-96 space-y-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center"> <div className="w-10 h-10 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center">
<Printer size={18} className="text-red-600" /> <Printer size={18} className="text-red-600" />

View File

@@ -21,7 +21,7 @@ const maxOcc = Math.max(...OCC)
export function ReportsPage() { export function ReportsPage() {
return ( return (
<div className="p-6 max-w-5xl mx-auto space-y-6"> <div className="p-4 md:p-6 max-w-5xl mx-auto space-y-6">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-cyan-600 flex items-center justify-center shrink-0"> <div className="w-10 h-10 rounded-xl bg-cyan-600 flex items-center justify-center shrink-0">
<BarChart3 size={20} className="text-white" /> <BarChart3 size={20} className="text-white" />
@@ -33,7 +33,7 @@ export function ReportsPage() {
</div> </div>
{/* KPI cards */} {/* KPI cards */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3"> <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{KPI.map(k => ( {KPI.map(k => (
<div key={k.label} className="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4"> <div key={k.label} className="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4">
<p className="text-xs text-slate-500 dark:text-slate-400 mb-2">{k.label}</p> <p className="text-xs text-slate-500 dark:text-slate-400 mb-2">{k.label}</p>

View File

@@ -200,8 +200,8 @@ function TariffModal({
> >
<div className="space-y-5"> <div className="space-y-5">
{/* Name + Code */} {/* Name + Code */}
<div className="grid grid-cols-3 gap-3"> <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div className="col-span-2"> <div className="sm:col-span-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5"> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
Название тарифа * Название тарифа *
</label> </label>
@@ -223,7 +223,7 @@ function TariffModal({
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Тип питания Тип питания
</label> </label>
<div className="grid grid-cols-5 gap-2"> <div className="grid grid-cols-3 sm:grid-cols-5 gap-2">
{mealPlans.map(mp => ( {mealPlans.map(mp => (
<button <button
key={mp.id} key={mp.id}
@@ -250,7 +250,7 @@ function TariffModal({
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Что включено Что включено
</label> </label>
<div className="grid grid-cols-4 gap-2"> <div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
{allInclusions.map(inc => { {allInclusions.map(inc => {
const active = form.inclusions.includes(inc.id) const active = form.inclusions.includes(inc.id)
return ( return (
@@ -278,7 +278,7 @@ function TariffModal({
</div> </div>
{/* Price modifier + min nights + cancellation */} {/* Price modifier + min nights + cancellation */}
<div className="grid grid-cols-3 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div> <div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5"> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
Надбавка / скидка к базовой цене Надбавка / скидка к базовой цене
@@ -413,7 +413,7 @@ export function TariffsPage() {
const inactiveTariffs = tariffs.filter(t => !t.isActive) const inactiveTariffs = tariffs.filter(t => !t.isActive)
return ( return (
<div className="p-6 space-y-6"> <div className="p-4 md:p-6 space-y-6">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
@@ -429,7 +429,7 @@ export function TariffsPage() {
</div> </div>
{/* Stats */} {/* Stats */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3"> <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{[ {[
{ label: 'Всего тарифов', value: tariffs.length, color: 'text-slate-900 dark:text-slate-100' }, { label: 'Всего тарифов', value: tariffs.length, color: 'text-slate-900 dark:text-slate-100' },
{ label: 'Активных', value: activeTariffs.length, color: 'text-emerald-600 dark:text-emerald-400' }, { label: 'Активных', value: activeTariffs.length, color: 'text-emerald-600 dark:text-emerald-400' },

View File

@@ -473,9 +473,9 @@ function RolesTab() {
const totalCount = ALL_MODULE_KEYS.length const totalCount = ALL_MODULE_KEYS.length
return ( return (
<div className="flex gap-5 items-start"> <div className="flex flex-col sm:flex-row gap-5 items-start">
{/* Role list */} {/* Role list */}
<div className="w-52 shrink-0"> <div className="w-full sm:w-52 shrink-0">
<div className="card overflow-hidden"> <div className="card overflow-hidden">
<div className="p-3 border-b border-slate-100 dark:border-slate-700"> <div className="p-3 border-b border-slate-100 dark:border-slate-700">
<p className="text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide">Роли</p> <p className="text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide">Роли</p>