commit a700774463e6c1d5f31e79702c9e2b6e55b07bb1 Author: Nick Tonjum Date: Thu Jul 23 14:17:24 2026 -0500 Initial commit: TonjumTechRecords - business income/expense tracker with Flask backend and Tailwind frontend diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e65e5cb --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +venv/ +__pycache__/ +*.pyc +.DS_Store +*.db +*.log +.env diff --git a/app.py b/app.py new file mode 100644 index 0000000..066af1b --- /dev/null +++ b/app.py @@ -0,0 +1,713 @@ +import os +import csv +import io +import datetime +import bcrypt +import jwt +import mysql.connector +from functools import wraps +from flask import Flask, render_template, request, jsonify, send_file, session +from flask_cors import CORS +from apscheduler.schedulers.background import BackgroundScheduler + +app = Flask(__name__) +app.secret_key = 'tonjumtechrecords-secret-key-change-in-production' +CORS(app) + +DB_CONFIG = { + 'host': '10.7.0.1', + 'user': 'nick', + 'password': 'nick9924Tucker9924##', + 'database': 'TonjumTechRecords', + 'charset': 'utf8mb4' +} +JWT_SECRET = 'tonjumtechrecords-jwt-secret-change-in-production' +JWT_ALGORITHM = 'HS256' + + +def get_db(): + conn = mysql.connector.connect(**DB_CONFIG) + return conn + + +def token_required(f): + @wraps(f) + def decorated(*args, **kwargs): + token = None + auth_header = request.headers.get('Authorization') + if auth_header and auth_header.startswith('Bearer '): + token = auth_header.split(' ')[1] + if not token: + return jsonify({'message': 'Token is missing'}), 401 + try: + data = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM]) + current_user = data['username'] + except jwt.ExpiredSignatureError: + return jsonify({'message': 'Token has expired'}), 401 + except jwt.InvalidTokenError: + return jsonify({'message': 'Invalid token'}), 401 + return f(current_user, *args, **kwargs) + return decorated + + +def advance_billing_date(next_date, interval): + d = next_date + if interval == 'Weekly': + return d + datetime.timedelta(weeks=1) + elif interval == 'Monthly': + month = d.month + 1 + year = d.year + if month > 12: + month = 1 + year += 1 + day = min(d.day, [31, 29 if year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) else 28, + 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1]) + return d.replace(year=year, month=month, day=day) + elif interval == 'Quarterly': + month = d.month + 3 + year = d.year + if month > 12: + month -= 12 + year += 1 + day = min(d.day, [31, 29 if year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) else 28, + 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1]) + return d.replace(year=year, month=month, day=day) + elif interval == 'Annually': + year = d.year + 1 + day = min(d.day, [31, 29 if year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) else 28, + 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][d.month - 1]) + return d.replace(year=year, day=day) + + +def run_subscription_billing(): + with app.app_context(): + try: + conn = get_db() + cursor = conn.cursor(dictionary=True) + today = datetime.date.today() + cursor.execute(""" + SELECT s.*, c.Name AS ClientName FROM Subscriptions s + JOIN Clients c ON c.Id = s.ClientId + WHERE s.IsActive = TRUE AND s.NextBillingDate <= %s + """, (today,)) + due = cursor.fetchall() + for sub in due: + if sub['EndDate'] and sub['NextBillingDate'] > sub['EndDate']: + cursor.execute("UPDATE Subscriptions SET IsActive = FALSE WHERE Id = %s", (sub['Id'],)) + conn.commit() + continue + cursor.execute(""" + INSERT INTO Transactions (Type, TransactionDate, ClientId, Amount, PaymentMethod, Description, SubscriptionId) + VALUES ('Income', %s, %s, %s, 'Other', %s, %s) + """, (sub['NextBillingDate'], sub['ClientId'], sub['Amount'], + f"Subscription: {sub['ServiceName']} (auto-billed)", sub['Id'])) + next_date = advance_billing_date(sub['NextBillingDate'], sub['Interval']) + if sub['EndDate'] and next_date > sub['EndDate']: + cursor.execute("UPDATE Subscriptions SET NextBillingDate = %s, IsActive = FALSE WHERE Id = %s", + (next_date, sub['Id'])) + else: + cursor.execute("UPDATE Subscriptions SET NextBillingDate = %s WHERE Id = %s", + (next_date, sub['Id'])) + conn.commit() + cursor.close() + conn.close() + except Exception as e: + print(f"[SubscriptionBilling] Error: {e}") + + +scheduler = None + +def start_scheduler(): + global scheduler + if scheduler is not None: + return + scheduler = BackgroundScheduler() + scheduler.add_job(run_subscription_billing, 'interval', hours=1, id='subscription_billing') + scheduler.start() + print("[Scheduler] Started subscription billing service") + run_subscription_billing() + + +# --- Auth --- + +@app.route('/api/auth/login', methods=['POST']) +def login(): + data = request.get_json() + username = data.get('username', '') + password = data.get('password', '') + conn = get_db() + cursor = conn.cursor(dictionary=True) + cursor.execute("SELECT * FROM Users WHERE Username = %s", (username,)) + user = cursor.fetchone() + cursor.close() + conn.close() + if user and bcrypt.checkpw(password.encode(), user['PasswordHash'].encode()): + token = jwt.encode({ + 'username': user['Username'], + 'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=24) + }, JWT_SECRET, algorithm=JWT_ALGORITHM) + return jsonify({'token': token, 'username': user['Username']}) + return jsonify({'message': 'Invalid credentials'}), 401 + + +# --- Dashboard --- + +@app.route('/api/dashboard/summary') +@token_required +def dashboard_summary(current_user): + conn = get_db() + cursor = conn.cursor(dictionary=True) + today = datetime.date.today() + first_of_month = today.replace(day=1) + year_start = today.replace(month=1, day=1) + + cursor.execute(""" + SELECT COALESCE(SUM(CASE WHEN Type='Income' THEN Amount ELSE -Amount END), 0) AS total + FROM Transactions + """) + total_profit = cursor.fetchone()['total'] + + cursor.execute(""" + SELECT COALESCE(SUM(CASE WHEN Type='Income' THEN Amount ELSE -Amount END), 0) AS total + FROM Transactions WHERE TransactionDate >= %s + """, (today - datetime.timedelta(days=365),)) + last_365 = cursor.fetchone()['total'] + + cursor.execute(""" + SELECT COALESCE(SUM(CASE WHEN Type='Income' THEN Amount ELSE -Amount END), 0) AS total + FROM Transactions WHERE TransactionDate >= %s + """, (today - datetime.timedelta(days=30),)) + last_30 = cursor.fetchone()['total'] + + months_data = [] + for i in range(11, -1, -1): + m = today.month - i + y = today.year + if m <= 0: + m += 12 + y -= 1 + month_start = datetime.date(y, m, 1) + if m == 12: + month_end = datetime.date(y + 1, 1, 1) + else: + month_end = datetime.date(y, m + 1, 1) + cursor.execute(""" + SELECT COALESCE(SUM(CASE WHEN Type='Income' THEN Amount ELSE 0 END), 0) AS income, + COALESCE(SUM(CASE WHEN Type='Expense' THEN Amount ELSE 0 END), 0) AS expense + FROM Transactions WHERE TransactionDate >= %s AND TransactionDate < %s + """, (month_start, month_end)) + row = cursor.fetchone() + months_data.append({ + 'month': f'{y}-{m:02d}', + 'income': float(row['income']), + 'expense': float(row['expense']) + }) + + cursor.execute(""" + SELECT COALESCE(SUM(CASE WHEN Type='Income' THEN Amount ELSE -Amount END), 0) AS total + FROM Transactions WHERE TransactionDate >= %s + """, (year_start,)) + ytd_profit = cursor.fetchone()['total'] + + cursor.execute(""" + SELECT * FROM Subscriptions WHERE IsActive = TRUE + AND (EndDate IS NULL OR EndDate > %s) + """, (today,)) + active_subs = cursor.fetchall() + active_subs_count = len(active_subs) + mrr_map = {'Weekly': 4.33, 'Monthly': 1, 'Quarterly': 1/3, 'Annually': 1/12} + active_subs_total = sum(float(s['Amount']) * mrr_map.get(s['Interval'], 1) for s in active_subs) + year_end = datetime.date(today.year, 12, 31) + + future_sub_projected = 0.0 + for sub in active_subs: + bdate = sub['NextBillingDate'] + while bdate <= year_end: + if sub['EndDate'] and bdate > sub['EndDate']: + break + future_sub_projected += float(sub['Amount']) + bdate = advance_billing_date(bdate, sub['Interval']) + + projected = float(ytd_profit) + future_sub_projected + + cursor.close() + conn.close() + return jsonify({ + 'totalProfit': float(total_profit), + 'last365': float(last_365), + 'last30': float(last_30), + 'projected': float(projected), + 'monthlyHistory': months_data, + 'activeSubscriptions': active_subs_count, + 'activeSubscriptionsTotal': float(active_subs_total) + }) + + +# --- Transactions --- + +@app.route('/api/transactions', methods=['GET']) +@token_required +def list_transactions(current_user): + conn = get_db() + cursor = conn.cursor(dictionary=True) + page = request.args.get('page', 1, type=int) + limit = request.args.get('limit', 50, type=int) + offset = (page - 1) * limit + ttype = request.args.get('type') + client_id = request.args.get('client_id', type=int) + from_date = request.args.get('from') + to_date = request.args.get('to') + + where = [] + params = [] + if ttype: + where.append("t.Type = %s") + params.append(ttype) + if client_id: + where.append("t.ClientId = %s") + params.append(client_id) + if from_date: + where.append("t.TransactionDate >= %s") + params.append(from_date) + if to_date: + where.append("t.TransactionDate <= %s") + params.append(to_date) + + where_clause = ('WHERE ' + ' AND '.join(where)) if where else '' + cursor.execute(f""" + SELECT t.*, c.Name AS ClientName, s.ServiceName AS SubscriptionName + FROM Transactions t + LEFT JOIN Clients c ON c.Id = t.ClientId + LEFT JOIN Subscriptions s ON s.Id = t.SubscriptionId + {where_clause} + ORDER BY t.TransactionDate DESC, t.Id DESC + LIMIT %s OFFSET %s + """, params + [limit, offset]) + transactions = cursor.fetchall() + for tx in transactions: + tx['Amount'] = float(tx['Amount']) + tx['HasImage'] = tx['ImageData'] is not None + tx['ImageData'] = None + tx.pop('UpdatedAt', None) + if tx['CreatedAt']: + tx['CreatedAt'] = tx['CreatedAt'].isoformat() + if tx['TransactionDate']: + tx['TransactionDate'] = tx['TransactionDate'].isoformat() + + cursor.execute(f"SELECT COUNT(*) AS cnt FROM Transactions t {where_clause}", params) + total = cursor.fetchone()['cnt'] + cursor.close() + conn.close() + return jsonify({'transactions': transactions, 'total': total, 'page': page, 'limit': limit}) + + +@app.route('/api/transactions', methods=['POST']) +@token_required +def create_transaction(current_user): + ttype = request.form.get('type') + date_str = request.form.get('date') + client_name = request.form.get('client_name', '').strip() + client_id = request.form.get('client_id', type=int) + amount = request.form.get('amount', type=float) + payment_method = request.form.get('payment_method') + description = request.form.get('description', '') + image_file = request.files.get('image') + + if not all([ttype, date_str, amount, payment_method]): + return jsonify({'message': 'Missing required fields'}), 400 + + conn = get_db() + cursor = conn.cursor(dictionary=True) + + if client_name and not client_id: + cursor.execute("INSERT IGNORE INTO Clients (Name) VALUES (%s)", (client_name,)) + conn.commit() + cursor.execute("SELECT Id FROM Clients WHERE Name = %s", (client_name,)) + row = cursor.fetchone() + client_id = row['Id'] if row else None + elif client_id: + pass + + image_data = None + image_content_type = None + if image_file and image_file.filename: + image_data = image_file.read() + image_content_type = image_file.content_type or 'image/jpeg' + + cursor.execute(""" + INSERT INTO Transactions (Type, TransactionDate, ClientId, Amount, PaymentMethod, Description, ImageData, ImageContentType) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s) + """, (ttype, date_str, client_id, amount, payment_method, description, image_data, image_content_type)) + conn.commit() + tx_id = cursor.lastrowid + cursor.close() + conn.close() + return jsonify({'message': 'Transaction created', 'id': tx_id}), 201 + + +@app.route('/api/transactions/', methods=['PUT']) +@token_required +def update_transaction(current_user, tx_id): + conn = get_db() + cursor = conn.cursor(dictionary=True) + cursor.execute("SELECT * FROM Transactions WHERE Id = %s", (tx_id,)) + existing = cursor.fetchone() + if not existing: + cursor.close() + conn.close() + return jsonify({'message': 'Not found'}), 404 + + ttype = request.form.get('type', existing['Type']) + date_str = request.form.get('date', existing['TransactionDate'].isoformat() if existing['TransactionDate'] else None) + client_name = request.form.get('client_name', '').strip() + client_id = request.form.get('client_id', type=int) + amount = request.form.get('amount', type=float) or float(existing['Amount']) + payment_method = request.form.get('payment_method', existing['PaymentMethod']) + description = request.form.get('description', existing['Description'] or '') + image_file = request.files.get('image') + + if client_name and not client_id: + cursor.execute("INSERT IGNORE INTO Clients (Name) VALUES (%s)", (client_name,)) + conn.commit() + cursor.execute("SELECT Id FROM Clients WHERE Name = %s", (client_name,)) + row = cursor.fetchone() + client_id = row['Id'] if row else existing['ClientId'] + else: + client_id = client_id if client_id is not None else existing['ClientId'] + + if image_file and image_file.filename: + image_data = image_file.read() + image_content_type = image_file.content_type or 'image/jpeg' + cursor.execute(""" + UPDATE Transactions SET Type=%s, TransactionDate=%s, ClientId=%s, Amount=%s, + PaymentMethod=%s, Description=%s, ImageData=%s, ImageContentType=%s + WHERE Id=%s + """, (ttype, date_str, client_id, amount, payment_method, description, image_data, image_content_type, tx_id)) + else: + cursor.execute(""" + UPDATE Transactions SET Type=%s, TransactionDate=%s, ClientId=%s, Amount=%s, + PaymentMethod=%s, Description=%s + WHERE Id=%s + """, (ttype, date_str, client_id, amount, payment_method, description, tx_id)) + + conn.commit() + cursor.close() + conn.close() + return jsonify({'message': 'Transaction updated'}) + + +@app.route('/api/transactions/', methods=['DELETE']) +@token_required +def delete_transaction(current_user, tx_id): + conn = get_db() + cursor = conn.cursor() + cursor.execute("DELETE FROM Transactions WHERE Id = %s", (tx_id,)) + conn.commit() + cursor.close() + conn.close() + return jsonify({'message': 'Transaction deleted'}) + + +@app.route('/api/transactions//image') +def get_image(tx_id): + conn = get_db() + cursor = conn.cursor(dictionary=True) + cursor.execute("SELECT ImageData, ImageContentType FROM Transactions WHERE Id = %s", (tx_id,)) + row = cursor.fetchone() + cursor.close() + conn.close() + if row and row['ImageData']: + return send_file(io.BytesIO(row['ImageData']), mimetype=row['ImageContentType'] or 'image/jpeg') + return jsonify({'message': 'No image'}), 404 + + +@app.route('/api/transactions/export/csv') +@token_required +def export_csv(current_user): + conn = get_db() + cursor = conn.cursor(dictionary=True) + cursor.execute(""" + SELECT t.TransactionDate, c.Name AS ClientName, t.Amount, t.Description + FROM Transactions t + LEFT JOIN Clients c ON c.Id = t.ClientId + ORDER BY t.TransactionDate DESC + """) + rows = cursor.fetchall() + cursor.close() + conn.close() + + output = io.StringIO() + writer = csv.writer(output) + writer.writerow(['Date', 'Client', 'Amount', 'Description']) + for r in rows: + writer.writerow([ + r['TransactionDate'].isoformat() if r['TransactionDate'] else '', + r['ClientName'] or '', + float(r['Amount']) if r['Amount'] else 0, + r['Description'] or '' + ]) + mem = io.BytesIO(output.getvalue().encode('utf-8')) + return send_file(mem, mimetype='text/csv', as_attachment=True, download_name='transactions.csv') + + +# --- Clients --- + +@app.route('/api/clients', methods=['GET']) +@token_required +def list_clients(current_user): + conn = get_db() + cursor = conn.cursor(dictionary=True) + cursor.execute("SELECT * FROM Clients ORDER BY Name ASC") + clients = cursor.fetchall() + cursor.close() + conn.close() + return jsonify(clients) + + +@app.route('/api/clients', methods=['POST']) +@token_required +def create_client(current_user): + data = request.get_json() + name = data.get('name', '').strip() + if not name: + return jsonify({'message': 'Name is required'}), 400 + conn = get_db() + cursor = conn.cursor(dictionary=True) + try: + cursor.execute(""" + INSERT INTO Clients (Name, ContactName, Phone, Email, Address) + VALUES (%s, %s, %s, %s, %s) + """, (name, data.get('contact_name', ''), data.get('phone', ''), data.get('email', ''), data.get('address', ''))) + conn.commit() + client_id = cursor.lastrowid + cursor.close() + conn.close() + return jsonify({'message': 'Client created', 'id': client_id}), 201 + except mysql.connector.IntegrityError: + cursor.close() + conn.close() + return jsonify({'message': 'Client already exists'}), 409 + + +@app.route('/api/clients/', methods=['PUT']) +@token_required +def update_client(current_user, client_id): + data = request.get_json() + conn = get_db() + cursor = conn.cursor(dictionary=True) + cursor.execute("SELECT * FROM Clients WHERE Id = %s", (client_id,)) + existing = cursor.fetchone() + if not existing: + cursor.close() + conn.close() + return jsonify({'message': 'Not found'}), 404 + cursor.execute(""" + UPDATE Clients SET Name=%s, ContactName=%s, Phone=%s, Email=%s, Address=%s + WHERE Id=%s + """, ( + data.get('name', existing['Name']), + data.get('contact_name', existing['ContactName'] or ''), + data.get('phone', existing['Phone'] or ''), + data.get('email', existing['Email'] or ''), + data.get('address', existing['Address'] or ''), + client_id + )) + conn.commit() + cursor.close() + conn.close() + return jsonify({'message': 'Client updated'}) + + +@app.route('/api/clients/', methods=['DELETE']) +@token_required +def delete_client(current_user, client_id): + conn = get_db() + cursor = conn.cursor() + cursor.execute("DELETE FROM Clients WHERE Id = %s", (client_id,)) + conn.commit() + cursor.close() + conn.close() + return jsonify({'message': 'Client deleted'}) + + +# --- Subscriptions --- + +@app.route('/api/subscriptions', methods=['GET']) +@token_required +def list_subscriptions(current_user): + conn = get_db() + cursor = conn.cursor(dictionary=True) + cursor.execute(""" + SELECT s.*, c.Name AS ClientName + FROM Subscriptions s + JOIN Clients c ON c.Id = s.ClientId + ORDER BY s.IsActive DESC, s.NextBillingDate ASC + """) + subs = cursor.fetchall() + for s in subs: + s['Amount'] = float(s['Amount']) + if s['StartDate']: + s['StartDate'] = s['StartDate'].isoformat() + if s['EndDate']: + s['EndDate'] = s['EndDate'].isoformat() + if s['NextBillingDate']: + s['NextBillingDate'] = s['NextBillingDate'].isoformat() + if s['CreatedAt']: + s['CreatedAt'] = s['CreatedAt'].isoformat() + if s['UpdatedAt']: + s['UpdatedAt'] = s['UpdatedAt'].isoformat() + cursor.close() + conn.close() + return jsonify(subs) + + +@app.route('/api/subscriptions', methods=['POST']) +@token_required +def create_subscription(current_user): + data = request.get_json() + client_id = data.get('client_id') + client_name = data.get('client_name', '').strip() + service_name = data.get('service_name', '').strip() + amount = data.get('amount') + if amount is not None: + amount = float(amount) + interval = data.get('interval') + start_date = data.get('start_date') + end_date = data.get('end_date') + + if not all([amount, interval, start_date, service_name]): + return jsonify({'message': 'Missing required fields'}), 400 + + conn = get_db() + cursor = conn.cursor(dictionary=True) + + if client_name and not client_id: + cursor.execute("INSERT IGNORE INTO Clients (Name) VALUES (%s)", (client_name,)) + conn.commit() + cursor.execute("SELECT Id FROM Clients WHERE Name = %s", (client_name,)) + row = cursor.fetchone() + client_id = row['Id'] if row else None + + if not client_id: + cursor.close() + conn.close() + return jsonify({'message': 'Client is required'}), 400 + + cursor.execute(""" + INSERT INTO Subscriptions (ClientId, ServiceName, Amount, `Interval`, StartDate, EndDate, NextBillingDate) + VALUES (%s, %s, %s, %s, %s, %s, %s) + """, (client_id, service_name, amount, interval, start_date, end_date or None, start_date)) + conn.commit() + sub_id = cursor.lastrowid + cursor.close() + conn.close() + return jsonify({'message': 'Subscription created', 'id': sub_id}), 201 + + +@app.route('/api/subscriptions/', methods=['PUT']) +@token_required +def update_subscription(current_user, sub_id): + data = request.get_json() + conn = get_db() + cursor = conn.cursor(dictionary=True) + cursor.execute("SELECT * FROM Subscriptions WHERE Id = %s", (sub_id,)) + existing = cursor.fetchone() + if not existing: + cursor.close() + conn.close() + return jsonify({'message': 'Not found'}), 404 + + client_name = data.get('client_name', '').strip() + client_id = data.get('client_id', existing['ClientId']) + if client_name: + cursor.execute("INSERT IGNORE INTO Clients (Name) VALUES (%s)", (client_name,)) + conn.commit() + cursor.execute("SELECT Id FROM Clients WHERE Name = %s", (client_name,)) + row = cursor.fetchone() + if row: + client_id = row['Id'] + + cursor.execute(""" + UPDATE Subscriptions SET ClientId=%s, ServiceName=%s, Amount=%s, + `Interval`=%s, StartDate=%s, EndDate=%s, IsActive=%s + WHERE Id=%s + """, ( + data.get('client_id', existing['ClientId']), + data.get('service_name', existing['ServiceName']), + data.get('amount', existing['Amount']), + data.get('interval', existing['Interval']), + data.get('start_date', existing['StartDate']), + data.get('end_date', existing['EndDate']), + data.get('is_active', existing['IsActive']), + sub_id + )) + conn.commit() + cursor.close() + conn.close() + return jsonify({'message': 'Subscription updated'}) + + +@app.route('/api/subscriptions/', methods=['DELETE']) +@token_required +def delete_subscription(current_user, sub_id): + conn = get_db() + cursor = conn.cursor() + cursor.execute("UPDATE Subscriptions SET IsActive = FALSE WHERE Id = %s", (sub_id,)) + conn.commit() + cursor.close() + conn.close() + return jsonify({'message': 'Subscription deactivated'}) + + +@app.route('/api/subscriptions//record-payment', methods=['POST']) +@token_required +def record_subscription_payment(current_user, sub_id): + conn = get_db() + cursor = conn.cursor(dictionary=True) + cursor.execute(""" + SELECT s.*, c.Name AS ClientName FROM Subscriptions s + JOIN Clients c ON c.Id = s.ClientId WHERE s.Id = %s + """, (sub_id,)) + sub = cursor.fetchone() + if not sub or not sub['IsActive']: + cursor.close() + conn.close() + return jsonify({'message': 'Subscription not found or inactive'}), 404 + + today = datetime.date.today() + cursor.execute(""" + INSERT INTO Transactions (Type, TransactionDate, ClientId, Amount, PaymentMethod, Description, SubscriptionId) + VALUES ('Income', %s, %s, %s, 'Other', %s, %s) + """, (today, sub['ClientId'], sub['Amount'], + f"Subscription: {sub['ServiceName']} (manual payment)", sub_id)) + + next_date = advance_billing_date(today, sub['Interval']) + if sub['EndDate'] and next_date > sub['EndDate']: + cursor.execute("UPDATE Subscriptions SET NextBillingDate = %s, IsActive = FALSE WHERE Id = %s", + (next_date, sub_id)) + else: + cursor.execute("UPDATE Subscriptions SET NextBillingDate = %s WHERE Id = %s", (next_date, sub_id)) + + conn.commit() + cursor.close() + conn.close() + return jsonify({'message': 'Payment recorded'}) + + +# --- Frontend Routes --- + +@app.route('/') +def login_page(): + return render_template('login.html') + + +@app.route('/dashboard') +def dashboard_page(): + return render_template('dashboard.html') + + +if __name__ == '__main__': + is_reloader = os.environ.get('WERKZEUG_RUN_MAIN') == 'true' + if not app.debug or is_reloader: + start_scheduler() + app.run(host='0.0.0.0', port=5740, debug=True, use_reloader=True) diff --git a/setup_db.py b/setup_db.py new file mode 100644 index 0000000..d74273b --- /dev/null +++ b/setup_db.py @@ -0,0 +1,69 @@ +import mysql.connector, bcrypt + +conn = mysql.connector.connect(host='10.7.0.1', user='nick', password='nick9924Tucker9924##', database='TonjumTechRecords') +cursor = conn.cursor() + +cursor.execute(""" +CREATE TABLE IF NOT EXISTS Users ( + Id INT AUTO_INCREMENT PRIMARY KEY, + Username VARCHAR(50) NOT NULL UNIQUE, + PasswordHash VARCHAR(255) NOT NULL, + CreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP +) +""") + +cursor.execute(""" +CREATE TABLE IF NOT EXISTS Clients ( + Id INT AUTO_INCREMENT PRIMARY KEY, + Name VARCHAR(255) NOT NULL UNIQUE, + ContactName VARCHAR(255) DEFAULT NULL, + Phone VARCHAR(50) DEFAULT NULL, + Email VARCHAR(255) DEFAULT NULL, + Address TEXT DEFAULT NULL, + CreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP +) +""") + +cursor.execute(""" +CREATE TABLE IF NOT EXISTS Subscriptions ( + Id INT AUTO_INCREMENT PRIMARY KEY, + ClientId INT NOT NULL, + ServiceName VARCHAR(255) NOT NULL, + Amount DECIMAL(12,2) NOT NULL, + `Interval` ENUM("Weekly","Monthly","Quarterly","Annually") NOT NULL, + StartDate DATE NOT NULL, + EndDate DATE NULL, + NextBillingDate DATE NOT NULL, + IsActive BOOLEAN DEFAULT TRUE, + CreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP, + UpdatedAt DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (ClientId) REFERENCES Clients(Id) ON DELETE CASCADE +) +""") + +cursor.execute(""" +CREATE TABLE IF NOT EXISTS Transactions ( + Id INT AUTO_INCREMENT PRIMARY KEY, + Type ENUM("Income","Expense") NOT NULL, + TransactionDate DATE NOT NULL, + ClientId INT, + Amount DECIMAL(12,2) NOT NULL, + PaymentMethod ENUM("Cash","Venmo","Check","Other","Card","Stripe") NOT NULL, + Description TEXT, + SubscriptionId INT NULL, + ImageData LONGBLOB, + ImageContentType VARCHAR(100), + CreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP, + UpdatedAt DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (ClientId) REFERENCES Clients(Id) ON DELETE SET NULL, + FOREIGN KEY (SubscriptionId) REFERENCES Subscriptions(Id) ON DELETE SET NULL +) +""") + +pw_hash = bcrypt.hashpw('nick9924Tucker9924##'.encode(), bcrypt.gensalt()).decode() +cursor.execute("INSERT IGNORE INTO Users (Username, PasswordHash) VALUES (%s, %s)", ('nick', pw_hash)) + +conn.commit() +cursor.close() +conn.close() +print('All tables created and user seeded OK') diff --git a/templates/dashboard.html b/templates/dashboard.html new file mode 100644 index 0000000..209c0ec --- /dev/null +++ b/templates/dashboard.html @@ -0,0 +1,696 @@ + + + + + + Tonjum Technologies - Dashboard + + + + + + + +
+ + + +
+ + + + +
+ + +
+
+
+

Total Profit

+

$0.00

+
+
+

Last 365 Days

+

$0.00

+
+
+

Last 30 Days

+

$0.00

+
+
+

Projected Year

+

$0.00

+
+
+ +
+
+

Monthly Income vs Expenses

+
+
+
+

Subscriptions

+

Active Subscriptions

+

0

+

Monthly Recurring

+

$0.00

+
+
+
+ + + + + + + + + + + + +
+ + + + + + + diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..a8ffcdf --- /dev/null +++ b/templates/login.html @@ -0,0 +1,82 @@ + + + + + + Tonjum Technologies - Login + + + + +
+
+
+ + + +
+

Tonjum Technologies

+

Business Records

+
+
+
+
+ + +
+
+ + +
+ + +
+
+
+ + +