commit 66eca93f01eb9b52f1a883395648e2e5c1113a5b Author: Benny Date: Sat Apr 25 16:08:12 2026 +0000 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ce0de34 --- /dev/null +++ b/.gitignore @@ -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 + diff --git a/app.py b/app.py new file mode 100644 index 0000000..2629b8c --- /dev/null +++ b/app.py @@ -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/', 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/', 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) + diff --git a/ga_key.json b/ga_key.json new file mode 100644 index 0000000..ad9e6c8 --- /dev/null +++ b/ga_key.json @@ -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" +} diff --git a/static/images/Logo.png b/static/images/Logo.png new file mode 100644 index 0000000..4244d4e Binary files /dev/null and b/static/images/Logo.png differ diff --git a/templates/Logo.png b/templates/Logo.png new file mode 100644 index 0000000..4244d4e Binary files /dev/null and b/templates/Logo.png differ diff --git a/templates/admin.html b/templates/admin.html new file mode 100644 index 0000000..4ab10dc --- /dev/null +++ b/templates/admin.html @@ -0,0 +1,131 @@ + + + + + + Admin Panel - Work Orders + + + + + + + +
+

Thor's Hammer Electrical - Admin Panel

+
+ + +
+ + + + + + + + + + + + + + + + + {% for order in work_orders %} + + + + + + + + + + + + + {% endfor %} + +
IDNameJobAddressCityStateZipcodePhoneSubmitted AtActions
{{ order[0] }} {{ order[1] }} {{ order[2] }} {{ order[3] }} {{ order[4] }} {{ order[5] }} {{ order[6] }} {{ order[7] }} {{ order[8] }} + +
+ +
+ + +
+ +
+
+
+ + +
+ View Completed Jobs + + +
+ +
+
+ +

Website Analytics

+
+
+
+
+

Total Visitors

+

Loading...

+
+
+
+ +
+
+
+

Page Views

+

Loading...

+
+
+
+ +
+
+
+

Most Visited Page

+

Loading...

+
+
+
+
+ + + + + + + + + diff --git a/templates/completed_jobs.html b/templates/completed_jobs.html new file mode 100644 index 0000000..f0bcdaf --- /dev/null +++ b/templates/completed_jobs.html @@ -0,0 +1,61 @@ + + + + + + Admin Panel - Completed Jobs + + + + + + + +
+

Thor's Hammer Electrical - Completed Jobs

+
+ + +
+ + + + + + + + + + + + + + + + {% for job in completed_jobs %} + + + + + + + + + + + + {% endfor %} + +
IDNameJobAddressCityStateZipcodePhoneCompleted At
{{ job[0] }} {{ job[1] }} {{ job[2] }} {{ job[3] }} {{ job[4] }} {{ job[5] }} {{ job[6] }} {{ job[7] }} {{ job[8] }}
+ + + + + + + diff --git a/templates/failure.html b/templates/failure.html new file mode 100644 index 0000000..754a0fb --- /dev/null +++ b/templates/failure.html @@ -0,0 +1,74 @@ + + + + + + + + + + + Work Order Form + + + + + + + + + + + + + + + + + + + +
+
+
+

Something went wrong. Please try again!

+
+ +
+ +
+ +
+
+
+ diff --git a/templates/form.html b/templates/form.html new file mode 100644 index 0000000..4921d36 --- /dev/null +++ b/templates/form.html @@ -0,0 +1,113 @@ + + + + + + + + + + + Work Order Form + + + + + + + + + + + + + + + + + + + + +
+

Thor's Hammer Electrical Work-Order Form

+

Got a job for us? Submit it here and Leonard will get back to you as soon as possible!

+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ +
+
+
+
+ + + + + + diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..a8eeef7 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,67 @@ + + + + + + + Login + + + + + + + diff --git a/templates/styles.css b/templates/styles.css new file mode 100644 index 0000000..a35bd55 --- /dev/null +++ b/templates/styles.css @@ -0,0 +1,9 @@ + +.bg-dark-gray { + background-color: #333333; /* Darker Gray */ + } + + body { + background-color: #000000; /* Black background for body */ +} + diff --git a/templates/success.html b/templates/success.html new file mode 100644 index 0000000..18c5aa5 --- /dev/null +++ b/templates/success.html @@ -0,0 +1,74 @@ + + + + + + + + + + + Work Order Form + + + + + + + + + + + + + + + + + + + +
+
+
+

Success! Thank you for choosing Thor's Hammer

+
+ +
+ +
+ +
+
+
+ diff --git a/wsgi.py b/wsgi.py new file mode 100644 index 0000000..afeaa0a --- /dev/null +++ b/wsgi.py @@ -0,0 +1,5 @@ +from app import app + +if __name__ == '__main__': + app.run() +