Initial Commit
This commit is contained in:
42
.gitignore
vendored
Normal file
42
.gitignore
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Virtual environment
|
||||||
|
.venv/
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
|
||||||
|
# Python bytecode and cache
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
|
||||||
|
# Flask specific
|
||||||
|
instance/
|
||||||
|
.webassets-cache/
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.egg-info/
|
||||||
|
|
||||||
|
# Editor specific files (optional, uncomment if applicable)
|
||||||
|
# .vscode/
|
||||||
|
# .idea/
|
||||||
|
# *.sublime-project
|
||||||
|
# *.sublime-workspace
|
||||||
|
|
||||||
|
# Database files (if using a local SQLite database)
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Sensitive files (e.g., environment variables)
|
||||||
|
.env
|
||||||
|
|
||||||
265
app.py
Normal file
265
app.py
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
import os
|
||||||
|
from flask import Flask, render_template, request, redirect, url_for, session, flash, g, jsonify
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import sqlite3
|
||||||
|
from datetime import timedelta
|
||||||
|
import requests
|
||||||
|
from google.oauth2 import service_account
|
||||||
|
from googleapiclient.discovery import build
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.secret_key = os.getenv('FLASK_SECRET_KEY', os.urandom(24))
|
||||||
|
app.permanent_session_lifetime = timedelta(minutes=30)
|
||||||
|
app.config['SESSION_COOKIE_PATH'] = '/'
|
||||||
|
|
||||||
|
USERNAME = os.getenv('FLASK_LOGIN_USER')
|
||||||
|
PASSWORD = os.getenv('FLASK_LOGIN_PASSWORD')
|
||||||
|
DATABASE = 'work_orders.db'
|
||||||
|
|
||||||
|
PUSHOVER_API_TOKEN = os.getenv('PUSHOVER_API_TOKEN')
|
||||||
|
PUSHOVER_USER_KEY = os.getenv('PUSHOVER_USER_KEY')
|
||||||
|
|
||||||
|
GA_PROPERTY_ID = os.getenv('GA_PROPERTY_ID')
|
||||||
|
GA_CREDENTIALS = 'ga_key.json'
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
filename='/var/log/ga4_debug.log', # Use an absolute path in production
|
||||||
|
level=logging.DEBUG,
|
||||||
|
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
db = getattr(g, '_database', None)
|
||||||
|
if db is None:
|
||||||
|
db = g._database = sqlite3.connect(DATABASE)
|
||||||
|
return db
|
||||||
|
|
||||||
|
@app.teardown_appcontext
|
||||||
|
def close_connection(exception):
|
||||||
|
db = getattr(g, '_database', None)
|
||||||
|
if db is not None:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
with app.app_context():
|
||||||
|
db = get_db()
|
||||||
|
# Orders table
|
||||||
|
db.execute('''CREATE TABLE IF NOT EXISTS orders (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
job TEXT NOT NULL,
|
||||||
|
address TEXT NOT NULL,
|
||||||
|
city TEXT NOT NULL,
|
||||||
|
state TEXT NOT NULL,
|
||||||
|
zipcode INTEGER NOT NULL,
|
||||||
|
phone TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);''')
|
||||||
|
# Completed Orders table
|
||||||
|
db.execute('''CREATE TABLE IF NOT EXISTS completed_orders (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
job TEXT NOT NULL,
|
||||||
|
address TEXT NOT NULL,
|
||||||
|
city TEXT NOT NULL,
|
||||||
|
state TEXT NOT NULL,
|
||||||
|
zipcode INTEGER NOT NULL,
|
||||||
|
phone TEXT NOT NULL,
|
||||||
|
completed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);''')
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
@app.route('/login', methods=['GET', 'POST'])
|
||||||
|
def login():
|
||||||
|
if request.method == 'POST':
|
||||||
|
username = request.form['username']
|
||||||
|
password = request.form['password']
|
||||||
|
if username == USERNAME and password == PASSWORD:
|
||||||
|
session.permanent = True
|
||||||
|
session['logged_in'] = True
|
||||||
|
return redirect(url_for('admin'))
|
||||||
|
else:
|
||||||
|
flash('Invalid username or password.')
|
||||||
|
return render_template('login.html')
|
||||||
|
|
||||||
|
@app.route('/logout', methods=['GET', 'POST'])
|
||||||
|
def logout():
|
||||||
|
session.pop('logged_in', None)
|
||||||
|
flash('You have been logged out.')
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
return render_template('form.html')
|
||||||
|
|
||||||
|
@app.route('/submit', methods=['POST'])
|
||||||
|
def submit():
|
||||||
|
try:
|
||||||
|
name = request.form.get('name')
|
||||||
|
job = request.form.get('job')
|
||||||
|
address = request.form.get('address')
|
||||||
|
city = request.form.get('city')
|
||||||
|
state = request.form.get('state')
|
||||||
|
zipcode = request.form.get('zipcode')
|
||||||
|
phone = request.form.get('phone')
|
||||||
|
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
conn.execute('''INSERT INTO orders (name, job, address, city, state, zipcode, phone)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)''', (name, job, address, city, state, zipcode, phone))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
send_push_notification(name, job)
|
||||||
|
|
||||||
|
|
||||||
|
session['form_submitted'] = True
|
||||||
|
|
||||||
|
return redirect(url_for('success'))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
session['form_submitted'] = True
|
||||||
|
return redirect(url_for('failure'))
|
||||||
|
|
||||||
|
@app.route('/success')
|
||||||
|
def success():
|
||||||
|
if not session.pop('form_submitted', None):
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
return render_template('success.html')
|
||||||
|
|
||||||
|
@app.route('/failure')
|
||||||
|
def failure():
|
||||||
|
if not session.pop('form_submitted', None):
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
return render_template('failure.html')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/admin')
|
||||||
|
def admin():
|
||||||
|
if not session.get('logged_in'):
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
orders = conn.execute('SELECT * FROM orders').fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return render_template('admin.html', work_orders=orders)
|
||||||
|
|
||||||
|
@app.route('/delete_order/<int:order_id>', methods=['POST'])
|
||||||
|
def delete_order(order_id):
|
||||||
|
if not session.get('logged_in'):
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
try:
|
||||||
|
db = get_db()
|
||||||
|
db.execute('DELETE FROM orders WHERE id = ?', (order_id,))
|
||||||
|
db.commit()
|
||||||
|
flash('Work order deleted successfully.', 'success')
|
||||||
|
except Exception as e:
|
||||||
|
flash(f'Error deleting work order: {str(e)}', 'danger')
|
||||||
|
return redirect(url_for('admin'))
|
||||||
|
|
||||||
|
@app.route('/mark_complete/<int:order_id>', methods=['POST'])
|
||||||
|
def mark_complete(order_id):
|
||||||
|
if not session.get('logged_in'):
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
try:
|
||||||
|
db = get_db()
|
||||||
|
# Fetch the order to mark as complete
|
||||||
|
order = db.execute('SELECT * FROM orders WHERE id = ?', (order_id,)).fetchone()
|
||||||
|
if order:
|
||||||
|
db.execute('INSERT INTO completed_orders (name, job, address, city, state, zipcode, phone) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||||
|
(order[1], order[2], order[3], order[4], order[5], order[6], order[7]))
|
||||||
|
db.execute('DELETE FROM orders WHERE id = ?', (order_id,))
|
||||||
|
db.commit()
|
||||||
|
flash('Work order marked as complete.', 'success')
|
||||||
|
else:
|
||||||
|
flash('Work order not found.', 'danger')
|
||||||
|
except Exception as e:
|
||||||
|
flash(f'Error marking work order as complete: {str(e)}', 'danger')
|
||||||
|
return redirect(url_for('admin'))
|
||||||
|
|
||||||
|
@app.route('/completed_jobs')
|
||||||
|
def completed_jobs():
|
||||||
|
if not session.get('logged_in'):
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
db = get_db()
|
||||||
|
jobs = db.execute('SELECT * FROM completed_orders').fetchall()
|
||||||
|
return render_template('completed_jobs.html', completed_jobs=jobs)
|
||||||
|
|
||||||
|
def send_push_notification(name, job):
|
||||||
|
if not PUSHOVER_API_TOKEN or not PUSHOVER_USER_KEY:
|
||||||
|
print("Pushover API token or user key is missing.")
|
||||||
|
return
|
||||||
|
|
||||||
|
message = f"New Work Order:\nName: {name}\nJob: {job}"
|
||||||
|
data = {
|
||||||
|
"token": PUSHOVER_API_TOKEN,
|
||||||
|
"user": PUSHOVER_USER_KEY,
|
||||||
|
"message": message
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post("https://api.pushover.net/1/messages.json", data=data)
|
||||||
|
if response.status_code == 200:
|
||||||
|
print("Push notification sent!")
|
||||||
|
else:
|
||||||
|
print(f"Failed to send notification: {response.status_code} - {response.text}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_ga4_data():
|
||||||
|
"""Fetch website analytics data from Google Analytics 4 (GA4) and handle missing data."""
|
||||||
|
try:
|
||||||
|
credentials = service_account.Credentials.from_service_account_file(
|
||||||
|
GA_CREDENTIALS, scopes=["https://www.googleapis.com/auth/analytics.readonly"]
|
||||||
|
)
|
||||||
|
analytics = build("analyticsdata", "v1beta", credentials=credentials)
|
||||||
|
|
||||||
|
response = analytics.properties().runReport(
|
||||||
|
property=f"properties/{int(GA_PROPERTY_ID)}",
|
||||||
|
body={
|
||||||
|
"dateRanges": [{"startDate": "7daysAgo", "endDate": "yesterday"}],
|
||||||
|
"metrics": [{"name": "totalUsers"}, {"name": "screenPageViews"}],
|
||||||
|
"dimensions": [{"name": "pagePath"}],
|
||||||
|
"limit": 1,
|
||||||
|
},
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
logging.debug("Full GA4 API Response: %s", json.dumps(response, indent=2))
|
||||||
|
|
||||||
|
# Extract top page from rows
|
||||||
|
rows = response.get("rows", [])
|
||||||
|
if not rows:
|
||||||
|
logging.error("No rows found in GA4 response!")
|
||||||
|
return {"total_users": "0", "total_pageviews": "0", "top_page": "N/A"}
|
||||||
|
|
||||||
|
top_page = rows[0]["dimensionValues"][0]["value"] if rows else "N/A"
|
||||||
|
|
||||||
|
# Extract users and pageviews from the first row
|
||||||
|
metric_values = rows[0]["metricValues"]
|
||||||
|
|
||||||
|
total_users = metric_values[0]["value"] if len(metric_values) > 0 else "0"
|
||||||
|
total_pageviews = metric_values[1]["value"] if len(metric_values) > 1 else "0"
|
||||||
|
|
||||||
|
logging.debug(f"Extracted Users: {total_users}, Pageviews: {total_pageviews}")
|
||||||
|
|
||||||
|
return {"total_users": total_users, "total_pageviews": total_pageviews, "top_page": top_page}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("Google Analytics API Error: %s", str(e))
|
||||||
|
return {"total_users": "0", "total_pageviews": "0", "top_page": "N/A"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/analytics')
|
||||||
|
def analytics():
|
||||||
|
if not session.get('logged_in'):
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
return jsonify(get_ga4_data())
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
init_db()
|
||||||
|
app.run(debug=True)
|
||||||
|
|
||||||
13
ga_key.json
Normal file
13
ga_key.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"type": "service_account",
|
||||||
|
"project_id": "thors-hammer-electrical",
|
||||||
|
"private_key_id": "79f95f7175cb393373b06fb18140b11cb63f7fe5",
|
||||||
|
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5jfg3AOV4mloj\ntOF8h6UbZWky3VgVIu5AMEeX7gMzQpxNX6cGorPHHyvXxePfBAq/n6Njb3iZ+Nc3\nb3IEtkhhObA9M8i9e6NLPbh5ODKJdlqGwwJ3Cd1hKEKdS4ssnQ9bbwyHI1uikl+c\ndAsW2ZNUtRlSSLt5h/t8oO+Fwr4p4PdMHcBtk8jA9aIrs5IRs7aDat6Kp+/WSwqv\nMdZ3Mj5pwZGIgzKsByXFDX7gbtZRHdJE467pJrmbgRtutOJQijHp0NZxosij5gYU\nyiZTewMdwCWUHhBlnBsn980szCO8IufurTSlEdL4priCz6MdzziMigYomsa1LFYC\nCY/LGegzAgMBAAECggEAHZTUjXBHJQL2e9rKV+AIImX4b3N6J2R6NyF7SG1ZdUKZ\nSHyHVDd8EbKWer/BpDwwunvowGF1CJbzOJM7yiSFRXq87gTja9HaJpSgZDLhW6jS\npclRC0k3UdXpSMpSVbp0SE9b3+9zHGfZdkfJvMrTAh4c+1E9EhLrtOKzTM/PIT3G\nda++giIYESFcx7ls2cqFED4U33pL9/EpRbE+6rZ1CwzJqWYbnN8joD1GeFMAu1lP\nQV1Yjzge+53mF2Tqr05SuYQNXExxW6BegUNQ3E/D/siY2WyTdSMGYpaC79fpzHdu\nXVzo3Wk7/9KOICYWc82y60DuKrlX2UeUzoRP69I9AQKBgQDsecejsmiD3YCu6lul\ngwXXc4r4IfiCaN7KlI4AzbiVE1lUBCDuZXJwfaaXnT15wmHhlq835lLjKkRwTR5x\nlrzotNC5yuwQV9gpUNUq5+3y5XmhPxLoBExAxhEbtOT5+Syp933Zi3RAgqkkw1B2\nxWy0t8rlaTveE0MmsOLlgWEI+wKBgQDI3+fY3N6Sbsp3JiQQJ2SWqd404Ebdfr4o\nBZq0cBsvxOqsT9tIg94BvcZlXHFvBg8vJRNKZywXCWx0giaorv8GXmPprYe7oLEX\n7ErJP6CoXZo5y7KiQvdcywsEbKj8LZDUyEGhBZH4896oEVRHi6PSBN9vp0XBXVGD\ndJpas4ToKQKBgQDrjjtBYrwdbo16r1RvQF6XSS8LELu9G72hyezR/Bp71PRMbnhn\nQIKIb4F80VKlcO2Ti0gqxLGYO0hFHWzP9TlkDIlGKU6Q0RAvx6cvwCwUomVQK8Yn\ne/CBLUtpb/4OyxikjjW8d99rSzw1tKD4TpyEP/hKIVNTWZiwd87skr4X9QKBgGdR\n66G68WxmOhOQ9amtaWqpUtblqO2SnGJfh5RZuVIXuhEJPiQNV6qTnzFRnDLb7gF0\n03hImv/6Y+OFcjb/U8NF16RBEniqjYxdiJX8+TjAdGxX3rjhMvRyp2cOMNkM4trf\nagpVoCBp51ORHkVyiL+kq/x1EEcGJcA0wJP4lFsJAoGBALxToCEw5kyUlnz8R7k3\nKN0YcxBDU1J7RiscPykO63vsf2gb3AFOldNb09hcG5+jph9vUOdOf5zx8gT9EUDg\niCHECGNZl2AZOjglekbs27l2s0U/wMO1MuqwxNP4nzu+CCkp4WFS9zlumL/KtPNp\nMgaMEHy0TMvGuC7OkSudask5\n-----END PRIVATE KEY-----\n",
|
||||||
|
"client_email": "thors-hammer-electrical@thors-hammer-electrical.iam.gserviceaccount.com",
|
||||||
|
"client_id": "106578123239401596254",
|
||||||
|
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||||
|
"token_uri": "https://oauth2.googleapis.com/token",
|
||||||
|
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||||
|
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/thors-hammer-electrical%40thors-hammer-electrical.iam.gserviceaccount.com",
|
||||||
|
"universe_domain": "googleapis.com"
|
||||||
|
}
|
||||||
BIN
static/images/Logo.png
Normal file
BIN
static/images/Logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 707 KiB |
BIN
templates/Logo.png
Normal file
BIN
templates/Logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 707 KiB |
131
templates/admin.html
Normal file
131
templates/admin.html
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Admin Panel - Work Orders</title>
|
||||||
|
|
||||||
|
<!-- Bootstrap CSS -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||||
|
</head>
|
||||||
|
<body class="bg-dark text-white">
|
||||||
|
|
||||||
|
<!-- Admin Panel Header -->
|
||||||
|
<div class="container mt-5 text-center">
|
||||||
|
<h2>Thor's Hammer Electrical - Admin Panel</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Table Container -->
|
||||||
|
<div class="container mt-4">
|
||||||
|
<table class="table table-striped table-bordered table-hover">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Job</th>
|
||||||
|
<th>Address</th>
|
||||||
|
<th>City</th>
|
||||||
|
<th>State</th>
|
||||||
|
<th>Zipcode</th>
|
||||||
|
<th>Phone</th>
|
||||||
|
<th>Submitted At</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for order in work_orders %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ order[0] }}</td> <!-- ID -->
|
||||||
|
<td>{{ order[1] }}</td> <!-- Name -->
|
||||||
|
<td>{{ order[2] }}</td> <!-- Job -->
|
||||||
|
<td>{{ order[3] }}</td> <!-- Address -->
|
||||||
|
<td>{{ order[4] }}</td> <!-- City -->
|
||||||
|
<td>{{ order[5] }}</td> <!-- State -->
|
||||||
|
<td>{{ order[6] }}</td> <!-- Zipcode -->
|
||||||
|
<td>{{ order[7] }}</td> <!-- Phone -->
|
||||||
|
<td>{{ order[8] }}</td> <!-- Submitted At -->
|
||||||
|
<td>
|
||||||
|
<!-- Delete Button -->
|
||||||
|
<form action="{{ url_for('delete_order', order_id=order[0]) }}" method="POST" style="display:inline;">
|
||||||
|
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Mark as Complete Button -->
|
||||||
|
<form action="{{ url_for('mark_complete', order_id=order[0]) }}" method="POST" style="display:inline;">
|
||||||
|
<button type="submit" class="btn btn-success btn-sm">Mark as Complete</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Logout Container -->
|
||||||
|
<div class="container text-center mt-4">
|
||||||
|
<a href="{{ url_for('completed_jobs') }}" class="btn btn-info btn-lg mb-3">View Completed Jobs</a>
|
||||||
|
|
||||||
|
<!-- Logout Button -->
|
||||||
|
<form action="{{ url_for('logout') }}" method="POST" class="d-inline-block">
|
||||||
|
<button type="submit" class="btn btn-warning btn-lg mb-3">Logout</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="text-center text-white">Website Analytics</h2>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card text-center bg-primary text-white">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4>Total Visitors</h4>
|
||||||
|
<h2 id="total-visitors">Loading...</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card text-center bg-success text-white">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4>Page Views</h4>
|
||||||
|
<h2 id="page-views">Loading...</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card text-center bg-warning text-white">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4>Most Visited Page</h4>
|
||||||
|
<h2 id="top-page">Loading...</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function fetchAnalytics() {
|
||||||
|
try {
|
||||||
|
let response = await fetch('/analytics');
|
||||||
|
let data = await response.json();
|
||||||
|
if (data.error) {
|
||||||
|
document.getElementById('total-visitors').textContent = "Error";
|
||||||
|
document.getElementById('page-views').textContent = "Error";
|
||||||
|
document.getElementById('top-page').textContent = "Error";
|
||||||
|
} else {
|
||||||
|
document.getElementById('total-visitors').textContent = data.total_users;
|
||||||
|
document.getElementById('page-views').textContent = data.total_pageviews;
|
||||||
|
document.getElementById('top-page').textContent = data.top_page;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching analytics:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchAnalytics();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Bootstrap JS and Popper -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
61
templates/completed_jobs.html
Normal file
61
templates/completed_jobs.html
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Admin Panel - Completed Jobs</title>
|
||||||
|
|
||||||
|
<!-- Bootstrap CSS -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||||
|
</head>
|
||||||
|
<body class="bg-dark text-white">
|
||||||
|
|
||||||
|
<!-- Admin Panel Header -->
|
||||||
|
<div class="container mt-5 text-center">
|
||||||
|
<h2>Thor's Hammer Electrical - Completed Jobs</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Table Container -->
|
||||||
|
<div class="container mt-4">
|
||||||
|
<table class="table table-striped table-bordered table-hover">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Job</th>
|
||||||
|
<th>Address</th>
|
||||||
|
<th>City</th>
|
||||||
|
<th>State</th>
|
||||||
|
<th>Zipcode</th>
|
||||||
|
<th>Phone</th>
|
||||||
|
<th>Completed At</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for job in completed_jobs %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ job[0] }}</td> <!-- ID -->
|
||||||
|
<td>{{ job[1] }}</td> <!-- Name -->
|
||||||
|
<td>{{ job[2] }}</td> <!-- Job -->
|
||||||
|
<td>{{ job[3] }}</td> <!-- Address -->
|
||||||
|
<td>{{ job[4] }}</td> <!-- City -->
|
||||||
|
<td>{{ job[5] }}</td> <!-- State -->
|
||||||
|
<td>{{ job[6] }}</td> <!-- Zipcode -->
|
||||||
|
<td>{{ job[7] }}</td> <!-- Phone -->
|
||||||
|
<td>{{ job[8] }}</td> <!-- Completed At -->
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="back-container">
|
||||||
|
<a href="{{ url_for('admin') }}">
|
||||||
|
<button class="btn btn-success btn-sm">
|
||||||
|
Back to Admin Panel</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
||||||
74
templates/failure.html
Normal file
74
templates/failure.html
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
|
||||||
|
<!-- Google tag (gtag.js) -->
|
||||||
|
<script async src="https://www.googletagmanager.com/gtag/js?id=G-ZJ4YKQNNG1"></script>
|
||||||
|
<script>
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag(){dataLayer.push(arguments);}
|
||||||
|
gtag('js', new Date());
|
||||||
|
|
||||||
|
gtag('config', 'G-ZJ4YKQNNG1');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Work Order Form</title>
|
||||||
|
<link rel="stylesheet" href="styles.css" type="text/css" />
|
||||||
|
|
||||||
|
<!-- Cyborg Bootstrap Theme -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.0/dist/cyborg/bootstrap.min.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
<link rel="icon" href="{{ url_for('static', filename='Logo.png') }}">
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Oswald:wght@200..700&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
|
<img src="{{ url_for('static', filename='Logo.png') }}" width="60" height="60">
|
||||||
|
<a class="navbar-brand" href="https://thorshammerelectrical.com/index.html">Thor's Hammer Electrical</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav ms-auto">
|
||||||
|
<li class="nav-item active">
|
||||||
|
<a class="nav-link" href="https://thorshammerelectrical.com/About.html">About</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="https://thorshammerelectrical.com/Services.html">Services</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="https://www.help.thorshammerelectrical.com">Submit Work-Order</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="https://thorshammerelectrical.com/Review.html">Reviews</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="d-flex min-vh-100 align-items-center justify-content-center">
|
||||||
|
<div class="d-flex flex-column align-items-center text-center">
|
||||||
|
<div class="p-2">
|
||||||
|
<h1>Something went wrong. Please try again!</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<div class="p-2">
|
||||||
|
<button class="btn btn-danger">
|
||||||
|
<a href="{{ url_for('index') }}" class="text-white text-decoration-none">Back to Form</a>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
113
templates/form.html
Normal file
113
templates/form.html
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
|
||||||
|
<!-- Google tag (gtag.js) -->
|
||||||
|
<script async src="https://www.googletagmanager.com/gtag/js?id=G-ZJ4YKQNNG1"></script>
|
||||||
|
<script>
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag(){dataLayer.push(arguments);}
|
||||||
|
gtag('js', new Date());
|
||||||
|
|
||||||
|
gtag('config', 'G-ZJ4YKQNNG1');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Work Order Form</title>
|
||||||
|
<link rel="stylesheet" href="styles.css" type="text/css" />
|
||||||
|
|
||||||
|
<!-- Cyborg Bootstrap Theme -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.0/dist/cyborg/bootstrap.min.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
<link rel="icon" href="{{ url_for('static', filename='Logo.png') }}">
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Oswald:wght@200..700&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
|
<img src="{{ url_for('static', filename='Logo.png') }}" width="60" height="60">
|
||||||
|
<a class="navbar-brand" href="https://thorshammerelectrical.com/index.html">Thor's Hammer Electrical</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav ms-auto">
|
||||||
|
<li class="nav-item active">
|
||||||
|
<a class="nav-link" href="https://thorshammerelectrical.com/About.html">About</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="https://thorshammerelectrical.com/Services.html">Services</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="https://www.help.thorshammerelectrical.com">Submit Work-Order</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="https://thorshammerelectrical.com/Review.html">Reviews</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Form Container -->
|
||||||
|
<div class="col-lg-8 mx-auto">
|
||||||
|
<h1 class="text-white">Thor's Hammer Electrical Work-Order Form</h1>
|
||||||
|
<p class="text-white">Got a job for us? Submit it here and Leonard will get back to you as soon as possible!</p>
|
||||||
|
|
||||||
|
<form action="/submit" method="POST">
|
||||||
|
<!-- Form Fields -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="name" class="form-label">Name:</label>
|
||||||
|
<input type="text" id="name" name="name" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="job" class="form-label">Job Details:</label>
|
||||||
|
<textarea id="job" name="job" class="form-control" required></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="address" class="form-label">Address:</label>
|
||||||
|
<textarea id="address" name="address" class="form-control" required></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="city" class="form-label">City:</label>
|
||||||
|
<input type="text" id="city" name="city" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="state" class="form-label">State:</label>
|
||||||
|
<input type="text" id="state" name="state" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="zipcode" class="form-label">Zip Code:</label>
|
||||||
|
<input type="text" id="zipcode" name="zipcode" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="phone" class="form-label">Phone Number:</label>
|
||||||
|
<input type="text" id="phone" name="phone" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit Button -->
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<input type="submit" value="Submit" class="btn btn-danger btn-lg">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bootstrap JS -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
67
templates/login.html
Normal file
67
templates/login.html
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<title>Login</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: linear-gradient(to right, #667eea, #764ba2);
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.login-container {
|
||||||
|
background: white;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
text-align: center;
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.logo-placeholder {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
background: #ddd;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0 auto 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 100px; /* Adjust size as needed */
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto 1rem; /* Centers the logo */
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-container">
|
||||||
|
<img src="{{ url_for('static', filename='Logo.png') }}" alt="Logo" class="logo">
|
||||||
|
|
||||||
|
|
||||||
|
<h2 class="mb-3">Login</h2>
|
||||||
|
<form action="/login" method="POST">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input type="text" class="form-control" id="username" name="username" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" class="form-control" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary btn-block">Login</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
9
templates/styles.css
Normal file
9
templates/styles.css
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
.bg-dark-gray {
|
||||||
|
background-color: #333333; /* Darker Gray */
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #000000; /* Black background for body */
|
||||||
|
}
|
||||||
|
|
||||||
74
templates/success.html
Normal file
74
templates/success.html
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
|
||||||
|
<!-- Google tag (gtag.js) -->
|
||||||
|
<script async src="https://www.googletagmanager.com/gtag/js?id=G-ZJ4YKQNNG1"></script>
|
||||||
|
<script>
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag(){dataLayer.push(arguments);}
|
||||||
|
gtag('js', new Date());
|
||||||
|
|
||||||
|
gtag('config', 'G-ZJ4YKQNNG1');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Work Order Form</title>
|
||||||
|
<link rel="stylesheet" href="styles.css" type="text/css" />
|
||||||
|
|
||||||
|
<!-- Cyborg Bootstrap Theme -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.0/dist/cyborg/bootstrap.min.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
<link rel="icon" href="{{ url_for('static', filename='Logo.png') }}">
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Oswald:wght@200..700&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
|
<img src="{{ url_for('static', filename='Logo.png') }}" width="60" height="60">
|
||||||
|
<a class="navbar-brand" href="https://thorshammerelectrical.com/index.html">Thor's Hammer Electrical</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav ms-auto">
|
||||||
|
<li class="nav-item active">
|
||||||
|
<a class="nav-link" href="https://thorshammerelectrical.com/About.html">About</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="https://thorshammerelectrical.com/Services.html">Services</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="https://www.help.thorshammerelectrical.com">Submit Work-Order</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="https://thorshammerelectrical.com/Review.html">Reviews</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="d-flex min-vh-100 align-items-center justify-content-center">
|
||||||
|
<div class="d-flex flex-column align-items-center text-center">
|
||||||
|
<div class="p-2">
|
||||||
|
<h1>Success! Thank you for choosing Thor's Hammer</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<div class="p-2">
|
||||||
|
<button class="btn btn-danger">
|
||||||
|
<a href="https://thorshammerelectrical.com" class="text-white text-decoration-none">Back to Home</a>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
Reference in New Issue
Block a user