<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CitiStayz — Breezeway Dashboard</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f5f0; color: #1a1a1a; min-height: 100vh; }
.topbar { background: #130e3d; color: #fff; padding: 0 1.5rem; height: 56px; display: flex; align-items: center; justify-content: space-between; position: sticky; top: 0; z-index: 100; }
.topbar-logo { font-size: 16px; font-weight: 600; letter-spacing: -0.01em; }
.topbar-logo span { color: #f0c040; }
.topbar-right { display: flex; align-items: center; gap: 12px; }
.status-pill { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; padding: 4px 12px; border-radius: 20px; border: 1px solid rgba(255,255,255,0.2); color: rgba(255,255,255,0.7); }
.status-pill.ok { background: rgba(34,197,94,0.2); color: #86efac; border-color: rgba(34,197,94,0.3); }
.status-pill.err { background: rgba(239,68,68,0.2); color: #fca5a5; border-color: rgba(239,68,68,0.3); }
.dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.refresh-btn { background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: #fff; padding: 6px 14px; border-radius: 8px; cursor: pointer; font-size: 13px; }
.refresh-btn:hover { background: rgba(255,255,255,0.2); }
.container { max-width: 1200px; margin: 0 auto; padding: 1.5rem; }
.metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 1.5rem; }
.metric { background: #fff; border-radius: 12px; padding: 1.25rem; border: 1px solid #e8e8e4; }
.metric-label { font-size: 12px; color: #888; margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.05em; }
.metric-value { font-size: 32px; font-weight: 600; color: #130e3d; }
.metric-sub { font-size: 12px; color: #aaa; margin-top: 3px; }
.tabs { display: flex; gap: 4px; margin-bottom: 1.25rem; background: #fff; border-radius: 10px; padding: 4px; border: 1px solid #e8e8e4; width: fit-content; }
.tab { padding: 7px 18px; border-radius: 7px; border: none; background: transparent; font-size: 13px; font-weight: 500; color: #888; cursor: pointer; }
.tab.on { background: #130e3d; color: #fff; }
.search { width: 100%; padding: 10px 14px; border: 1px solid #e8e8e4; border-radius: 10px; font-size: 14px; background: #fff; margin-bottom: 1rem; outline: none; }
.search:focus { border-color: #130e3d; }
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem; }
.section-title { font-size: 13px; font-weight: 600; color: #555; text-transform: uppercase; letter-spacing: 0.05em; }
.section-count { font-size: 13px; color: #aaa; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; }
.card { background: #fff; border: 1px solid #e8e8e4; border-radius: 12px; padding: 1rem 1.25rem; cursor: pointer; transition: border-color 0.15s, box-shadow 0.15s; }
.card:hover { border-color: #130e3d; box-shadow: 0 2px 12px rgba(19,14,61,0.08); }
.card-name { font-size: 14px; font-weight: 600; color: #1a1a1a; margin-bottom: 3px; }
.card-addr { font-size: 13px; color: #888; margin-bottom: 8px; }
.card-footer { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.badge { display: inline-block; font-size: 11px; font-weight: 500; padding: 3px 10px; border-radius: 20px; }
.badge-active { background: #f0fdf4; color: #16a34a; border: 1px solid #bbf7d0; }
.badge-inactive { background: #f5f5f5; color: #888; border: 1px solid #e8e8e4; }
.badge-open { background: #fffbeb; color: #d97706; border: 1px solid #fde68a; }
.badge-done { background: #f0fdf4; color: #16a34a; border: 1px solid #bbf7d0; }
.badge-clean { background: #eff6ff; color: #2563eb; border: 1px solid #bfdbfe; }
.badge-maint { background: #fef2f2; color: #dc2626; border: 1px solid #fecaca; }
.task-list { display: flex; flex-direction: column; gap: 8px; }
.task-card { background: #fff; border: 1px solid #e8e8e4; border-radius: 12px; padding: 0.875rem 1.25rem; display: flex; align-items: flex-start; gap: 12px; }
.task-dot { width: 8px; height: 8px; border-radius: 50%; margin-top: 5px; flex-shrink: 0; }
.dot-open { background: #f59e0b; }
.dot-done { background: #22c55e; }
.task-body { flex: 1; min-width: 0; }
.task-name { font-size: 14px; font-weight: 500; color: #1a1a1a; margin-bottom: 3px; }
.task-meta { font-size: 12px; color: #aaa; display: flex; gap: 10px; flex-wrap: wrap; }
.loader { text-align: center; padding: 3rem; color: #aaa; font-size: 14px; }
.spinner { display: inline-block; width: 24px; height: 24px; border: 2px solid #e8e8e4; border-top-color: #130e3d; border-radius: 50%; animation: spin 0.8s linear infinite; margin-bottom: 12px; }
@keyframes spin { to { transform: rotate(360deg); } }
.error-box { background: #fef2f2; border: 1px solid #fecaca; border-radius: 12px; padding: 1.25rem; color: #dc2626; margin-bottom: 1rem; }
.error-box strong { display: block; margin-bottom: 4px; }
.empty { text-align: center; padding: 2rem; color: #aaa; font-size: 14px; background: #fff; border-radius: 12px; border: 1px solid #e8e8e4; }
.filter-row { display: flex; gap: 6px; margin-bottom: 1rem; flex-wrap: wrap; }
.filter-btn { font-size: 12px; padding: 5px 12px; border-radius: 8px; border: 1px solid #e8e8e4; background: #fff; color: #888; cursor: pointer; }
.filter-btn.on { background: #130e3d; color: #fff; border-color: #130e3d; }
@media (max-width: 600px) {
.container { padding: 1rem; }
.metrics { grid-template-columns: repeat(2, 1fr); }
.grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="topbar">
<div class="topbar-logo">Citi<span>Stayz</span> — Breezeway</div>
<div class="topbar-right">
<span class="status-pill" id="status-pill"><span class="dot"></span> Connecting…</span>
<button class="refresh-btn" onclick="boot()">↻ Refresh</button>
</div>
</div>
<div class="container">
<div id="metrics-row" class="metrics" style="display:none">
<div class="metric"><div class="metric-label">Properties</div><div class="metric-value" id="m-props">—</div><div class="metric-sub">total</div></div>
<div class="metric"><div class="metric-label">Active</div><div class="metric-value" id="m-active">—</div><div class="metric-sub">in portfolio</div></div>
<div class="metric"><div class="metric-label">Tasks</div><div class="metric-value" id="m-tasks">—</div><div class="metric-sub">total loaded</div></div>
<div class="metric"><div class="metric-label">Open tasks</div><div class="metric-value" id="m-open">—</div><div class="metric-sub">need attention</div></div>
</div>
<div id="main"><div class="loader"><div class="spinner"></div><br>Loading your portfolio…</div></div>
</div>
<script>
const PROXY = 'https://breezeway-proxy.ssingh-d7e.workers.dev';
let allProps = [], allTasks = [], activeTab = 'props', taskFilter = 'all';
async function get(path) {
const r = await fetch(PROXY + path);
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
}
function fdate(d) {
if (!d) return '';
try { return new Date(d).toLocaleDateString('en-AU', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }); } catch(e) { return ''; }
}
function isDone(t) {
const s = (t.status || '').toLowerCase();
return s.includes('complet') || s.includes('closed') || s.includes('approv') || s.includes('done');
}
function taskType(t) {
const type = (t.task_type || t.type || t.template_name || t.name || '').toLowerCase();
if (type.includes('clean') || type.includes('departure')) return 'clean';
if (type.includes('maint') || type.includes('repair')) return 'maint';
if (type.includes('inspect') || type.includes('check')) return 'inspect';
return 'other';
}
function typeBadge(t) {
const type = taskType(t);
if (type === 'clean') return '<span class="badge badge-clean">Clean</span>';
if (type === 'maint') return '<span class="badge badge-maint">Maintenance</span>';
if (type === 'inspect') return '<span class="badge badge-open">Inspection</span>';
return '';
}
function renderProps(list) {
if (!list.length) return '<div class="empty">No properties found</div>';
return '<div class="grid">' + list.map(p => {
const addr = [p.address1, p.address2, p.city].filter(Boolean).join(', ');
const s = p.status === 'active' ? 'badge-active' : 'badge-inactive';
return `<div class="card">
<div class="card-name">${p.name || 'Unnamed'}</div>
<div class="card-addr">${addr}</div>
<div class="card-footer"><span class="badge ${s}">${p.status || 'active'}</span></div>
</div>`;
}).join('') + '</div>';
}
function renderTasks(list) {
if (!list.length) return '<div class="empty">No tasks found</div>';
return '<div class="task-list">' + list.map(t => {
const done = isDone(t);
const prop = t.property_name || t.home_name || '';
const due = fdate(t.scheduled_start_time || t.due_date || t.start_time);
const assignee = t.assignee_name || (t.assignees && t.assignees[0] && t.assignees[0].name) || '';
return `<div class="task-card">
<div class="task-dot ${done ? 'dot-done' : 'dot-open'}"></div>
<div class="task-body">
<div class="task-name">${t.name || t.title || t.task_type || 'Task'}</div>
<div class="task-meta">
${prop ? '<span>📍 ' + prop + '</span>' : ''}
${due ? '<span>🕐 ' + due + '</span>' : ''}
${assignee ? '<span>👤 ' + assignee + '</span>' : ''}
</div>
<div style="margin-top:6px;display:flex;gap:6px;flex-wrap:wrap">
${typeBadge(t)}
<span class="badge ${done ? 'badge-done' : 'badge-open'}">${t.status || 'open'}</span>
</div>
</div>
</div>`;
}).join('') + '</div>';
}
function updateMetrics() {
document.getElementById('metrics-row').style.display = 'grid';
document.getElementById('m-props').textContent = allProps.length;
document.getElementById('m-active').textContent = allProps.filter(p => p.status === 'active').length;
document.getElementById('m-tasks').textContent = allTasks.length;
document.getElementById('m-open').textContent = allTasks.filter(t => !isDone(t)).length;
}
function renderMain() {
const main = document.getElementById('main');
if (activeTab === 'props') {
main.innerHTML = `
<div class="tabs">
<button class="tab on" onclick="switchTab('props')">Properties</button>
<button class="tab" onclick="switchTab('tasks')">Tasks</button>
</div>
<input class="search" type="text" placeholder="Search properties…" oninput="filterProps(this.value)">
<div class="section-header">
<span class="section-title">All properties</span>
<span class="section-count" id="prop-count">${allProps.length} properties</span>
</div>
<div id="prop-list">${renderProps(allProps)}</div>
`;
} else {
const filtered = taskFilter === 'all' ? allTasks :
taskFilter === 'open' ? allTasks.filter(t => !isDone(t)) :
allTasks.filter(t => taskType(t) === taskFilter);
main.innerHTML = `
<div class="tabs">
<button class="tab" onclick="switchTab('props')">Properties</button>
<button class="tab on" onclick="switchTab('tasks')">Tasks</button>
</div>
<div class="filter-row">
<button class="filter-btn ${taskFilter==='all'?'on':''}" onclick="setFilter('all')">All</button>
<button class="filter-btn ${taskFilter==='open'?'on':''}" onclick="setFilter('open')">Open only</button>
<button class="filter-btn ${taskFilter==='clean'?'on':''}" onclick="setFilter('clean')">Cleans</button>
<button class="filter-btn ${taskFilter==='maint'?'on':''}" onclick="setFilter('maint')">Maintenance</button>
<button class="filter-btn ${taskFilter==='inspect'?'on':''}" onclick="setFilter('inspect')">Inspections</button>
</div>
<div class="section-header">
<span class="section-title">Tasks</span>
<span class="section-count">${filtered.length} tasks</span>
</div>
${renderTasks(filtered)}
`;
}
}
function switchTab(t) { activeTab = t; renderMain(); }
function setFilter(f) { taskFilter = f; renderMain(); }
function filterProps(q) {
const f = allProps.filter(p =>
(p.name||'').toLowerCase().includes(q.toLowerCase()) ||
(p.address1||'').toLowerCase().includes(q.toLowerCase()) ||
(p.city||'').toLowerCase().includes(q.toLowerCase())
);
document.getElementById('prop-list').innerHTML = renderProps(f);
document.getElementById('prop-count').textContent = f.length + ' properties';
}
async function boot() {
const pill = document.getElementById('status-pill');
pill.className = 'status-pill';
pill.innerHTML = '<span class="dot"></span> Connecting…';
document.getElementById('main').innerHTML = '<div class="loader"><div class="spinner"></div><br>Loading your portfolio…</div>';
document.getElementById('metrics-row').style.display = 'none';
try {
const [pd, td] = await Promise.all([
get('/public/inventory/v1/property?limit=100'),
get('/public/inventory/v1/task/?limit=100').catch(() => ({ results: [] }))
]);
allProps = pd.results || pd || [];
const raw = td.results || td.tasks || td || [];
allTasks = Array.isArray(raw) ? raw : [];
pill.className = 'status-pill ok';
pill.innerHTML = '<span class="dot"></span> Live — ' + new Date().toLocaleTimeString('en-AU', { hour: '2-digit', minute: '2-digit' });
updateMetrics();
renderMain();
} catch(e) {
pill.className = 'status-pill err';
pill.innerHTML = '<span class="dot"></span> Error';
document.getElementById('main').innerHTML = `
<div class="error-box">
<strong>Could not connect to Breezeway</strong>
${e.message}<br><br>
If you see "429" — wait 60 seconds and click Refresh.<br>
If you see "Failed to fetch" — check the proxy is deployed.
</div>
`;
}
}
boot();
</script>
</body>
</html>