9 changed files with 4034 additions and 13 deletions
@ -0,0 +1,151 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html lang="en"> |
||||||
|
<head> |
||||||
|
<meta charset="UTF-8"> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||||
|
<title>Client Dashboard - OutsideTheBox</title> |
||||||
|
<link rel="stylesheet" href="/static/style.css"> |
||||||
|
<style> |
||||||
|
.portal-wrap { max-width: 1100px; margin: 2rem auto; padding: 1.25rem; } |
||||||
|
.portal-top { |
||||||
|
display:flex; justify-content:space-between; align-items:center; gap:1rem; flex-wrap:wrap; |
||||||
|
margin-bottom: 1rem; |
||||||
|
} |
||||||
|
.portal-actions a { |
||||||
|
margin-left: 0.75rem; |
||||||
|
text-decoration: underline; |
||||||
|
} |
||||||
|
.summary-grid { |
||||||
|
display:grid; |
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); |
||||||
|
gap:1rem; |
||||||
|
margin: 1rem 0 1.25rem 0; |
||||||
|
} |
||||||
|
.summary-card { |
||||||
|
border: 1px solid rgba(255,255,255,0.16); |
||||||
|
border-radius: 14px; |
||||||
|
padding: 1rem; |
||||||
|
background: rgba(255,255,255,0.03); |
||||||
|
} |
||||||
|
.summary-card h3 { margin-top:0; margin-bottom:0.4rem; } |
||||||
|
table.portal-table { |
||||||
|
width: 100%; |
||||||
|
border-collapse: collapse; |
||||||
|
} |
||||||
|
table.portal-table th, table.portal-table td { |
||||||
|
padding: 0.8rem; |
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.12); |
||||||
|
text-align: left; |
||||||
|
} |
||||||
|
table.portal-table th { |
||||||
|
background: #e9eef7; |
||||||
|
color: #10203f; |
||||||
|
} |
||||||
|
.invoice-link { |
||||||
|
color: inherit; |
||||||
|
text-decoration: underline; |
||||||
|
font-weight: 600; |
||||||
|
} |
||||||
|
.status-badge { |
||||||
|
display: inline-block; |
||||||
|
padding: 0.18rem 0.55rem; |
||||||
|
border-radius: 999px; |
||||||
|
font-size: 0.86rem; |
||||||
|
font-weight: 700; |
||||||
|
} |
||||||
|
.status-paid { |
||||||
|
background: rgba(34, 197, 94, 0.18); |
||||||
|
color: #4ade80; |
||||||
|
} |
||||||
|
.status-pending { |
||||||
|
background: rgba(245, 158, 11, 0.20); |
||||||
|
color: #fbbf24; |
||||||
|
} |
||||||
|
.status-overdue { |
||||||
|
background: rgba(239, 68, 68, 0.18); |
||||||
|
color: #f87171; |
||||||
|
} |
||||||
|
.status-other { |
||||||
|
background: rgba(148, 163, 184, 0.20); |
||||||
|
color: #cbd5e1; |
||||||
|
} |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div class="portal-wrap"> |
||||||
|
<div class="portal-top"> |
||||||
|
<div> |
||||||
|
<h1>Client Dashboard</h1> |
||||||
|
<p>{{ client.company_name or client.contact_name or client.email }}</p> |
||||||
|
</div> |
||||||
|
<div class="portal-actions"> |
||||||
|
<a href="https://outsidethebox.top/">Home</a> |
||||||
|
<a href="mailto:support@outsidethebox.top?subject=Portal%20Support">Contact Support</a> |
||||||
|
<a href="/portal/logout">Logout</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="summary-grid"> |
||||||
|
<div class="summary-card"> |
||||||
|
<h3>Total Invoices</h3> |
||||||
|
<div>{{ invoice_count }}</div> |
||||||
|
</div> |
||||||
|
<div class="summary-card"> |
||||||
|
<h3>Total Outstanding</h3> |
||||||
|
<div>{{ total_outstanding }}</div> |
||||||
|
</div> |
||||||
|
<div class="summary-card"> |
||||||
|
<h3>Total Paid</h3> |
||||||
|
<div>{{ total_paid }}</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<h2>Invoices</h2> |
||||||
|
<table class="portal-table"> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th>Invoice</th> |
||||||
|
<th>Status</th> |
||||||
|
<th>Created</th> |
||||||
|
<th>Total</th> |
||||||
|
<th>Paid</th> |
||||||
|
<th>Outstanding</th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
{% for row in invoices %} |
||||||
|
<tr> |
||||||
|
<td> |
||||||
|
<a class="invoice-link" href="/portal/invoice/{{ row.id }}"> |
||||||
|
{{ row.invoice_number or ("INV-" ~ row.id) }} |
||||||
|
</a> |
||||||
|
</td> |
||||||
|
<td> |
||||||
|
{% set s = (row.status or "")|lower %} |
||||||
|
{% if s == "paid" %} |
||||||
|
<span class="status-badge status-paid">{{ row.status }}</span> |
||||||
|
{% elif s == "pending" %} |
||||||
|
<span class="status-badge status-pending">{{ row.status }}</span> |
||||||
|
{% elif s == "overdue" %} |
||||||
|
<span class="status-badge status-overdue">{{ row.status }}</span> |
||||||
|
{% else %} |
||||||
|
<span class="status-badge status-other">{{ row.status }}</span> |
||||||
|
{% endif %} |
||||||
|
</td> |
||||||
|
<td>{{ row.created_at }}</td> |
||||||
|
<td>{{ row.total_amount }}</td> |
||||||
|
<td>{{ row.amount_paid }}</td> |
||||||
|
<td>{{ row.outstanding }}</td> |
||||||
|
</tr> |
||||||
|
{% else %} |
||||||
|
<tr> |
||||||
|
<td colspan="6">No invoices available.</td> |
||||||
|
</tr> |
||||||
|
{% endfor %} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
|
||||||
|
{% include "footer.html" %} |
||||||
|
</body> |
||||||
|
</html> |
||||||
@ -0,0 +1,166 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html lang="en"> |
||||||
|
<head> |
||||||
|
<meta charset="UTF-8"> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||||
|
<title>Invoice Detail - OutsideTheBox</title> |
||||||
|
<link rel="stylesheet" href="/static/style.css"> |
||||||
|
<style> |
||||||
|
.portal-wrap { max-width: 1100px; margin: 2rem auto; padding: 1.25rem; } |
||||||
|
.portal-top { |
||||||
|
display:flex; justify-content:space-between; align-items:center; gap:1rem; flex-wrap:wrap; |
||||||
|
margin-bottom: 1rem; |
||||||
|
} |
||||||
|
.portal-actions a { |
||||||
|
margin-left: 0.75rem; |
||||||
|
text-decoration: underline; |
||||||
|
} |
||||||
|
.detail-grid { |
||||||
|
display:grid; |
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); |
||||||
|
gap:1rem; |
||||||
|
margin: 1rem 0 1.25rem 0; |
||||||
|
} |
||||||
|
.detail-card { |
||||||
|
border: 1px solid rgba(255,255,255,0.16); |
||||||
|
border-radius: 14px; |
||||||
|
padding: 1rem; |
||||||
|
background: rgba(255,255,255,0.03); |
||||||
|
} |
||||||
|
.detail-card h3 { |
||||||
|
margin-top: 0; |
||||||
|
margin-bottom: 0.4rem; |
||||||
|
} |
||||||
|
table.portal-table { |
||||||
|
width: 100%; |
||||||
|
border-collapse: collapse; |
||||||
|
margin-top: 1rem; |
||||||
|
} |
||||||
|
table.portal-table th, table.portal-table td { |
||||||
|
padding: 0.8rem; |
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.12); |
||||||
|
text-align: left; |
||||||
|
} |
||||||
|
table.portal-table th { |
||||||
|
background: #e9eef7; |
||||||
|
color: #10203f; |
||||||
|
} |
||||||
|
.invoice-actions { |
||||||
|
margin-top: 1rem; |
||||||
|
} |
||||||
|
.invoice-actions a { |
||||||
|
margin-right: 1rem; |
||||||
|
text-decoration: underline; |
||||||
|
} |
||||||
|
.status-badge { |
||||||
|
display: inline-block; |
||||||
|
padding: 0.18rem 0.55rem; |
||||||
|
border-radius: 999px; |
||||||
|
font-size: 0.86rem; |
||||||
|
font-weight: 700; |
||||||
|
} |
||||||
|
.status-paid { |
||||||
|
background: rgba(34, 197, 94, 0.18); |
||||||
|
color: #4ade80; |
||||||
|
} |
||||||
|
.status-pending { |
||||||
|
background: rgba(245, 158, 11, 0.20); |
||||||
|
color: #fbbf24; |
||||||
|
} |
||||||
|
.status-overdue { |
||||||
|
background: rgba(239, 68, 68, 0.18); |
||||||
|
color: #f87171; |
||||||
|
} |
||||||
|
.status-other { |
||||||
|
background: rgba(148, 163, 184, 0.20); |
||||||
|
color: #cbd5e1; |
||||||
|
} |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div class="portal-wrap"> |
||||||
|
<div class="portal-top"> |
||||||
|
<div> |
||||||
|
<h1>Invoice Detail</h1> |
||||||
|
<p>{{ client.company_name or client.contact_name or client.email }}</p> |
||||||
|
</div> |
||||||
|
<div class="portal-actions"> |
||||||
|
<a href="/portal/dashboard">Back to Dashboard</a> |
||||||
|
<a href="https://outsidethebox.top/">Home</a> |
||||||
|
<a href="mailto:support@outsidethebox.top?subject=Portal%20Support">Contact Support</a> |
||||||
|
<a href="/portal/logout">Logout</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="detail-grid"> |
||||||
|
<div class="detail-card"> |
||||||
|
<h3>Invoice</h3> |
||||||
|
<div>{{ invoice.invoice_number or ("INV-" ~ invoice.id) }}</div> |
||||||
|
</div> |
||||||
|
<div class="detail-card"> |
||||||
|
<h3>Status</h3> |
||||||
|
{% set s = (invoice.status or "")|lower %} |
||||||
|
{% if s == "paid" %} |
||||||
|
<span class="status-badge status-paid">{{ invoice.status }}</span> |
||||||
|
{% elif s == "pending" %} |
||||||
|
<span class="status-badge status-pending">{{ invoice.status }}</span> |
||||||
|
{% elif s == "overdue" %} |
||||||
|
<span class="status-badge status-overdue">{{ invoice.status }}</span> |
||||||
|
{% else %} |
||||||
|
<span class="status-badge status-other">{{ invoice.status }}</span> |
||||||
|
{% endif %} |
||||||
|
</div> |
||||||
|
<div class="detail-card"> |
||||||
|
<h3>Created</h3> |
||||||
|
<div>{{ invoice.created_at }}</div> |
||||||
|
</div> |
||||||
|
<div class="detail-card"> |
||||||
|
<h3>Total</h3> |
||||||
|
<div>{{ invoice.total_amount }}</div> |
||||||
|
</div> |
||||||
|
<div class="detail-card"> |
||||||
|
<h3>Paid</h3> |
||||||
|
<div>{{ invoice.amount_paid }}</div> |
||||||
|
</div> |
||||||
|
<div class="detail-card"> |
||||||
|
<h3>Outstanding</h3> |
||||||
|
<div>{{ invoice.outstanding }}</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<h2>Invoice Items</h2> |
||||||
|
<table class="portal-table"> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th>Description</th> |
||||||
|
<th>Qty</th> |
||||||
|
<th>Unit Price</th> |
||||||
|
<th>Line Total</th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
{% for item in items %} |
||||||
|
<tr> |
||||||
|
<td>{{ item.description }}</td> |
||||||
|
<td>{{ item.quantity }}</td> |
||||||
|
<td>{{ item.unit_price }}</td> |
||||||
|
<td>{{ item.line_total }}</td> |
||||||
|
</tr> |
||||||
|
{% else %} |
||||||
|
<tr> |
||||||
|
<td colspan="4">No invoice line items found.</td> |
||||||
|
</tr> |
||||||
|
{% endfor %} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
|
||||||
|
{% if pdf_url %} |
||||||
|
<div class="invoice-actions"> |
||||||
|
<a href="{{ pdf_url }}" target="_blank" rel="noopener noreferrer">Open Invoice PDF</a> |
||||||
|
</div> |
||||||
|
{% endif %} |
||||||
|
</div> |
||||||
|
|
||||||
|
{% include "footer.html" %} |
||||||
|
</body> |
||||||
|
</html> |
||||||
@ -0,0 +1,89 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html lang="en"> |
||||||
|
<head> |
||||||
|
<meta charset="UTF-8"> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||||
|
<title>Client Portal - OutsideTheBox</title> |
||||||
|
<link rel="stylesheet" href="/static/style.css"> |
||||||
|
<style> |
||||||
|
.portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; } |
||||||
|
.portal-card { |
||||||
|
border: 1px solid rgba(255,255,255,0.16); |
||||||
|
border-radius: 16px; |
||||||
|
padding: 1.4rem; |
||||||
|
background: rgba(255,255,255,0.03); |
||||||
|
box-shadow: 0 10px 24px rgba(0,0,0,0.18); |
||||||
|
} |
||||||
|
.portal-card h1 { margin-top: 0; margin-bottom: 0.45rem; } |
||||||
|
.portal-sub { opacity: 0.9; margin-bottom: 1.25rem; } |
||||||
|
.portal-form { display: grid; gap: 0.9rem; } |
||||||
|
.portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; } |
||||||
|
.portal-form input { |
||||||
|
width: 100%; |
||||||
|
padding: 0.8rem 0.9rem; |
||||||
|
border-radius: 10px; |
||||||
|
border: 1px solid rgba(255,255,255,0.18); |
||||||
|
background: rgba(255,255,255,0.05); |
||||||
|
color: inherit; |
||||||
|
box-sizing: border-box; |
||||||
|
} |
||||||
|
.portal-actions { display: flex; gap: 0.8rem; flex-wrap: wrap; margin-top: 0.4rem; } |
||||||
|
.portal-btn { |
||||||
|
display: inline-block; |
||||||
|
padding: 0.8rem 1rem; |
||||||
|
border-radius: 10px; |
||||||
|
text-decoration: none; |
||||||
|
border: 1px solid rgba(255,255,255,0.18); |
||||||
|
background: rgba(255,255,255,0.06); |
||||||
|
color: inherit; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
.portal-note { margin-top: 1rem; opacity: 0.88; font-size: 0.95rem; } |
||||||
|
.portal-links { margin-top: 1rem; } |
||||||
|
.portal-links a { margin-right: 1rem; } |
||||||
|
.portal-msg { |
||||||
|
margin-bottom: 1rem; |
||||||
|
padding: 0.85rem 1rem; |
||||||
|
border-radius: 10px; |
||||||
|
border: 1px solid rgba(255,255,255,0.16); |
||||||
|
background: rgba(255,255,255,0.04); |
||||||
|
} |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div class="portal-wrap"> |
||||||
|
<div class="portal-card"> |
||||||
|
<h1>OutsideTheBox Client Portal</h1> |
||||||
|
<p class="portal-sub">Secure access for invoices, balances, and account information.</p> |
||||||
|
|
||||||
|
{% if portal_message %} |
||||||
|
<div class="portal-msg">{{ portal_message }}</div> |
||||||
|
{% endif %} |
||||||
|
|
||||||
|
<form class="portal-form" method="post" action="/portal/login"> |
||||||
|
<div> |
||||||
|
<label for="email">Email Address</label> |
||||||
|
<input id="email" name="email" type="email" placeholder="client@example.com" value="{{ portal_email or '' }}" required> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div> |
||||||
|
<label for="credential">Access Code or Password</label> |
||||||
|
<input id="credential" name="credential" type="password" placeholder="Enter your one-time access code or password" required> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="portal-actions"> |
||||||
|
<button class="portal-btn" type="submit">Sign In</button> |
||||||
|
<a class="portal-btn" href="https://outsidethebox.top/">Home</a> |
||||||
|
<a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Portal%20Support">Contact Support</a> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
|
||||||
|
<p class="portal-note"> |
||||||
|
First-time users should sign in with the one-time access code provided by OutsideTheBox, then set a password. |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
{% include "footer.html" %} |
||||||
|
</body> |
||||||
|
</html> |
||||||
@ -0,0 +1,75 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html lang="en"> |
||||||
|
<head> |
||||||
|
<meta charset="UTF-8"> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||||
|
<title>Set Portal Password - OutsideTheBox</title> |
||||||
|
<link rel="stylesheet" href="/static/style.css"> |
||||||
|
<style> |
||||||
|
.portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; } |
||||||
|
.portal-card { |
||||||
|
border: 1px solid rgba(255,255,255,0.16); |
||||||
|
border-radius: 16px; |
||||||
|
padding: 1.4rem; |
||||||
|
background: rgba(255,255,255,0.03); |
||||||
|
box-shadow: 0 10px 24px rgba(0,0,0,0.18); |
||||||
|
} |
||||||
|
.portal-form { display: grid; gap: 0.9rem; } |
||||||
|
.portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; } |
||||||
|
.portal-form input { |
||||||
|
width: 100%; |
||||||
|
padding: 0.8rem 0.9rem; |
||||||
|
border-radius: 10px; |
||||||
|
border: 1px solid rgba(255,255,255,0.18); |
||||||
|
background: rgba(255,255,255,0.05); |
||||||
|
color: inherit; |
||||||
|
box-sizing: border-box; |
||||||
|
} |
||||||
|
.portal-btn { |
||||||
|
display: inline-block; |
||||||
|
padding: 0.8rem 1rem; |
||||||
|
border-radius: 10px; |
||||||
|
text-decoration: none; |
||||||
|
border: 1px solid rgba(255,255,255,0.18); |
||||||
|
background: rgba(255,255,255,0.06); |
||||||
|
color: inherit; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
.portal-msg { |
||||||
|
margin-bottom: 1rem; |
||||||
|
padding: 0.85rem 1rem; |
||||||
|
border-radius: 10px; |
||||||
|
border: 1px solid rgba(255,255,255,0.16); |
||||||
|
background: rgba(255,255,255,0.04); |
||||||
|
} |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div class="portal-wrap"> |
||||||
|
<div class="portal-card"> |
||||||
|
<h1>Create Your Portal Password</h1> |
||||||
|
<p>Welcome, {{ client_name }}. Your one-time access code worked. Please create a password for future logins.</p> |
||||||
|
|
||||||
|
{% if portal_message %} |
||||||
|
<div class="portal-msg">{{ portal_message }}</div> |
||||||
|
{% endif %} |
||||||
|
|
||||||
|
<form class="portal-form" method="post" action="/portal/set-password"> |
||||||
|
<div> |
||||||
|
<label for="password">New Password</label> |
||||||
|
<input id="password" name="password" type="password" required> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<label for="password2">Confirm Password</label> |
||||||
|
<input id="password2" name="password2" type="password" required> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<button class="portal-btn" type="submit">Set Password</button> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
{% include "footer.html" %} |
||||||
|
</body> |
||||||
|
</html> |
||||||
Loading…
Reference in new issue