// Hotel Admin PMS — strani: Dashboard, Koledar, Rezervacije, Sobe, Gostje, Cene/Kanali, Finance, Nastavitve function HADashboard({ dark, onNav }) { const muted = dark?'#9A9A9A':'#707070'; return <> Dnevni izkaz onNav('reservations')}>Nova rezervacija }/>
Zasedenost — naslednjih 14 dni
Vključuje vse kanale
onNav('calendar')}>Odpri koledar →
{CALENDAR_14D.map((d,i)=>{ const isWk = d.weekday==='So'||d.weekday==='Ne'; return
85?'#FF9800':muted}}>{d.occupancy}%
85 ? 'linear-gradient(180deg,#FF9800,#F57C00)' : isWk ? 'linear-gradient(180deg,#00BCD4,#2196F3)' : (dark?'#333':'#E3F2FD'), minHeight:6, transition:'all 200ms'}}/>
{d.weekday}
{d.day}.
; })}
Kanali — danes
{CHANNELS.slice(0,5).map((c,i)=>{ const max = Math.max(...CHANNELS.map(x=>x.share)); return
0?`1px solid ${dark?'#2B2B2B':'#F5F5F5'}`:'none'}}>
{c.name}
{c.share}%
; })}
Današnji prihodi
onNav('reservations')}>Vse →
{RESERVATIONS.filter(r=>r.checkin==='28.04.').slice(0,5).map((r,i)=>
0?`1px solid ${dark?'#2B2B2B':'#F5F5F5'}`:'none'}}>
{r.guest.split(' ').map(w=>w[0]).join('')}
{r.guest}
{r.vip && VIP}
{r.id} · soba {r.room} · {r.nights} noči · {r.channel}
{r.status==='arriving'?'Prihaja':'Potrjeno'}
)}
Prošnje gostov
{GUEST_REQUESTS.map((g,i)=>{ const pri = {urgent:'danger', high:'orange', med:'cyan', low:'neutral'}[g.priority]; const stt = {open:['orange','Odprto'], in_progress:['blue','V obdelavi'], approved:['success','Odobreno']}[g.status]; return
0?`1px solid ${dark?'#2B2B2B':'#F5F5F5'}`:'none', display:'flex', gap:12, alignItems:'center'}}>
{g.room}
{g.request}
{g.guest} · {g.time}
{stt[1]}
; })}
; } function HACalendar({ dark }) { const muted = dark?'#9A9A9A':'#707070'; // Simple 14-day × room-type calendar grid return <> Filtri Izvozi Nova rezervacija }/>
April – Maj 2026
{['Dan','Teden','14d','Mesec'].map((v,i)=>)}
{/* Header row */}
Tip sobe
{CALENDAR_14D.map((d,i)=>
{d.weekday}
{d.day}
)}
{/* Type rows */} {ROOM_TYPES.map(t=>(
{t.name}
{t.count} sob · €{t.rate}/noč
{CALENDAR_14D.map((d,i)=>{ const occ = Math.round(t.count * d.occupancy/100); const free = t.count - occ; const pct = (occ/t.count)*100; const color = pct>90 ? '#FF9800' : pct>70 ? '#2DD164' : pct>40 ? '#2196F3' : '#9A9A9A'; return
e.currentTarget.style.background=dark?'#2B2B2B':'#FAFAFA'} onMouseLeave={e=>e.currentTarget.style.background='transparent'}>
{free}
prosto
; })}
))}
Legenda: nizka zasedenost srednja visoka kritična (>90%)
; } function HAReservations({ dark }) { const chMap = {'Booking.com':'blue','Expedia':'orange','Airbnb':'danger','Direct':'cyan','Walk-in':'neutral','Hotelbeds':'purple'}; const stMap = {confirmed:['blue','Potrjeno'], arriving:['orange','Prihaja'], in_house:['success','V hiši'], departed:['neutral','Odšel']}; return <> r.checkin==='28.04.').length} prihaja danes`} actions={<>IzvoziNova rezervacija}/>
{['Vse','Prihodi danes','V hiši','Odhodi','Nepotrjene'].map((t,i)=> )}
{r.id}}, {key:'guest', label:'Gost', render:r=>
{r.guest.split(' ').map(w=>w[0]).join('')}
{r.guest} {r.vip && VIP}
{r.email}
}, {key:'checkin', label:'Prihod / odhod', render:r=>
{r.checkin} → {r.checkout}
{r.nights} noči · {r.pax} oseb
}, {key:'room', label:'Soba', render:r=>
{r.room}
{r.type}
}, {key:'channel', label:'Kanal', render:r=>{r.channel}}, {key:'total', label:'Znesek', align:'right', render:r=>
€{r.total}
{r.paid===r.total?'plačano':r.paid===0?'neplačano':`-€${r.total-r.paid}`}
}, {key:'status', label:'Status', render:r=>{stMap[r.status]?.[1]||r.status}}, ]} rows={RESERVATIONS}/>
; } function HARooms({ dark }) { const muted = dark?'#9A9A9A':'#707070'; const stColor = {clean:'#2DD164', dirty:'#FF9800', inspected:'#00BCD4', occupied:'#2196F3', arriving:'#9C27B0', departing:'#FFB74D', out_of_order:'#E53935'}; const stLbl = {clean:'Čista', dirty:'Umazana', inspected:'Pregledana', occupied:'Zasedena', arriving:'Prihaja', departing:'Odhaja', out_of_order:'Izven'}; const floors = [1,2,3,4]; const summary = ROOM_STATUS.map(s=>({s, n: ROOMS.filter(r=>r.status===s).length})); return <> r.status==='occupied').length} zasedenih · ${ROOMS.filter(r=>r.status==='clean').length} pripravljenih`} actions={<>FiltriNova soba}/>
{summary.map(({s,n})=>
{stLbl[s]}
{n}
)}
{floors.map(f=>(
{f}. nadstropje
{ROOMS.filter(r=>r.floor===f).length} sob
{ROOMS.filter(r=>r.floor===f).map(r=>(
e.currentTarget.style.background=dark?'#333':'#fff'} onMouseLeave={e=>e.currentTarget.style.background=dark?'#2B2B2B':'#F8F9FA'}>
{r.num}
{r.type}
{stLbl[r.status]}
{r.guest &&
{r.guest}
} {r.guest &&
{r.nights} {r.nights===1?'noč':r.nights<5?'noči':'noči'}
}
))}
))} ; } function HAGuests({ dark }) { const muted = dark?'#9A9A9A':'#707070'; const guests = RESERVATIONS.map(r=>({...r, stays: ((r.guest.charCodeAt(0)*7)%6)+1, spend: r.total*(((r.guest.charCodeAt(0)%4)+1))})).slice(0,12); return <> IzvoziNov gost}/>
{r.guest.split(' ').map(w=>w[0]).join('')}
{r.guest} {r.vip && VIP}
{r.email}
}, {key:'channel', label:'Vir', render:r=>{r.channel}}, {key:'stays', label:'Bivanj', align:'center', render:r=>{r.stays}}, {key:'last', label:'Zadnje bivanje', render:r=>{r.checkin}}, {key:'spend', label:'LTV', align:'right', render:r=>€{r.spend.toLocaleString('sl-SI')}}, {key:'note', label:'Opombe', render:r=>{r.note||}}, ]} rows={guests}/>
; } function HAChannels({ dark }) { const muted = dark?'#9A9A9A':'#707070'; const stMap = {ok:['success','Sinhronizirano'], syncing:['blue','Sinhronizira'], down:['danger','Nedosegljivo']}; return <> PoročiloDodaj kanal}/>
c.status!=='down').length}/> s+c.live,0)}/> s+c.revenue,0).toLocaleString('sl-SI')}`} delta="+12,4 %"/>
{c.name.slice(0,2).toUpperCase()}
{c.name}
}, {key:'live', label:'Aktivne rez.', align:'right', render:c=>{c.live}}, {key:'share', label:'Delež', render:c=>
{c.share}%
}, {key:'revenue', label:'Prihodek (mes.)', align:'right', render:c=>€{c.revenue.toLocaleString('sl-SI')}}, {key:'sync', label:'Sinhroniziran', render:c=>{c.sync}}, {key:'status', label:'Status', render:c=>{stMap[c.status][1]}}, ]} rows={CHANNELS}/>
Cene po tipu sobe — april 2026
{t.name}}, {key:'count', label:'Sob', align:'right'}, {key:'rate', label:'Bazna cena', align:'right', render:t=>€{t.rate}}, {key:'wknd', label:'Vikend', align:'right', render:t=>€{Math.round(t.rate*1.25)}}, {key:'high', label:'Visoka sez.', align:'right', render:t=>€{Math.round(t.rate*1.5)}}, {key:'last', label:'Last-minute', align:'right', render:t=>€{Math.round(t.rate*0.85)}}, ]} rows={ROOM_TYPES}/>
; } function HAReports({ dark }) { const muted = dark?'#9A9A9A':'#707070'; return <> PDFExcel}/>
Zasedenost · 30 dni
Povprečje 78,4 % · vrh 88 % (12. apr)
ADR · 30 dni
Povprečje €142 · vrh €152
Prihodek po tipu sobe
{ROOM_TYPES.map(t=>{ const rev = t.count * t.rate * 22; const max = Math.max(...ROOM_TYPES.map(x=>x.count*x.rate*22)); return
{t.name} · {t.count} sob
€{rev.toLocaleString('sl-SI')}
; })}
Poročila
{[ ['Dnevni izkaz','Promet, plačila, zasedenost'], ['Mesečno poročilo','April 2026 · za upravo'], ['DDV obračun','Davčna evidenca FURS'], ['AJPES gostje','Tedensko poročilo'], ['Provizijski obračun','Booking.com, Expedia'], ['Plače / fond ur','Po oddelkih'], ].map(([n,d],i)=>
0?`1px solid ${dark?'#2B2B2B':'#F5F5F5'}`:'none', display:'flex', alignItems:'center', gap:12, cursor:'pointer'}}>
{n}
{d}
)}
; } function HASettings({ dark }) { const muted = dark?'#9A9A9A':'#707070'; const [tab, setTab] = React.useState('hotel'); const tabs = [['hotel','Hotel'],['rooms','Tipi sob'],['policies','Pravila'],['emails','E-mail predloge'],['integrations','Integracije'],['team','Ekipa']]; return <>
{tabs.map(([k,l])=> )}
{tab==='hotel' &&
Osnovni podatki
{[['Ime hotela','Hotel Triglav Bled'],['Naslov','Cesta svobode 33, 4260 Bled'],['Telefon','+386 4 579 1000'],['E-pošta','info@hoteltriglav.si'],['Davčna št.','SI12345678'],['Klasifikacija','★★★★ (4 zvezdice)']].map(([l,v])=>
{l}
{v}
)}
Slovenska zakonodaja
{[['FURS davčna blagajna','poslovni prostor T-001','success'],['AJPES poročanje gostov','avtomatsko ob check-in','success'],['Turistična taksa','€2,50 / odrasli / nočitev','success'],['Pravna obvestila','GDPR, ZVOP-2, ZGos','success']].map(([l,v,t])=>
{l}
{v}
OK
)}
} {tab!=='hotel' &&
Vsebina za zavihek "{tabs.find(t=>t[0]===tab)[1]}" — konfiguracija sob, pravil odpovedi, e-mail predlog ipd.
} ; } // ─── PLAHTA — Gantt-style room rack calendar ───────────────────────────────── function HAPlahta({ dark }) { const muted = dark ? '#9A9A9A' : '#707070'; const border = dark ? '#2B2B2B' : '#F0F0F0'; const cellBg = dark ? '#1A1A1A' : '#fff'; // State: start date (default Apr 28 2026), view range, selected reservation popover const [startDate, setStartDate] = React.useState(new Date(2026, 3, 28)); const [viewDays, setViewDays] = React.useState(14); const [selected, setSelected] = React.useState(null); // { res, cellEl } const [popoverPos, setPopoverPos] = React.useState({ top: 0, left: 0 }); const ROOM_COL_W = 160; const CELL_W = 62; // Channel color map const CH_COLOR = { 'Booking.com': '#2196F3', 'Expedia': '#FF9800', 'Direct': '#00BCD4', 'Walk-in': '#78909C', 'Airbnb': '#9C27B0', 'Hotelbeds': '#43A047', }; const CH_TONE = { 'Booking.com': 'blue', 'Expedia': 'orange', 'Direct': 'cyan', 'Walk-in': 'neutral', 'Airbnb': 'purple', 'Hotelbeds': 'success', }; // Build date columns const days = []; for (let i = 0; i < viewDays; i++) { const d = new Date(startDate); d.setDate(d.getDate() + i); days.push(d); } const fmtDay = d => d.getDate(); const fmtWd = d => ['Ne','Po','To','Sr','Če','Pe','So'][d.getDay()]; const isWknd = d => d.getDay() === 0 || d.getDay() === 6; // Parse reservation date strings like "28.04." into a Date object // Assumes year is 2026 function parseResDate(str) { if (!str || str === '—') return null; const parts = str.replace(/\.$/, '').split('.'); if (parts.length < 2) return null; const day = parseInt(parts[0], 10); const month = parseInt(parts[1], 10) - 1; return new Date(2026, month, day); } // For each room, find overlapping reservations within [startDate, startDate+viewDays) function getResForRoom(roomNum) { const endDate = new Date(startDate); endDate.setDate(endDate.getDate() + viewDays); return RESERVATIONS.filter(r => { if (!r.room || r.room === '—') return false; if (r.room !== roomNum) return false; const ci = parseResDate(r.checkin); const co = parseResDate(r.checkout); if (!ci || !co) return false; // overlaps if checkin < endDate AND checkout > startDate return ci < endDate && co > startDate; }); } // Returns [startCol (0-based), spanCols] for a reservation within the visible window function getBarSpan(res) { const ci = parseResDate(res.checkin); const co = parseResDate(res.checkout); if (!ci || !co) return null; const windowStart = new Date(startDate); const windowEnd = new Date(startDate); windowEnd.setDate(windowEnd.getDate() + viewDays); const barStart = ci < windowStart ? windowStart : ci; const barEnd = co > windowEnd ? windowEnd : co; const startCol = Math.round((barStart - windowStart) / 86400000); const span = Math.round((barEnd - barStart) / 86400000); if (span <= 0) return null; return [startCol, span]; } function navDate(delta) { const d = new Date(startDate); d.setDate(d.getDate() + delta * viewDays); setStartDate(d); setSelected(null); } function fmtRange() { const end = new Date(startDate); end.setDate(end.getDate() + viewDays - 1); const months = ['jan','feb','mar','apr','maj','jun','jul','avg','sep','okt','nov','dec']; const s = startDate; const e = end; if (s.getMonth() === e.getMonth()) return `${s.getDate()}. – ${e.getDate()}. ${months[s.getMonth()]} ${s.getFullYear()}`; return `${s.getDate()}. ${months[s.getMonth()]} – ${e.getDate()}. ${months[e.getMonth()]} ${e.getFullYear()}`; } function handleBarClick(e, res) { e.stopPropagation(); const rect = e.currentTarget.getBoundingClientRect(); setPopoverPos({ top: rect.bottom + window.scrollY + 4, left: Math.min(rect.left + window.scrollX, window.innerWidth - 280) }); setSelected(selected && selected.id === res.id ? null : res); } // Group rooms by floor const floors = [1,2,3,4]; const statusSl = {confirmed:'Potrjeno', arriving:'Prihaja', in_house:'V hiši', departed:'Odšel'}; const statusTone = {confirmed:'blue', arriving:'orange', in_house:'success', departed:'neutral'}; const totalWidth = ROOM_COL_W + viewDays * CELL_W; return <> navDate(-1)}>Nazaj navDate(1)}>Naprej Nova rezervacija }/> {/* View range toggle */}
Prikaz: {[['7d',7],['14d',14],['30d',30]].map(([lbl,v])=>( ))}
setSelected(null)}>
{/* Sticky header row */}
Soba
{days.map((d,i)=>(
{fmtWd(d)}
{fmtDay(d)}
))}
{/* Room rows grouped by floor */} {floors.map(floor=>{ const floorRooms = ROOMS.filter(r=>r.floor===floor); return {/* Floor label row */}
{floor}. nadstropje · {floorRooms.length} sob
{/* Individual room rows */} {floorRooms.map((room, ri)=>{ const reservations = getResForRoom(room.num); const rowBg = ri%2===0 ? cellBg : (dark?'#161616':'#FAFAFA'); // Build bar map: colIndex -> reservation const barMap = {}; reservations.forEach(res=>{ const span = getBarSpan(res); if (span) barMap[span[0]] = { res, span: span[1] }; }); return (
{/* Sticky left: room info */}
{room.num} {room.type}
{/* Date cells with reservation bars */} {days.map((d,ci)=>{ const bar = barMap[ci]; const wknd = isWknd(d); const baseCellBg = wknd ? (dark?'rgba(255,152,0,.04)':'#FFFDE7') : rowBg; return (
{bar && (
handleBarClick(e, bar.res)} style={{ position:'absolute', top:'50%', transform:'translateY(-50%)', left:2, height:28, width: `calc(${bar.span * CELL_W}px - 4px)`, background: CH_COLOR[bar.res.channel] || '#9A9A9A', borderRadius:6, cursor:'pointer', zIndex:3, display:'flex', alignItems:'center', paddingLeft:8, overflow:'hidden', boxShadow:'0 1px 4px rgba(0,0,0,.18)', transition:'filter 120ms', opacity: 0.92, }} onMouseEnter={e=>e.currentTarget.style.filter='brightness(1.12)'} onMouseLeave={e=>e.currentTarget.style.filter='none'} > {bar.res.guest} {bar.span > 1 && ( {bar.res.nights}n )}
)}
); })}
); })}
; })}
{/* Popover for selected reservation */} {selected && (
e.stopPropagation()}>
{selected.guest}
Rezervacija {selected.id}
Soba {selected.room} ({selected.type})
Prihod {selected.checkin}
Odhod {selected.checkout}
Kanal {selected.channel}
Status {statusSl[selected.status]||selected.status}
{selected.note && (
{selected.note}
)}
)} {/* Legend */}
Kanali: {Object.entries(CH_COLOR).map(([name, color])=>( {name} ))}
; } Object.assign(window, { HADashboard, HACalendar, HAReservations, HARooms, HAGuests, HAChannels, HAReports, HASettings, HAPlahta });