28 changed files with 692 additions and 14710 deletions
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,59 @@ |
|||||||
|
#!/usr/bin/env bash |
||||||
|
set -e |
||||||
|
|
||||||
|
VERSION="v0.3.0" |
||||||
|
DATE="$(date +%Y-%m-%d)" |
||||||
|
STAMP="$(date +%Y%m%d-%H%M%S)" |
||||||
|
|
||||||
|
echo "===== CLEAN TEMP FILES =====" |
||||||
|
find . -type f \( -name "*.bak*" -o -name "patch*.sh" \) |
||||||
|
|
||||||
|
read -p "Delete these files? Type YES: " CONFIRM |
||||||
|
if [ "$CONFIRM" = "YES" ]; then |
||||||
|
find . -type f \( -name "*.bak*" -o -name "patch*.sh" \) -delete |
||||||
|
fi |
||||||
|
|
||||||
|
echo "===== SET VERSION =====" |
||||||
|
echo "$VERSION" > VERSION |
||||||
|
|
||||||
|
echo "===== UPDATE PROJECT_STATE.md =====" |
||||||
|
cat <<STATE >> PROJECT_STATE.md |
||||||
|
|
||||||
|
## $VERSION - $DATE |
||||||
|
- Portal onboarding flow upgraded |
||||||
|
- Email invites now include clickable activation link |
||||||
|
- /portal/set-password now supports direct email+code login |
||||||
|
- Auto session creation from invite link |
||||||
|
- Improved UX: no manual code entry required |
||||||
|
- Portal onboarding now production-ready |
||||||
|
|
||||||
|
STATE |
||||||
|
|
||||||
|
echo "===== UPDATE README.md =====" |
||||||
|
sed -i "1i\\ |
||||||
|
## $VERSION ($DATE)\\ |
||||||
|
- Clickable portal invite links\\ |
||||||
|
- Direct account activation from email\\ |
||||||
|
- Improved onboarding UX\\ |
||||||
|
" README.md |
||||||
|
|
||||||
|
echo "===== VERIFY PYTHON =====" |
||||||
|
python3 -m py_compile backend/app.py |
||||||
|
|
||||||
|
echo "===== CREATE FULL BACKUP =====" |
||||||
|
zip -r "/home/def/backuphere/otb_billing-$VERSION-$STAMP.zip" . >/dev/null |
||||||
|
|
||||||
|
echo "===== GIT ADD =====" |
||||||
|
git add . |
||||||
|
|
||||||
|
echo "===== GIT COMMIT =====" |
||||||
|
git commit -m "Release $VERSION - Portal onboarding flow complete (email link activation)" |
||||||
|
|
||||||
|
echo "===== GIT TAG =====" |
||||||
|
git tag "$VERSION" |
||||||
|
|
||||||
|
echo "===== GIT PUSH =====" |
||||||
|
git push |
||||||
|
git push origin "$VERSION" |
||||||
|
|
||||||
|
echo "===== DONE =====" |
||||||
@ -1,70 +0,0 @@ |
|||||||
cd /home/def/otb_billing || exit 1 |
|
||||||
|
|
||||||
STAMP=$(date +%Y%m%d-%H%M%S) |
|
||||||
NEWVER="v0.6.1" |
|
||||||
|
|
||||||
echo "===== backup full project =====" |
|
||||||
mkdir -p /home/def/backuphere |
|
||||||
cd /home/def |
|
||||||
tar -czf /home/def/backuphere/otb_billing-${NEWVER}-${STAMP}.tar.gz otb_billing |
|
||||||
|
|
||||||
echo "===== update VERSION =====" |
|
||||||
cd /home/def/otb_billing || exit 1 |
|
||||||
echo "${NEWVER}" > VERSION |
|
||||||
|
|
||||||
echo "===== update README.md =====" |
|
||||||
cp README.md /home/def/backuphere/README.md.${STAMP}.bak |
|
||||||
|
|
||||||
cat > /tmp/readme_entry.txt <<EOF |
|
||||||
## ${NEWVER} - $(date +%Y-%m-%d) |
|
||||||
|
|
||||||
### Added |
|
||||||
- Service Templates system (standalone pricing catalog) |
|
||||||
- Admin UI for managing reusable service pricing |
|
||||||
- Template selector on service create/edit pages (auto-fill fields) |
|
||||||
|
|
||||||
### Notes |
|
||||||
- Templates are not yet linked to services via template_id (planned) |
|
||||||
- Setup amount stored in templates for future invoice integration |
|
||||||
- Maintains compatibility with existing services table |
|
||||||
|
|
||||||
--- |
|
||||||
EOF |
|
||||||
|
|
||||||
cat /tmp/readme_entry.txt README.md > README.md.new |
|
||||||
mv README.md.new README.md |
|
||||||
|
|
||||||
echo "===== update PROJECT_STATE.md =====" |
|
||||||
cp PROJECT_STATE.md /home/def/backuphere/PROJECT_STATE.md.${STAMP}.bak |
|
||||||
|
|
||||||
cat > /tmp/state_entry.txt <<EOF |
|
||||||
## ${NEWVER} - Service Templates Phase 1 |
|
||||||
|
|
||||||
- Added service_templates table |
|
||||||
- Implemented admin CRUD routes in app.py |
|
||||||
- Added templates UI pages |
|
||||||
- Integrated template selection into services/new and services/edit |
|
||||||
- Auto-fill JS implemented for template selection |
|
||||||
|
|
||||||
Status: FUNCTIONAL |
|
||||||
Next: link templates to services + invoice integration |
|
||||||
|
|
||||||
--- |
|
||||||
EOF |
|
||||||
|
|
||||||
cat /tmp/state_entry.txt PROJECT_STATE.md > PROJECT_STATE.md.new |
|
||||||
mv PROJECT_STATE.md.new PROJECT_STATE.md |
|
||||||
|
|
||||||
echo "===== git status =====" |
|
||||||
git status |
|
||||||
|
|
||||||
echo "===== commit =====" |
|
||||||
git add . |
|
||||||
git commit -m "bump ${NEWVER} - add service templates system" |
|
||||||
|
|
||||||
echo "===== push =====" |
|
||||||
git push |
|
||||||
|
|
||||||
echo "===== done =====" |
|
||||||
echo "Backup saved at:" |
|
||||||
ls -lh /home/def/backuphere/otb_billing-${NEWVER}-${STAMP}.tar.gz |
|
||||||
@ -1,110 +0,0 @@ |
|||||||
#!/usr/bin/env python3 |
|
||||||
|
|
||||||
import sys |
|
||||||
from datetime import datetime, timedelta |
|
||||||
|
|
||||||
sys.path.append("/home/def/otb_billing/backend") |
|
||||||
|
|
||||||
from app import get_db_connection, send_configured_email |
|
||||||
|
|
||||||
REMINDER_DAYS = 7 |
|
||||||
OVERDUE_DAYS = 14 |
|
||||||
|
|
||||||
|
|
||||||
def main(): |
|
||||||
conn = get_db_connection() |
|
||||||
cursor = conn.cursor(dictionary=True) |
|
||||||
|
|
||||||
now = datetime.utcnow() |
|
||||||
|
|
||||||
cursor.execute(""" |
|
||||||
SELECT |
|
||||||
i.id, |
|
||||||
i.invoice_number, |
|
||||||
i.created_at, |
|
||||||
i.total, |
|
||||||
i.client_id, |
|
||||||
c.email, |
|
||||||
c.company_name, |
|
||||||
c.contact_name |
|
||||||
FROM invoices i |
|
||||||
JOIN clients c ON c.id = i.client_id |
|
||||||
WHERE i.status IN ('pending','sent') |
|
||||||
""") |
|
||||||
|
|
||||||
invoices = cursor.fetchall() |
|
||||||
|
|
||||||
for inv in invoices: |
|
||||||
age = (now - inv["created_at"]).days |
|
||||||
|
|
||||||
email = inv["email"] |
|
||||||
if not email: |
|
||||||
continue |
|
||||||
|
|
||||||
name = inv.get("contact_name") or inv.get("company_name") or "Client" |
|
||||||
|
|
||||||
portal_url = f"https://portal.outsidethebox.top/portal/invoice/{inv['id']}" |
|
||||||
|
|
||||||
if age >= OVERDUE_DAYS: |
|
||||||
|
|
||||||
subject = f"Invoice {inv['invoice_number']} is overdue" |
|
||||||
|
|
||||||
body = f""" |
|
||||||
Hello {name}, |
|
||||||
|
|
||||||
Invoice {inv['invoice_number']} is now overdue. |
|
||||||
|
|
||||||
Amount Due: |
|
||||||
{inv['total']} |
|
||||||
|
|
||||||
View invoice: |
|
||||||
{portal_url} |
|
||||||
|
|
||||||
Please arrange payment at your earliest convenience. |
|
||||||
|
|
||||||
OutsideTheBox |
|
||||||
""" |
|
||||||
|
|
||||||
send_configured_email( |
|
||||||
to_email=email, |
|
||||||
subject=subject, |
|
||||||
body=body, |
|
||||||
attachments=None, |
|
||||||
email_type="invoice_overdue", |
|
||||||
invoice_id=inv["id"] |
|
||||||
) |
|
||||||
|
|
||||||
elif age >= REMINDER_DAYS: |
|
||||||
|
|
||||||
subject = f"Invoice {inv['invoice_number']} reminder" |
|
||||||
|
|
||||||
body = f""" |
|
||||||
Hello {name}, |
|
||||||
|
|
||||||
This is a reminder that invoice {inv['invoice_number']} is still outstanding. |
|
||||||
|
|
||||||
Amount Due: |
|
||||||
{inv['total']} |
|
||||||
|
|
||||||
View invoice: |
|
||||||
{portal_url} |
|
||||||
|
|
||||||
Thank you. |
|
||||||
|
|
||||||
OutsideTheBox |
|
||||||
""" |
|
||||||
|
|
||||||
send_configured_email( |
|
||||||
to_email=email, |
|
||||||
subject=subject, |
|
||||||
body=body, |
|
||||||
attachments=None, |
|
||||||
email_type="invoice_reminder", |
|
||||||
invoice_id=inv["id"] |
|
||||||
) |
|
||||||
|
|
||||||
conn.close() |
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__": |
|
||||||
main() |
|
||||||
@ -1,117 +0,0 @@ |
|||||||
#!/usr/bin/env python3 |
|
||||||
|
|
||||||
|
|
||||||
import sys |
|
||||||
import os |
|
||||||
from datetime import datetime, timedelta |
|
||||||
from dotenv import load_dotenv |
|
||||||
|
|
||||||
# load same environment config as Flask |
|
||||||
load_dotenv("/home/def/otb_billing/.env") |
|
||||||
|
|
||||||
sys.path.append("/home/def/otb_billing/backend") |
|
||||||
|
|
||||||
from app import get_db_connection, send_configured_email |
|
||||||
|
|
||||||
|
|
||||||
REMINDER_DAYS = 7 |
|
||||||
OVERDUE_DAYS = 14 |
|
||||||
|
|
||||||
|
|
||||||
def main(): |
|
||||||
conn = get_db_connection() |
|
||||||
cursor = conn.cursor(dictionary=True) |
|
||||||
|
|
||||||
now = datetime.utcnow() |
|
||||||
|
|
||||||
cursor.execute(""" |
|
||||||
SELECT |
|
||||||
i.id, |
|
||||||
i.invoice_number, |
|
||||||
i.created_at, |
|
||||||
i.total, |
|
||||||
i.client_id, |
|
||||||
c.email, |
|
||||||
c.company_name, |
|
||||||
c.contact_name |
|
||||||
FROM invoices i |
|
||||||
JOIN clients c ON c.id = i.client_id |
|
||||||
WHERE i.status IN ('pending','sent') |
|
||||||
""") |
|
||||||
|
|
||||||
invoices = cursor.fetchall() |
|
||||||
|
|
||||||
for inv in invoices: |
|
||||||
age = (now - inv["created_at"]).days |
|
||||||
|
|
||||||
email = inv["email"] |
|
||||||
if not email: |
|
||||||
continue |
|
||||||
|
|
||||||
name = inv.get("contact_name") or inv.get("company_name") or "Client" |
|
||||||
|
|
||||||
portal_url = f"https://portal.outsidethebox.top/portal/invoice/{inv['id']}" |
|
||||||
|
|
||||||
if age >= OVERDUE_DAYS: |
|
||||||
|
|
||||||
subject = f"Invoice {inv['invoice_number']} is overdue" |
|
||||||
|
|
||||||
body = f""" |
|
||||||
Hello {name}, |
|
||||||
|
|
||||||
Invoice {inv['invoice_number']} is now overdue. |
|
||||||
|
|
||||||
Amount Due: |
|
||||||
{inv['total']} |
|
||||||
|
|
||||||
View invoice: |
|
||||||
{portal_url} |
|
||||||
|
|
||||||
Please arrange payment at your earliest convenience. |
|
||||||
|
|
||||||
OutsideTheBox |
|
||||||
""" |
|
||||||
|
|
||||||
send_configured_email( |
|
||||||
to_email=email, |
|
||||||
subject=subject, |
|
||||||
body=body, |
|
||||||
attachments=None, |
|
||||||
email_type="invoice_overdue", |
|
||||||
invoice_id=inv["id"] |
|
||||||
) |
|
||||||
|
|
||||||
elif age >= REMINDER_DAYS: |
|
||||||
|
|
||||||
subject = f"Invoice {inv['invoice_number']} reminder" |
|
||||||
|
|
||||||
body = f""" |
|
||||||
Hello {name}, |
|
||||||
|
|
||||||
This is a reminder that invoice {inv['invoice_number']} is still outstanding. |
|
||||||
|
|
||||||
Amount Due: |
|
||||||
{inv['total']} |
|
||||||
|
|
||||||
View invoice: |
|
||||||
{portal_url} |
|
||||||
|
|
||||||
Thank you. |
|
||||||
|
|
||||||
OutsideTheBox |
|
||||||
""" |
|
||||||
|
|
||||||
send_configured_email( |
|
||||||
to_email=email, |
|
||||||
subject=subject, |
|
||||||
body=body, |
|
||||||
attachments=None, |
|
||||||
email_type="invoice_reminder", |
|
||||||
invoice_id=inv["id"] |
|
||||||
) |
|
||||||
|
|
||||||
conn.close() |
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__": |
|
||||||
main() |
|
||||||
@ -1,116 +0,0 @@ |
|||||||
#!/usr/bin/env python3 |
|
||||||
|
|
||||||
|
|
||||||
import sys |
|
||||||
import os |
|
||||||
from datetime import datetime, timedelta |
|
||||||
from dotenv import load_dotenv |
|
||||||
|
|
||||||
# load same environment config as Flask |
|
||||||
load_dotenv("/home/def/otb_billing/.env") |
|
||||||
|
|
||||||
sys.path.append("/home/def/otb_billing/backend") |
|
||||||
|
|
||||||
from app import get_db_connection, send_configured_email |
|
||||||
|
|
||||||
|
|
||||||
REMINDER_DAYS = 7 |
|
||||||
OVERDUE_DAYS = 14 |
|
||||||
|
|
||||||
|
|
||||||
def main(): |
|
||||||
conn = get_db_connection() |
|
||||||
cursor = conn.cursor(dictionary=True) |
|
||||||
|
|
||||||
now = datetime.utcnow() |
|
||||||
|
|
||||||
cursor.execute(""" |
|
||||||
SELECT |
|
||||||
i.id, |
|
||||||
i.invoice_number, |
|
||||||
i.created_at, |
|
||||||
i.client_id, |
|
||||||
c.email, |
|
||||||
c.company_name, |
|
||||||
c.contact_name |
|
||||||
FROM invoices i |
|
||||||
JOIN clients c ON c.id = i.client_id |
|
||||||
WHERE i.status IN ('pending','sent') |
|
||||||
""") |
|
||||||
|
|
||||||
invoices = cursor.fetchall() |
|
||||||
|
|
||||||
for inv in invoices: |
|
||||||
age = (now - inv["created_at"]).days |
|
||||||
|
|
||||||
email = inv["email"] |
|
||||||
if not email: |
|
||||||
continue |
|
||||||
|
|
||||||
name = inv.get("contact_name") or inv.get("company_name") or "Client" |
|
||||||
|
|
||||||
portal_url = f"https://portal.outsidethebox.top/portal/invoice/{inv['id']}" |
|
||||||
|
|
||||||
if age >= OVERDUE_DAYS: |
|
||||||
|
|
||||||
subject = f"Invoice {inv['invoice_number']} is overdue" |
|
||||||
|
|
||||||
body = f""" |
|
||||||
Hello {name}, |
|
||||||
|
|
||||||
Invoice {inv['invoice_number']} is now overdue. |
|
||||||
|
|
||||||
Amount Due: |
|
||||||
Invoice amount available in portal |
|
||||||
|
|
||||||
View invoice: |
|
||||||
{portal_url} |
|
||||||
|
|
||||||
Please arrange payment at your earliest convenience. |
|
||||||
|
|
||||||
OutsideTheBox |
|
||||||
""" |
|
||||||
|
|
||||||
send_configured_email( |
|
||||||
to_email=email, |
|
||||||
subject=subject, |
|
||||||
body=body, |
|
||||||
attachments=None, |
|
||||||
email_type="invoice_overdue", |
|
||||||
invoice_id=inv["id"] |
|
||||||
) |
|
||||||
|
|
||||||
elif age >= REMINDER_DAYS: |
|
||||||
|
|
||||||
subject = f"Invoice {inv['invoice_number']} reminder" |
|
||||||
|
|
||||||
body = f""" |
|
||||||
Hello {name}, |
|
||||||
|
|
||||||
This is a reminder that invoice {inv['invoice_number']} is still outstanding. |
|
||||||
|
|
||||||
Amount Due: |
|
||||||
Invoice amount available in portal |
|
||||||
|
|
||||||
View invoice: |
|
||||||
{portal_url} |
|
||||||
|
|
||||||
Thank you. |
|
||||||
|
|
||||||
OutsideTheBox |
|
||||||
""" |
|
||||||
|
|
||||||
send_configured_email( |
|
||||||
to_email=email, |
|
||||||
subject=subject, |
|
||||||
body=body, |
|
||||||
attachments=None, |
|
||||||
email_type="invoice_reminder", |
|
||||||
invoice_id=inv["id"] |
|
||||||
) |
|
||||||
|
|
||||||
conn.close() |
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__": |
|
||||||
main() |
|
||||||
@ -0,0 +1,83 @@ |
|||||||
|
{% extends "portal_base.html" %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
|
||||||
|
<div class="card"> |
||||||
|
<h2>Outsidethebox.top Service Agreement (v1.1)</h2> |
||||||
|
|
||||||
|
<p>You must read and accept this agreement before using the portal.</p> |
||||||
|
|
||||||
|
<h3>1. Nature of Services</h3> |
||||||
|
<p> |
||||||
|
Outsidethebox.top provides a range of digital services including, but not limited to: |
||||||
|
</p> |
||||||
|
<ul> |
||||||
|
<li>Data storage and file backup services</li> |
||||||
|
<li>Video and image processing and conversion</li> |
||||||
|
<li>GPS and location-based tracking applications</li> |
||||||
|
<li>Web hosting and infrastructure services</li> |
||||||
|
<li>Custom tools and SaaS-style applications</li> |
||||||
|
</ul> |
||||||
|
|
||||||
|
<p> |
||||||
|
These services may involve the collection, storage, processing, and transmission of user data as required for proper functionality. |
||||||
|
</p> |
||||||
|
|
||||||
|
<h3>2. Use of Services</h3> |
||||||
|
<p> |
||||||
|
Each service provided by Outsidethebox.top may have specific operational requirements, including data handling, processing, or tracking capabilities. |
||||||
|
</p> |
||||||
|
|
||||||
|
<p> |
||||||
|
By using any service, you acknowledge and accept the requirements necessary for that service to function. |
||||||
|
</p> |
||||||
|
|
||||||
|
<p> |
||||||
|
If you do not agree with the requirements of a specific service, you must not use that service. |
||||||
|
</p> |
||||||
|
|
||||||
|
<h3>3. User Responsibility</h3> |
||||||
|
<p> |
||||||
|
You are responsible for: |
||||||
|
</p> |
||||||
|
<ul> |
||||||
|
<li>Understanding the purpose and function of each service you use</li> |
||||||
|
<li>Ensuring you have appropriate authorization for any data you upload or process</li> |
||||||
|
<li>Choosing not to use services that conflict with your privacy or operational preferences</li> |
||||||
|
</ul> |
||||||
|
|
||||||
|
<h3>4. Data Handling</h3> |
||||||
|
<p> |
||||||
|
Outsidethebox.top systems may store, process, and transmit data as required to deliver services. This may include files, metadata, and service-related information. |
||||||
|
</p> |
||||||
|
|
||||||
|
<h3>5. Service-Specific Agreements</h3> |
||||||
|
<p> |
||||||
|
Certain services may present additional agreements or notices before use. These must be accepted before accessing those services. |
||||||
|
</p> |
||||||
|
|
||||||
|
<h3>6. Acceptance</h3> |
||||||
|
<p> |
||||||
|
By proceeding, you confirm that: |
||||||
|
</p> |
||||||
|
|
||||||
|
<ul> |
||||||
|
<li>You understand the nature of the services provided</li> |
||||||
|
<li>You accept that services may require data processing or tracking to function</li> |
||||||
|
<li>You will not use services whose requirements you do not accept</li> |
||||||
|
</ul> |
||||||
|
|
||||||
|
<form method="post"> |
||||||
|
<label> |
||||||
|
<input type="checkbox" name="accept" required> |
||||||
|
I understand and accept the Outsidethebox.top account and service requirements. |
||||||
|
</label> |
||||||
|
|
||||||
|
<br><br> |
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">Continue</button> |
||||||
|
<a href="/logout" class="btn">Logout</a> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
|
||||||
|
{% endblock %} |
||||||
@ -1,209 +0,0 @@ |
|||||||
<!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/css/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> |
|
||||||
<link rel="icon" type="image/png" href="/static/favicon.png"> |
|
||||||
</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="/portal/invoice/{{ invoice.id }}/pdf" target="_blank" rel="noopener noreferrer">Open Invoice PDF</a> |
|
||||||
</div> |
|
||||||
{% endif %} |
|
||||||
</div> |
|
||||||
|
|
||||||
{% include "footer.html" %} |
|
||||||
|
|
||||||
<hr> |
|
||||||
|
|
||||||
<h3>Payment Instructions</h3> |
|
||||||
|
|
||||||
<p><strong>Interac e-Transfer</strong><br> |
|
||||||
Send payment to:<br> |
|
||||||
payment@outsidethebox.top<br> |
|
||||||
Reference: Invoice {{ invoice.invoice_number or ("INV-" ~ invoice.id) }} |
|
||||||
</p> |
|
||||||
|
|
||||||
<p><strong>Credit Card (Square)</strong><br> |
|
||||||
|
|
||||||
<br><br> |
|
||||||
|
|
||||||
<p><strong>Credit Card (Square)</strong></p> |
|
||||||
|
|
||||||
<a href="https://square.link/u/H0cimZku" target="_blank" |
|
||||||
style=" |
|
||||||
display:inline-block; |
|
||||||
padding:12px 18px; |
|
||||||
background:#28a745; |
|
||||||
color:white; |
|
||||||
text-decoration:none; |
|
||||||
border-radius:6px; |
|
||||||
font-weight:bold; |
|
||||||
"> |
|
||||||
Pay with Card (Square) |
|
||||||
</a> |
|
||||||
|
|
||||||
<p style="font-size:12px;color:#666;margin-top:6px;"> |
|
||||||
You will be redirected to Square's secure payment page. |
|
||||||
Please include your invoice number in the payment note. |
|
||||||
</p> |
|
||||||
</p> |
|
||||||
|
|
||||||
<p> |
|
||||||
If you have questions please contact |
|
||||||
<a href="mailto:support@outsidethebox.top">support@outsidethebox.top</a> |
|
||||||
</p> |
|
||||||
|
|
||||||
|
|
||||||
</body> |
|
||||||
</html> |
|
||||||
@ -0,0 +1,46 @@ |
|||||||
|
{% extends "portal_base.html" %} |
||||||
|
|
||||||
|
{% block title %}Create Portal Access - OutsideTheBox{% endblock %} |
||||||
|
|
||||||
|
{% block portal_content %} |
||||||
|
<div class="portal-card"> |
||||||
|
<h1>Request Portal Access</h1> |
||||||
|
<p>Enter your details and we will email you a one-time access code. After first login, you will set your password.</p> |
||||||
|
|
||||||
|
{% if error %} |
||||||
|
<div class="portal-alert portal-alert-error">{{ error }}</div> |
||||||
|
{% endif %} |
||||||
|
|
||||||
|
{% if message %} |
||||||
|
<div class="portal-alert portal-alert-success">{{ message }}</div> |
||||||
|
<p><a href="/portal">Return to portal login</a></p> |
||||||
|
{% else %} |
||||||
|
<form method="post" action="/portal/register"> |
||||||
|
<div style="margin-bottom: 14px;"> |
||||||
|
<label for="email">Email Address *</label><br> |
||||||
|
<input id="email" type="email" name="email" value="{{ form_email or '' }}" required style="width: 100%; max-width: 640px;"> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div style="margin-bottom: 14px;"> |
||||||
|
<label for="company_name">Company / Organization</label><br> |
||||||
|
<input id="company_name" type="text" name="company_name" value="{{ form_company or '' }}" style="width: 100%; max-width: 640px;"> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div style="margin-bottom: 14px;"> |
||||||
|
<label for="contact_name">Your Name</label><br> |
||||||
|
<input id="contact_name" type="text" name="contact_name" value="{{ form_contact or '' }}" style="width: 100%; max-width: 640px;"> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div style="margin-bottom: 14px;"> |
||||||
|
<label for="note">What are you requesting?</label><br> |
||||||
|
<textarea id="note" name="note" rows="5" style="width: 100%; max-width: 640px;">{{ form_note or '' }}</textarea> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="portal-actions"> |
||||||
|
<button type="submit">Email My Access Code</button> |
||||||
|
<a class="portal-button-secondary" href="/portal">Back to Login</a> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
{% endif %} |
||||||
|
</div> |
||||||
|
{% endblock %} |
||||||
@ -1,173 +0,0 @@ |
|||||||
<!doctype html> |
|
||||||
<html> |
|
||||||
<head> |
|
||||||
<title>Settings</title> |
|
||||||
<style> |
|
||||||
body { font-family: Arial, sans-serif; } |
|
||||||
.form-grid { |
|
||||||
display: grid; |
|
||||||
grid-template-columns: 1fr 1fr; |
|
||||||
gap: 24px; |
|
||||||
max-width: 1100px; |
|
||||||
} |
|
||||||
.card { |
|
||||||
border: 1px solid #ccc; |
|
||||||
padding: 16px; |
|
||||||
} |
|
||||||
.card h2 { |
|
||||||
margin-top: 0; |
|
||||||
} |
|
||||||
input[type="text"], |
|
||||||
input[type="email"], |
|
||||||
input[type="password"], |
|
||||||
input[type="number"], |
|
||||||
textarea, |
|
||||||
select { |
|
||||||
width: 100%; |
|
||||||
box-sizing: border-box; |
|
||||||
margin-top: 4px; |
|
||||||
margin-bottom: 12px; |
|
||||||
padding: 8px; |
|
||||||
} |
|
||||||
textarea { min-height: 90px; } |
|
||||||
.checkbox-row { |
|
||||||
margin: 8px 0 14px 0; |
|
||||||
} |
|
||||||
.save-row { |
|
||||||
margin-top: 18px; |
|
||||||
} |
|
||||||
</style> |
|
||||||
</head> |
|
||||||
<body> |
|
||||||
|
|
||||||
<h1>Settings / Config</h1> |
|
||||||
|
|
||||||
<p><a href="/">Home</a></p> |
|
||||||
|
|
||||||
<form method="post"> |
|
||||||
<div class="form-grid"> |
|
||||||
<div class="card"> |
|
||||||
<h2>Business Identity</h2> |
|
||||||
|
|
||||||
Business Name<br> |
|
||||||
Business Logo URL<br> |
|
||||||
<input type="text" name="business_logo_url" value="{{ settings.business_logo_url }}"><br> |
|
||||||
<small>Example: /static/logo.png or https://site.com/logo.png</small><br><br> |
|
||||||
|
|
||||||
<input type="text" name="business_name" value="{{ settings.business_name }}"><br> |
|
||||||
|
|
||||||
Slogan / Tagline<br> |
|
||||||
<input type="text" name="business_tagline" value="{{ settings.business_tagline }}"><br> |
|
||||||
|
|
||||||
Business Email<br> |
|
||||||
<input type="email" name="business_email" value="{{ settings.business_email }}"><br> |
|
||||||
|
|
||||||
Business Phone<br> |
|
||||||
<input type="text" name="business_phone" value="{{ settings.business_phone }}"><br> |
|
||||||
|
|
||||||
Business Address<br> |
|
||||||
<textarea name="business_address">{{ settings.business_address }}</textarea><br> |
|
||||||
|
|
||||||
Website<br> |
|
||||||
<input type="text" name="business_website" value="{{ settings.business_website }}"><br> |
|
||||||
|
|
||||||
Business Number / Registration Number<br> |
|
||||||
<input type="text" name="business_number" value="{{ settings.business_number }}"><br> |
|
||||||
|
|
||||||
Default Currency<br> |
|
||||||
<select name="default_currency"> |
|
||||||
<option value="CAD" {% if settings.default_currency == 'CAD' %}selected{% endif %}>CAD</option> |
|
||||||
<option value="USD" {% if settings.default_currency == 'USD' %}selected{% endif %}>USD</option> |
|
||||||
<option value="ETHO" {% if settings.default_currency == 'ETHO' %}selected{% endif %}>ETHO</option> |
|
||||||
<option value="EGAZ" {% if settings.default_currency == 'EGAZ' %}selected{% endif %}>EGAZ</option> |
|
||||||
<option value="ALT" {% if settings.default_currency == 'ALT' %}selected{% endif %}>ALT</option> |
|
||||||
</select> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="card"> |
|
||||||
<h2>Tax Settings</h2> |
|
||||||
|
|
||||||
Local Country<br> |
|
||||||
<input type="text" name="local_country" value="{{ settings.local_country }}"><br> |
|
||||||
|
|
||||||
Tax Label<br> |
|
||||||
<input type="text" name="tax_label" value="{{ settings.tax_label }}"><br> |
|
||||||
|
|
||||||
Tax Rate (%)<br> |
|
||||||
<input type="number" step="0.01" name="tax_rate" value="{{ settings.tax_rate }}"><br> |
|
||||||
|
|
||||||
Tax Number<br> |
|
||||||
<input type="text" name="tax_number" value="{{ settings.tax_number }}"><br> |
|
||||||
|
|
||||||
<div class="checkbox-row"> |
|
||||||
<label> |
|
||||||
<input type="checkbox" name="apply_local_tax_only" value="1" {% if settings.apply_local_tax_only == '1' %}checked{% endif %}> |
|
||||||
Apply tax only to local clients |
|
||||||
</label> |
|
||||||
</div> |
|
||||||
|
|
||||||
Payment Terms<br> |
|
||||||
<textarea name="payment_terms">{{ settings.payment_terms }}</textarea><br> |
|
||||||
|
|
||||||
Invoice Footer<br> |
|
||||||
<textarea name="invoice_footer">{{ settings.invoice_footer }}</textarea><br> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="card"> |
|
||||||
<h2>Email / SMTP</h2> |
|
||||||
|
|
||||||
SMTP Host<br> |
|
||||||
<input type="text" name="smtp_host" value="{{ settings.smtp_host }}"><br> |
|
||||||
|
|
||||||
SMTP Port<br> |
|
||||||
<input type="number" name="smtp_port" value="{{ settings.smtp_port }}"><br> |
|
||||||
|
|
||||||
SMTP Username<br> |
|
||||||
<input type="text" name="smtp_user" value="{{ settings.smtp_user }}"><br> |
|
||||||
|
|
||||||
SMTP Password<br> |
|
||||||
<input type="password" name="smtp_pass" value="{{ settings.smtp_pass }}"><br> |
|
||||||
|
|
||||||
From Email<br> |
|
||||||
<input type="email" name="smtp_from_email" value="{{ settings.smtp_from_email }}"><br> |
|
||||||
|
|
||||||
From Name<br> |
|
||||||
<input type="text" name="smtp_from_name" value="{{ settings.smtp_from_name }}"><br> |
|
||||||
|
|
||||||
<div class="checkbox-row"> |
|
||||||
<label> |
|
||||||
<input type="checkbox" name="smtp_use_tls" value="1" {% if settings.smtp_use_tls == '1' %}checked{% endif %}> |
|
||||||
Use TLS |
|
||||||
</label> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="checkbox-row"> |
|
||||||
<label> |
|
||||||
<input type="checkbox" name="smtp_use_ssl" value="1" {% if settings.smtp_use_ssl == '1' %}checked{% endif %}> |
|
||||||
Use SSL |
|
||||||
</label> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="card"> |
|
||||||
<h2>Notes</h2> |
|
||||||
<p> |
|
||||||
These settings become the identity and delivery configuration for this installation. |
|
||||||
</p> |
|
||||||
<p> |
|
||||||
Email sending is not wired yet, but these SMTP settings are stored now so the next step can use them. |
|
||||||
</p> |
|
||||||
<p> |
|
||||||
Tax settings are also stored now so invoice and automation logic can use them later. |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="save-row"> |
|
||||||
<button type="submit">Save Settings</button> |
|
||||||
</div> |
|
||||||
</form> |
|
||||||
|
|
||||||
{% include "footer.html" %} |
|
||||||
</body> |
|
||||||
</html> |
|
||||||
Loading…
Reference in new issue