init commit

This commit is contained in:
2025-08-31 17:55:26 +03:00
commit 876ddec94a
78 changed files with 11999 additions and 0 deletions

BIN
transportmanager/server/.DS_Store vendored Normal file

Binary file not shown.

View File

View File

@@ -0,0 +1,53 @@
from flask import Blueprint, jsonify, request
from flask_jwt_extended import jwt_required
from models.subscription import Subscription
admin_subscription_bp = Blueprint("admin_subscription", __name__, url_prefix="/admin/subscriptions")
# List all subscriptions
@admin_subscription_bp.route("", methods=["GET"])
@jwt_required()
def list_subscriptions():
subscription_model = Subscription()
# Optional: if you want, you can add pagination here later!
subscriptions = subscription_model.get_all()
return jsonify(subscriptions), 200
# Update subscription status
@admin_subscription_bp.route("/update_status", methods=["POST"])
@jwt_required()
def update_subscription_status():
data = request.json
subscription_id = data.get("subscription_id")
new_status = data.get("status")
if not subscription_id or not new_status:
return jsonify({"error": "Missing subscription_id or status"}), 400
subscription_model = Subscription()
updated_rows = subscription_model.update_status(subscription_id, new_status)
if updated_rows == 0:
return jsonify({"error": "Subscription not found"}), 404
return jsonify({"message": "Subscription status updated."}), 200
# Cancel subscription
@admin_subscription_bp.route("/cancel", methods=["POST"])
@jwt_required()
def cancel_subscription():
data = request.json
subscription_id = data.get("subscription_id")
if not subscription_id:
return jsonify({"error": "Missing subscription_id"}), 400
subscription_model = Subscription()
updated_rows = subscription_model.update_status(subscription_id, "cancelled")
if updated_rows == 0:
return jsonify({"error": "Subscription not found"}), 404
return jsonify({"message": "Subscription cancelled."}), 200

View File

@@ -0,0 +1,40 @@
from flask import Blueprint, jsonify, request
from flask_jwt_extended import jwt_required
from models.user import Users
admin_user_bp = Blueprint("admin_user", __name__, url_prefix="/admin/users")
# Get all users with role "user"
@admin_user_bp.route("", methods=["GET"])
@jwt_required()
def get_all_users():
users_model = Users()
users = users_model.get_all_users_with_role("user")
return jsonify(users), 200
# Get a single user by ID
@admin_user_bp.route("/<int:user_id>", methods=["GET"])
@jwt_required()
def get_user(user_id):
users_model = Users()
user = users_model.get_user_by_id(user_id)
if not user:
return jsonify({"error": "User not found"}), 404
return jsonify(user), 200
# Update a user
@admin_user_bp.route("/update", methods=["POST"])
@jwt_required()
def update_user():
if not request.is_json:
print("Content-Type received:", request.content_type)
return jsonify({"error": "Invalid content type, must be application/json"}), 415
data = request.get_json()
if not data.get("user_id"):
return jsonify({"error": "Missing user_id"}), 400
users_model = Users()
users_model.update_user(data)
return jsonify({"message": "User updated successfully."}), 200

View File

@@ -0,0 +1,93 @@
import os
from flask import Flask, jsonify
from routes.auth import auth_bp
from flask_jwt_extended import JWTManager
from routes.profile import profile_bp
from routes.clients import clients_bp
from routes.transporters import transporters_bp
from routes.destinations import destinations_bp
from routes.orders_out import orders_bp
from routes.ouders_in import orders_in_bp
from routes.report import report_bp
from admin.subscription import admin_subscription_bp
from routes.subscription import subscription_bp
from admin.tenants import admin_user_bp
from apscheduler.schedulers.background import BackgroundScheduler
from models.subscription import Subscription
from flask_cors import CORS
app = Flask(__name__)
CORS(
app,
resources={r"/*": {"origins": [os.getenv("WEB_ORIGIN", "*")]}},
allow_headers=["Authorization", "Content-Type"],
expose_headers=["Content-Type"],
)
@app.get("/db/check")
def db_check():
try:
import psycopg # psycopg3 client
except Exception as e:
return {"ok": False, "error": f"psycopg not available: {e}"}, 500
dsn = os.getenv("DATABASE_URL")
if not dsn:
return {"ok": False, "error": "DATABASE_URL not set"}, 500
try:
with psycopg.connect(dsn, connect_timeout=5) as conn:
with conn.cursor() as cur:
cur.execute("SELECT version();")
ver = cur.fetchone()[0]
return {"ok": True, "version": ver}, 200
except Exception as e:
return {"ok": False, "error": str(e)}, 500
@app.get("/health")
def health():
return {"ok": True}, 200
app.config["JWT_SECRET_KEY"] = os.environ.get("JWT_SECRET_KEY", "your-jwt-secret")
app.config["JWT_TOKEN_LOCATION"] = ["headers", "query_string"]
app.config["JWT_QUERY_STRING_NAME"] = "token"
jwt = JWTManager(app)
env = os.environ.get("FLASK_ENV", "development")
# Register blueprints
app.register_blueprint(auth_bp, url_prefix="/auth")
app.register_blueprint(profile_bp, url_prefix="/profile")
app.register_blueprint(clients_bp)
app.register_blueprint(transporters_bp, url_prefix="/transporters")
app.register_blueprint(destinations_bp, url_prefix="/destinations")
app.register_blueprint(orders_bp, url_prefix="/orders")
app.register_blueprint(orders_in_bp, url_prefix="/orders_in")
app.register_blueprint(report_bp, url_prefix="/report")
app.register_blueprint(admin_subscription_bp)
app.register_blueprint(subscription_bp)
app.register_blueprint(admin_user_bp)
def update_subscription_statuses_job():
print("[Scheduler] Running daily subscription status check...")
subscription_model = Subscription()
subscription_model.update_subscription_statuses()
RUN_SCHEDULER = os.getenv("RUN_SCHEDULER", "1") == "1"
if RUN_SCHEDULER:
scheduler = BackgroundScheduler(daemon=True)
scheduler.add_job(func=update_subscription_statuses_job, trigger="interval", days=1)
scheduler.start()
if __name__ == "__main__":
if env != "production":
# Avoid running the scheduler twice in development mode
import logging
logging.getLogger("apscheduler").setLevel(logging.DEBUG)
app.run(debug=(env == "development"), use_reloader=False)

View File

@@ -0,0 +1,70 @@
import os
import sqlite3
from pathlib import Path
try:
import psycopg
except ImportError:
psycopg = None
DB_TYPE = 'sqlite3'
BASE_DIR = Path(__file__).resolve().parent
SCHEMA_PATH = BASE_DIR / "schema.sql" if DB_TYPE != 'sqlite3' else BASE_DIR / "schema_sqlite.sql"
DATABASE_URL = os.getenv("DATABASE_URL")
SQLITE_PATH = BASE_DIR / "instance" / "dev.db"
def is_postgres():
return DATABASE_URL and DATABASE_URL.lower().startswith("postgres")
def get_connection():
if is_postgres():
if psycopg is None:
raise RuntimeError("psycopg is required for PostgreSQL but not installed.")
return psycopg.connect(DATABASE_URL, autocommit=True)
else:
SQLITE_PATH.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(str(SQLITE_PATH))
conn.execute("PRAGMA foreign_keys = ON;")
return conn
def _iter_sql_statements(script: str):
buffer = []
for line in script.splitlines():
buffer.append(line)
if line.strip().endswith(";"):
stmt = "\n".join(buffer).strip()
buffer = []
if stmt:
yield stmt
tail = "\n".join(buffer).strip()
if tail:
yield tail
def db_init():
if not SCHEMA_PATH.is_file():
raise FileNotFoundError(f"Schema file not found: {SCHEMA_PATH}")
with open(SCHEMA_PATH, "r", encoding="utf-8") as f:
sql_script = f.read()
conn = get_connection()
try:
if is_postgres():
with conn.cursor() as cur:
for stmt in _iter_sql_statements(sql_script):
cur.execute(stmt)
else:
conn.executescript(sql_script)
conn.commit()
print("Database initialized successfully.")
finally:
conn.close()
if __name__ == "__main__":
db_init()

Binary file not shown.

View File

@@ -0,0 +1,79 @@
from datetime import datetime
from database import get_connection, is_postgres
class Clients:
def __init__(self):
# Parameter style: Postgres uses %s, SQLite uses ?
self.ph = "%s" if is_postgres() else "?"
def client_to_dict(self, row):
return {
"id": row[0],
"user_id": row[1],
"name": row[2],
"address": row[3],
"register_number": row[4],
"contact_person": row[5],
"phone": row[6],
"email": row[7],
"vat":row[8],
"created_at": row[9],
}
def create(self, user_id, name, address, register_number, contact_person, phone=None, email=None, vat=None):
created_at = datetime.now().isoformat()
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
f"""
INSERT INTO clients (user_id, name, address, register_number, contact_person, phone, email, vat, created_at)
VALUES ({self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph})
""",
(user_id, name, address, register_number, contact_person, phone, email, vat, created_at),
)
if hasattr(conn, "commit"):
conn.commit()
def get_all_by_user(self, user_id):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
f"SELECT * FROM clients WHERE user_id = {self.ph}",
(user_id,),
)
return [self.client_to_dict(row) for row in cur.fetchall()]
def get_by_id(self, client_id):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
f"SELECT * FROM clients WHERE id = {self.ph}",
(client_id,),
)
row = cur.fetchone()
return self.client_to_dict(row) if row else None
def update(self, client_id, name, address, register_number, contact_person, phone=None, email=None, vat = None):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
f"""
UPDATE clients
SET name={self.ph}, address={self.ph}, register_number={self.ph}, contact_person={self.ph},
phone={self.ph}, email={self.ph}, vat={self.ph}
WHERE id={self.ph}
""",
(name, address, register_number, contact_person, phone, email, vat, client_id),
)
if hasattr(conn, "commit"):
conn.commit()
def delete(self, client_id):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
f"DELETE FROM clients WHERE id={self.ph}",
(client_id,),
)
if hasattr(conn, "commit"):
conn.commit()

View File

@@ -0,0 +1,92 @@
from datetime import datetime
from database import get_connection, is_postgres
class Destinations:
def __init__(self):
# Parameter style: Postgres uses %s, SQLite uses ?
self.ph = "%s" if is_postgres() else "?"
def destination_to_dict(self, row):
destination = {
"id": row[0],
"user_id": row[1],
"name": row[2],
"address": row[3],
"latitude": row[4],
"longitude": row[5],
"created_at": row[6]
}
return destination
def create(self, user_id, name, address):
created_at = datetime.now().isoformat()
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
f"""
INSERT INTO destinations (user_id, name, address, created_at)
VALUES ({self.ph}, {self.ph}, {self.ph}, {self.ph})
""",
(user_id, name, address, created_at),
)
if hasattr(conn, "commit"):
conn.commit()
return cur.lastrowid if hasattr(cur, "lastrowid") else None
def update(self, id, user_id, name, address):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
f"""
UPDATE destinations
SET user_id = {self.ph}, name = {self.ph}, address = {self.ph}
WHERE id = {self.ph}
""",
(user_id, name, address, id),
)
if hasattr(conn, "commit"):
conn.commit()
def add_gps_coordinates(self, id, latitude, longitude):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
f"""
UPDATE destinations
SET latitude = {self.ph}, longitude = {self.ph}
WHERE id = {self.ph}
""",
(latitude, longitude, id),
)
if hasattr(conn, "commit"):
conn.commit()
def delete(self, id):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
f"DELETE FROM destinations WHERE id = {self.ph}",
(id,),
)
if hasattr(conn, "commit"):
conn.commit()
def get_all_by_user(self, user_id):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
f"SELECT * FROM destinations WHERE user_id = {self.ph} ORDER BY created_at DESC",
(user_id,),
)
rows = cur.fetchall()
return [self.destination_to_dict(row) for row in rows]
def get_by_id(self, id):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
f"SELECT * FROM destinations WHERE id = {self.ph}",
(id,),
)
row = cur.fetchone()
return self.destination_to_dict(row) if row else None

View File

@@ -0,0 +1,193 @@
from datetime import datetime
from database import get_connection, is_postgres
class OrdersIn:
def __init__(self):
# Parameter placeholder per backend
self.ph = "%s" if is_postgres() else "?"
def order_to_dict(self, row):
return {
"id": row[0],
"order_number": row[1],
"user_id": row[2],
"client_id": row[3],
"products_description": row[4],
"ldb_quantity": row[5],
"kg_quantity": row[6],
"track_reg_number": row[7],
"trailer_reg_number": row[8],
"received_price": row[9],
"created_at": row[10],
}
def order_point_to_dict(self, row):
return {
"id": row[0],
"order_id": row[1],
"destination_id": row[2],
"informatins": row[3],
"point_data": row[4],
"point_hour": row[5],
'point_type': row[6],
}
def create_order(self, data):
created_at = datetime.now().isoformat()
with get_connection() as conn:
cur = conn.cursor()
returning = " RETURNING id" if is_postgres() else ""
cur.execute(
f"""
INSERT INTO orders_in
(user_id, client_id, products_description, received_price, order_number,
ldb_quantity, kg_quantity, track_reg_number, trailer_reg_number, created_at)
VALUES ({self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}){returning}
""",
(
data["user_id"],
data["client_id"],
data['products_description'],
data["received_price"],
data["order_number"],
data["ldb_quantity"],
data["kg_quantity"],
data["track_reg_number"],
data["trailer_reg_number"],
created_at,
),
)
new_id = cur.fetchone()[0] if is_postgres() else getattr(cur, "lastrowid", None)
if hasattr(conn, "commit"):
conn.commit()
return new_id
def update_order(self, data):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
f"""
UPDATE orders_in SET
user_id = {self.ph}, client_id = {self.ph}, products_description = {self.ph},
received_price = {self.ph}, order_number = {self.ph},
ldb_quantity = {self.ph}, kg_quantity = {self.ph}, track_reg_number = {self.ph},
trailer_reg_number = {self.ph}
WHERE id = {self.ph}
""",
(
data["user_id"],
data["client_id"],
data["products_description"],
data["received_price"],
data["order_number"],
data["ldb_quantity"],
data["kg_quantity"],
data["track_reg_number"],
data["trailer_reg_number"],
data["id"],
),
)
if hasattr(conn, "commit"):
conn.commit()
def delete_order(self, order_id):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(f"DELETE FROM orders_in WHERE id = {self.ph}", (order_id,))
if hasattr(conn, "commit"):
conn.commit()
def get_order_by_id(self, order_id):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(f"SELECT * FROM orders_in WHERE id = {self.ph}", (order_id,))
row = cur.fetchone()
return self.order_to_dict(row) if row else None
def get_orders_by_user(self, user_id):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
f"SELECT * FROM orders_in WHERE user_id = {self.ph} ORDER BY created_at DESC",
(user_id,),
)
rows = cur.fetchall()
return [self.order_to_dict(row) for row in rows]
def create_order_point(self, data):
with get_connection() as conn:
cur = conn.cursor()
returning = " RETURNING id" if is_postgres() else ""
cur.execute(
f"""
INSERT INTO order_in_points
(order_id, destination_id, informatins, point_data, point_hour, point_type)
VALUES ({self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}){returning}
""",
(
data["order_id"],
data["destination_id"],
data["informatins"],
data["point_data"],
data["point_hour"],
data['point_type'],
),
)
new_id = cur.fetchone()[0] if is_postgres() else getattr(cur, "lastrowid", None)
if hasattr(conn, "commit"):
conn.commit()
return new_id
def update_order_point(self, data):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
f"""
UPDATE order_in_points SET
order_id = {self.ph}, destination_id = {self.ph}, informatins = {self.ph},
point_data = {self.ph}, point_hour = {self.ph}, point_type = {self.ph}
WHERE id = {self.ph}
""",
(
data["order_id"],
data["destination_id"],
data["informatins"],
data["point_data"],
data["point_hour"],
data['point_type'],
data["id"],
),
)
if hasattr(conn, "commit"):
conn.commit()
def delete_order_point(self, point_id):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(f"DELETE FROM order_in_points WHERE id = {self.ph}", (point_id,))
if hasattr(conn, "commit"):
conn.commit()
def delete_points_by_order_id(self, order_id):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(f"DELETE FROM order_in_points WHERE order_id = {self.ph}", (order_id,))
if hasattr(conn, "commit"):
conn.commit()
def get_order_point_by_id(self, point_id):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(f"SELECT * FROM order_in_points WHERE id = {self.ph}", (point_id,))
row = cur.fetchone()
return self.order_point_to_dict(row) if row else None
def get_order_points_by_order(self, order_id):
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
f"SELECT * FROM order_in_points WHERE order_id = {self.ph} ORDER BY id",
(order_id,),
)
rows = cur.fetchall()
return [self.order_point_to_dict(row) for row in rows]

View File

@@ -0,0 +1,209 @@
from datetime import datetime
from database import get_connection, is_postgres
class OrdersOut:
def __init__(self):
# Parameter placeholder per backend
self.ph = "%s" if is_postgres() else "?"
def order_to_dict(self, row):
return {
"id": row[0],
"order_number": row[1],
"user_id": row[2],
"client_id": row[3],
"transporter_id": row[4],
"products_description": row[5],
"ldb_quantity": row[6],
"kg_quantity": row[7],
"track_reg_number": row[8],
"trailer_reg_number": row[9],
"received_price": row[10],
"paid_price": row[11],
"created_at": row[12],
"status": row[13]
}
def order_point_to_dict(self, row):
return {
"id": row[0],
"order_id": row[1],
"destination_id": row[2],
"informatins": row[3],
"point_data": row[4],
"point_hour": row[5],
'point_type': row[6],
}
def create_order(self, data):
created_at = datetime.now().isoformat()
with get_connection() as conn:
cursor = conn.cursor()
returning = " RETURNING id" if is_postgres() else ""
cursor.execute(
f"""
INSERT INTO orders_out
(user_id, client_id, transporter_id, products_description, received_price, paid_price, order_number,
ldb_quantity, kg_quantity, track_reg_number, trailer_reg_number, created_at)
VALUES ({self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}){returning}
""",
(
data["user_id"],
data["client_id"],
data["transporter_id"],
data['products_description'],
data["received_price"],
data["paid_price"],
data["order_number"],
data["ldb_quantity"],
data["kg_quantity"],
data["track_reg_number"],
data["trailer_reg_number"],
created_at,
),
)
new_id = cursor.fetchone()[0] if is_postgres() else getattr(cursor, "lastrowid", None)
if hasattr(conn, "commit"):
conn.commit()
return new_id
def update_order(self, data):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
UPDATE orders_out SET client_id = {self.ph}, transporter_id = {self.ph}, products_description = {self.ph},
received_price = {self.ph}, paid_price = {self.ph}, order_number = {self.ph},
ldb_quantity = {self.ph}, kg_quantity = {self.ph}, track_reg_number = {self.ph},
trailer_reg_number = {self.ph}
WHERE id = {self.ph}
""",
(
data["client_id"],
data["transporter_id"],
data["products_description"],
data["received_price"],
data["paid_price"],
data["order_number"],
data["ldb_quantity"],
data["kg_quantity"],
data["track_reg_number"],
data["trailer_reg_number"],
data["id"],
),
)
if hasattr(conn, "commit"):
conn.commit()
def cancel_order(self, order_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"UPDATE orders_out SET order_status = {self.ph} WHERE id = {self.ph}",
('cancelled', order_id,),
)
if hasattr(conn, "commit"):
conn.commit()
def delete_order(self, order_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(f"DELETE FROM orders_out WHERE id = {self.ph}", (order_id,))
if hasattr(conn, "commit"):
conn.commit()
def get_order_by_id(self, order_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(f"SELECT * FROM orders_out WHERE id = {self.ph}", (order_id,))
row = cursor.fetchone()
return self.order_to_dict(row) if row else None
def get_orders_by_user(self, user_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"SELECT * FROM orders_out WHERE user_id = {self.ph} ORDER BY created_at DESC",
(user_id,),
)
rows = cursor.fetchall()
return [self.order_to_dict(row) for row in rows]
def create_order_point(self, data):
with get_connection() as conn:
cursor = conn.cursor()
returning = " RETURNING id" if is_postgres() else ""
cursor.execute(
f"""
INSERT INTO order_out_points
(order_id, destination_id, informatins, point_data, point_hour, point_type)
VALUES ({self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}){returning}
""",
(
data["order_id"],
data["destination_id"],
data["informatins"],
data["point_data"],
data["point_hour"],
data['point_type']
),
)
# keep behavior similar: no return expected, but commit consistently
if is_postgres():
_ = cursor.fetchone() # consume RETURNING if present
if hasattr(conn, "commit"):
conn.commit()
def update_order_point(self, data):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
UPDATE order_out_points SET
order_id = {self.ph}, destination_id = {self.ph}, informatins = {self.ph}, point_data = {self.ph}, point_hour = {self.ph}, point_type = {self.ph}
WHERE id = {self.ph}
""",
(
data["order_id"],
data["destination_id"],
data["informatins"],
data["point_data"],
data["point_hour"],
data['point_type'],
data["id"],
),
)
if hasattr(conn, "commit"):
conn.commit()
def delete_order_point(self, point_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(f"DELETE FROM order_out_points WHERE id = {self.ph}", (point_id,))
if hasattr(conn, "commit"):
conn.commit()
def delete_points_by_order_id(self, order_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(f"DELETE FROM order_out_points WHERE order_id = {self.ph}", (order_id,))
if hasattr(conn, "commit"):
conn.commit()
def get_order_point_by_id(self, point_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(f"SELECT * FROM order_out_points WHERE id = {self.ph}", (point_id,))
row = cursor.fetchone()
return self.order_point_to_dict(row) if row else None
def get_order_points_by_order(self, order_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"SELECT * FROM order_out_points WHERE order_id = {self.ph} ORDER BY id",
(order_id,),
)
rows = cursor.fetchall()
return [self.order_point_to_dict(row) for row in rows]

View File

@@ -0,0 +1,130 @@
from datetime import datetime, timedelta
from database import get_connection, is_postgres
class Subscription:
def __init__(self):
# Parameter placeholder per backend
self.ph = "%s" if is_postgres() else "?"
def subscription_to_dict(self, row):
return {
"id": row[0],
"user_id": row[1], #company id
"plan": row[2],
"start_date": row[3],
"end_date": row[4],
"status": row[5],
"register_number": row[6],
"created_at": row[7],
}
def create(self, user_id, plan, start_date, end_date, register_number, status="active"):
created_at = datetime.now().isoformat()
with get_connection() as conn:
cursor = conn.cursor()
returning = " RETURNING id" if is_postgres() else ""
cursor.execute(
f"""
INSERT INTO subscriptions (user_id, plan, start_date, end_date, status, register_number, created_at)
VALUES ({self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}){returning}
""",
(user_id, plan, start_date, end_date, status, register_number, created_at),
)
new_id = cursor.fetchone()[0] if is_postgres() else getattr(cursor, "lastrowid", None)
if hasattr(conn, "commit"):
conn.commit()
return new_id
def get_by_user_id(self, user_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
SELECT * FROM subscriptions
WHERE user_id = {self.ph}
ORDER BY start_date DESC
""",
(user_id,),
)
rows = cursor.fetchall()
return [self.subscription_to_dict(row) for row in rows]
def get_by_id(self, subscription_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
SELECT * FROM subscriptions
WHERE id = {self.ph}
""",
(subscription_id,),
)
row = cursor.fetchone()
return self.subscription_to_dict(row) if row else None
def update_status(self, subscription_id, new_status):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
UPDATE subscriptions
SET status = {self.ph}
WHERE id = {self.ph}
""",
(new_status, subscription_id),
)
if hasattr(conn, "commit"):
conn.commit()
return cursor.rowcount if hasattr(cursor, "rowcount") else 0
def delete(self, subscription_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
DELETE FROM subscriptions
WHERE id = {self.ph}
""",
(subscription_id,),
)
if hasattr(conn, "commit"):
conn.commit()
return cursor.rowcount if hasattr(cursor, "rowcount") else 0
def get_first_2_months_subscription_for_register_number(self, register_number):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
SELECT * FROM subscriptions
WHERE register_number = {self.ph} AND plan = 'first_2_months' AND status = 'active'
""",
(register_number,),
)
row = cursor.fetchone()
return self.subscription_to_dict(row) if row else None
def get_all(self):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"""
SELECT * FROM subscriptions
ORDER BY created_at DESC
"""
)
rows = cursor.fetchall()
return [self.subscription_to_dict(row) for row in rows]
def update_subscription_statuses(self):
now = datetime.now()
subscriptions = self.get_all()
for sub in subscriptions:
end_date = datetime.fromisoformat(sub["end_date"])
days_left = (end_date - now).days
if days_left < 0 and sub["status"] != "expired":
self.update_status(sub["id"], "expired")
elif 0 <= days_left <= 5 and sub["status"] != "less_than_5_days":
self.update_status(sub["id"], "less_than_5_days")

View File

@@ -0,0 +1,87 @@
from datetime import datetime
from database import get_connection, is_postgres
class Transporters:
def __init__(self):
# Parameter placeholder depending on backend
self.ph = "%s" if is_postgres() else "?"
def transporter_to_dict(self, row):
return {
"id": row[0],
"user_id": row[1],
"name": row[2],
"address": row[3],
"register_number": row[4],
"contact_person": row[5],
"phone": row[6],
"email": row[7],
"vat": row[8],
"created_at": row[9]
}
def create_transporter(self, name, address, register_number, contact_person, phone, email, vat, user_id):
created_at = datetime.now().isoformat()
returning = " RETURNING id" if is_postgres() else ""
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
INSERT INTO transporters
(name, address, register_number, contact_person, phone, email, vat, created_at, user_id)
VALUES ({self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph}){returning}
""",
(name, address, register_number, contact_person, phone, email, vat, created_at, user_id),
)
transporter_id = cursor.fetchone()[0] if is_postgres() else cursor.lastrowid
if hasattr(conn, "commit"):
conn.commit()
return transporter_id
def get_all_transporters_by_user(self, user_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"SELECT * FROM transporters WHERE user_id = {self.ph} ORDER BY created_at DESC",
(user_id,),
)
rows = cursor.fetchall()
return [self.transporter_to_dict(row) for row in rows]
def get_transporter_by_id(self, transporter_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"SELECT * FROM transporters WHERE id = {self.ph}",
(transporter_id,),
)
row = cursor.fetchone()
return self.transporter_to_dict(row) if row else None
def update_transporter(self, transporter_id, name, address, register_number, contact_person, phone, email, vat):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
UPDATE transporters SET
name = {self.ph},
address = {self.ph},
register_number = {self.ph},
contact_person = {self.ph},
phone = {self.ph},
email = {self.ph},
vat = {self.ph}
WHERE id = {self.ph}
""",
(name, address, register_number, contact_person, phone, email, vat, transporter_id),
)
if hasattr(conn, "commit"):
conn.commit()
def delete_transporter(self, transporter_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(f"DELETE FROM transporters WHERE id = {self.ph}", (transporter_id,))
if hasattr(conn, "commit"):
conn.commit()

View File

@@ -0,0 +1,194 @@
from datetime import datetime
from database import get_connection, is_postgres
class Users:
def __init__(self):
self.ph = "%s" if is_postgres() else "?"
def user_to_dict(self, row):
user = {
'id': row[0],
'name': row[1],
'contact_name': row[2],
'email': row[3],
'password_hash': row[4],
'phone': row[5],
'register_number': row[6],
'vat':row[7],
'address': row[8],
'logo_filename': row[9],
'terms': row[10],
'first_order_number': row[11],
'created_at': row[12],
'otp_code': row[13],
'otp_expiration': row[14],
'user_role': row[15]
}
return user
def email_to_dict(self, row):
email = {
'id': row[0],
'user_id': row[1],
'smtp_host': row[2],
'smtp_port': row[3],
'smtp_user': row[4],
'created_at': row[5]
}
return email
def get_user_by_email(self, email):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(f"SELECT * FROM users WHERE email = {self.ph}", (email,))
row = cursor.fetchone()
return self.user_to_dict(row) if row else None
def get_user_by_id(self, user_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(f"SELECT * FROM users WHERE id = {self.ph}", (user_id,))
row = cursor.fetchone()
return self.user_to_dict(row) if row else None
def insert_user(self, name, email, password_hash):
created_at = datetime.now().isoformat()
with get_connection() as conn:
cursor = conn.cursor()
returning = "RETURNING id" if is_postgres() else ""
query = f"""
INSERT INTO users (
name, email, password_hash, created_at
) VALUES ({self.ph}, {self.ph}, {self.ph}, {self.ph}) {returning}
"""
cursor.execute(query, (name, email, password_hash, created_at))
inserted_id = None
if is_postgres():
inserted_id = cursor.fetchone()[0]
else:
inserted_id = cursor.lastrowid
if hasattr(conn, "commit"):
conn.commit()
return inserted_id
def update_user_otp(self, user_id, otp_code, expiration):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
UPDATE users
SET otp_code = {self.ph}, otp_expiration = {self.ph}
WHERE id = {self.ph}
""",
(otp_code, expiration.isoformat(), user_id)
)
if hasattr(conn, "commit"):
conn.commit()
def clear_user_otp(self, user_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
UPDATE users
SET otp_code = NULL, otp_expiration = NULL
WHERE id = {self.ph}
""",
(user_id,)
)
if hasattr(conn, "commit"):
conn.commit()
def update_user_password(self, user_id, new_password_hash):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
UPDATE users
SET password_hash = {self.ph}
WHERE id = {self.ph}
""",
(new_password_hash, user_id)
)
if hasattr(conn, "commit"):
conn.commit()
def update_user(self, data):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
UPDATE users
SET name = {self.ph}, contact_name = {self.ph}, email = {self.ph}, phone = {self.ph}, register_number = {self.ph}, vat = {self.ph}, address = {self.ph}, logo_filename = {self.ph}, terms = {self.ph}, first_order_number = {self.ph}
WHERE id = {self.ph}
""",
(
data['name'],
data['contact_name'],
data['email'],
data['phone'],
data['register_number'],
data['vat'],
data['address'],
data['logo_filename'],
data['terms'],
data['first_order_number'],
data['user_id']
)
)
if hasattr(conn, "commit"):
conn.commit()
def update_user_logo(self, data):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
UPDATE users SET logo_filename = {self.ph} WHERE id = {self.ph}
""",
(data['logo_filename'], data['user_id'])
)
if hasattr(conn, "commit"):
conn.commit()
def get_all_users_with_role(self, role='user'):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(f"SELECT * FROM users WHERE user_role = {self.ph}", (role,))
rows = cursor.fetchall()
return [self.user_to_dict(row) for row in rows]
#--- email credentials ---
def insert_email_credentials(self, user_id, smtp_host, smtp_port, smtp_user):
created_at = datetime.now().isoformat()
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
INSERT INTO email (
user_id, smtp_host, smtp_port, smtp_user, created_at
) VALUES ({self.ph}, {self.ph}, {self.ph}, {self.ph}, {self.ph})
""",
(user_id, smtp_host, smtp_port, smtp_user, created_at)
)
if hasattr(conn, "commit"):
conn.commit()
def get_email_credentials(self, user_id):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(f"SELECT * FROM email WHERE user_id = {self.ph}", (user_id,))
row = cursor.fetchone()
return self.email_to_dict(row) if row else None
def update_email_credentials(self, user_id, smtp_host, smtp_port, smtp_user):
with get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
f"""
UPDATE email SET smtp_host={self.ph}, smtp_port={self.ph}, smtp_user={self.ph} WHERE id = {self.ph}
""",
(smtp_host, smtp_port, smtp_user, user_id)
)
if hasattr(conn, "commit"):
conn.commit()

View File

@@ -0,0 +1,23 @@
# --- Flask API (server) requirements ---
# Web framework
Flask==2.3.3
Flask-Cors==4.0.0
Flask-JWT-Extended==4.6.0
# WSGI server
gunicorn==22.0.0
# Database (Postgres)
psycopg[binary]==3.2.1
# Utilities
python-dotenv==1.0.1
requests==2.32.3
# Features used by your server
geopy==2.4.1
reportlab>=3.6.12
PyPDF2==3.0.1
APScheduler==3.10.4
tzlocal==5.2

View File

@@ -0,0 +1,3 @@
from flask import Blueprint
# Placeholder for blueprint registration if needed dynamically

View File

@@ -0,0 +1,204 @@
from flask import Blueprint, request, jsonify
from werkzeug.security import generate_password_hash, check_password_hash
from utils.email import send_email, send_gmail
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity
from flask_jwt_extended import decode_token
import datetime
import random
import os
from datetime import timezone
from models.user import Users
auth_bp = Blueprint("auth", __name__)
@auth_bp.route("/register", methods=["POST"])
def register():
users = Users()
data = request.get_json()
name = data.get("name")
email = data.get("email")
password = data.get("password")
if not name or not email or not password:
return jsonify({"error": "Missing required fields"}), 400
existing_user = users.get_user_by_email(email)
if existing_user:
return jsonify({"error": "User already exists"}), 409
password_hash = generate_password_hash(password)
users.insert_user(name, email, password_hash)
return jsonify({"message": "User registered successfully!"}), 201
@auth_bp.route("/login", methods=["POST"])
def login():
users = Users()
data = request.get_json()
email = data.get("email", "").strip().lower()
password = data.get("password", "")
if not email or not password:
return jsonify({"error": "Missing email or password"}), 400
user = users.get_user_by_email(email)
if not user or not check_password_hash(user["password_hash"], password):
return jsonify({"error": "Invalid credentials"}), 401
otp_code = str(random.randint(100000, 999999))
expiration = datetime.datetime.now(timezone.utc) + datetime.timedelta(minutes=10)
users.update_user_otp(user["id"], otp_code, expiration)
send_gmail(
to_email=user["email"],
subject="Your Login Verification Code",
body=f"Your login verification code is: {otp_code}"
)
return jsonify({"message": "Verification code sent to your email."}), 200
@auth_bp.route("/verify_code", methods=["POST"])
def verify_code():
users = Users()
data = request.get_json()
email = data.get("email", "").strip().lower()
code = data.get("code", "")
if not email or not code:
return jsonify({"error": "Missing email or verification code"}), 400
user = users.get_user_by_email(email)
#-----------------------------------------------> for testing only remove in prod
#if email != 'test@test.com':
#-----------------------------------------------> for testing only remove in prod
if not user or user.get("otp_code") != code:
return jsonify({"error": "Invalid code"}), 401
exp = user.get("otp_expiration")
# Normalize to aware UTC datetime for safe comparison across SQLite (string) and Postgres (datetime)
now_utc = datetime.datetime.now(timezone.utc)
if isinstance(exp, str):
try:
exp_dt = datetime.datetime.fromisoformat(exp)
except Exception:
return jsonify({"error": "Invalid expiration format"}), 500
if exp_dt.tzinfo is None:
exp_dt = exp_dt.replace(tzinfo=timezone.utc)
else:
# Assume a datetime object from DB driver
exp_dt = exp
if exp_dt is None:
return jsonify({"error": "Missing expiration"}), 500
if exp_dt.tzinfo is None:
exp_dt = exp_dt.replace(tzinfo=timezone.utc)
if now_utc > exp_dt:
return jsonify({"error": "Verification code has expired"}), 403
users.clear_user_otp(user["id"])
access_token = create_access_token(
identity=str(user["id"]),
expires_delta=datetime.timedelta(hours=12)
)
return jsonify({
"message": "Login successful",
"access_token": access_token
}), 200
@auth_bp.route("/forgot_password", methods=["POST"])
def forgot_password():
users = Users()
data = request.get_json()
email = data.get("email", "").strip().lower()
if not email:
return jsonify({"error": "Email is required"}), 400
user = users.get_user_by_email(email)
if user:
reset_token = create_access_token(
identity=user["id"],
expires_delta=datetime.timedelta(minutes=15),
additional_claims={"purpose": "password_reset"}
)
send_gmail(
to_email=user["email"],
subject="Password Reset Request",
body=(
"Click the link to reset your password: "
f"{os.getenv('FRONTEND_BASE_URL', 'http://127.0.0.1:5100')}/reset_password?token={reset_token}"
)
)
return jsonify({"message": "If this email is registered, a reset link has been sent."}), 200
@auth_bp.route("/reset_password", methods=["POST"])
def reset_password():
users = Users()
data = request.get_json()
token = data.get("token", "")
new_password = data.get("new_password", "")
if not token or not new_password:
return jsonify({"error": "Missing token or new password"}), 400
try:
decoded_token = decode_token(token)
if decoded_token.get("purpose") != "password_reset":
return jsonify({"error": "Invalid token purpose"}), 403
except Exception:
return jsonify({"error": "Invalid or expired token"}), 403
user_id = decoded_token["sub"]
user = users.get_user_by_id(user_id)
if not user:
return jsonify({"error": "User not found"}), 404
password_hash = generate_password_hash(new_password)
users.update_user_password(user_id, password_hash)
return jsonify({"message": "Password has been reset successfully."}), 200
@auth_bp.route("/me", methods=["GET"])
@jwt_required()
def me():
users = Users()
user_id = get_jwt_identity()
user = users.get_user_by_id(user_id)
if not user:
return jsonify({"error": "User not found"}), 404
return jsonify({
"id": user["id"],
"name": user["name"],
"contact_name": user['contact_name'],
"email": user["email"],
"phone": user["phone"],
"register_number": user["register_number"],
"vat":user["vat"],
"address": user["address"],
"logo_filename": user["logo_filename"],
"terms": user["terms"],
"first_order_number": user["first_order_number"],
"created_at": user["created_at"],
"user_role": user["user_role"]
}), 200
# Validate token endpoint
@auth_bp.route("/validate_token", methods=["GET"])
@jwt_required()
def validate_token():
users = Users()
user_id = get_jwt_identity()
user = users.get_user_by_id(user_id)
if not user:
return jsonify({"error": "User not found"}), 404
return jsonify({"message": "Token is valid"}), 200

View File

@@ -0,0 +1,65 @@
from flask import Blueprint, request, jsonify
from models.client import Clients
from flask_jwt_extended import jwt_required, get_jwt_identity
clients_bp = Blueprint("clients", __name__, url_prefix="/clients")
@clients_bp.route("/", methods=["GET"])
@jwt_required()
def list_clients():
clients_db = Clients()
user_id = get_jwt_identity()
clients = clients_db.get_all_by_user(user_id)
return jsonify(clients), 200
@clients_bp.route("/", methods=["POST"])
@jwt_required()
def create_client():
clients_db = Clients()
user_id = get_jwt_identity()
data = request.get_json()
client_id = clients_db.create(
user_id=user_id,
name=data["name"],
address=data["address"],
register_number=data["register_number"],
contact_person=data["contact_person"],
phone=data["phone"],
email=data["email"],
vat = data["vat"]
)
return jsonify({"message": "Client created", "id": client_id}), 201
@clients_bp.route("/<int:client_id>", methods=["PUT"])
@jwt_required()
def update_client(client_id):
clients_db = Clients()
data = request.get_json()
name=data["name"]
address=data["address"]
register_number=data["register_number"]
contact_person=data["contact_person"]
phone=data["phone"]
email=data["email"]
vat = data["vat"]
clients_db.update(client_id, name, address, register_number, contact_person, phone, email, vat)
return jsonify({"message": "Client updated"}), 200
@clients_bp.route("/<int:client_id>", methods=["DELETE"])
@jwt_required()
def delete_client(client_id):
clients_db = Clients()
success = clients_db.delete(client_id)
if not success:
return jsonify({"message": "Client not found or unauthorized"}), 404
return jsonify({"message": "Client deleted"}), 200
@clients_bp.route("/<int:client_id>", methods=["GET"])
@jwt_required()
def get_client(client_id):
clients_db = Clients()
client = clients_db.get_by_id(client_id)
if not client:
return jsonify({"message": "Client not found"}), 404
return jsonify(client), 200

View File

@@ -0,0 +1,73 @@
from flask import Blueprint, request, jsonify
from models.destinations import Destinations
from flask_jwt_extended import jwt_required, get_jwt_identity
from utils.maps import AdressCoordinates
destinations_bp = Blueprint("destinations", __name__, url_prefix="/destinations")
@destinations_bp.route("/", methods=["GET"])
@jwt_required()
def list_destinations():
destinations_db = Destinations()
user_id = get_jwt_identity()
destinations = destinations_db.get_all_by_user(user_id)
return jsonify([dict(d) for d in destinations]), 200
@destinations_bp.route("/", methods=["POST"])
@jwt_required()
def create_destination():
destinations_db = Destinations()
user_id = get_jwt_identity()
data = request.get_json()
destination_id = destinations_db.create(user_id, data.get("name"), data.get("address"))
# coordinates = AdressCoordinates(data.get("address"))
# lat_log = coordinates.open_Maps_by_address()
# if lat_log:
# latitude = lat_log['latitude']
# longitude = lat_log['longitude']
# destinations_db.add_gps_coordinates(destination_id, latitude, longitude)
return jsonify({"id": destination_id, "message": "Destination created"}), 201
@destinations_bp.route("/<int:id>", methods=["PUT"])
@jwt_required()
def update_destination(id):
destinations_db = Destinations()
user_id = get_jwt_identity()
data = request.get_json()
destinations_db.update(id, user_id, data.get("name"), data.get("address"))
coordinates = AdressCoordinates(data.get("address"))
lat_log = coordinates.open_Maps_by_address()
if lat_log:
latitude = lat_log['latitude']
longitude = lat_log['longitude']
destinations_db.add_gps_coordinates(id, latitude, longitude)
return jsonify({"message": "Destination updated"}), 200
@destinations_bp.route("/<int:id>", methods=["DELETE"])
@jwt_required()
def delete_destination(id):
destinations_db = Destinations()
success = destinations_db.delete(id)
if not success:
return jsonify({"message": "Destination not found or unauthorized"}), 404
return "", 204
# New route to update GPS coordinates of a destination
@destinations_bp.route("/<int:id>/coordinates", methods=["PUT"])
@jwt_required()
def update_coordinates(id):
destinations_db = Destinations()
data = request.get_json()
latitude = data.get("latitude")
longitude = data.get("longitude")
if latitude is None or longitude is None:
return jsonify({"message": "Latitude and longitude are required"}), 400
success = destinations_db.add_gps_coordinates(id, latitude, longitude)
if not success:
return jsonify({"message": "Failed to update coordinates"}), 404
return jsonify({"message": "Coordinates updated"}), 200

View File

@@ -0,0 +1,259 @@
from flask import Blueprint, request, jsonify
from flask_jwt_extended import jwt_required, get_jwt_identity
from models.order_out import OrdersOut
from models.user import Users
from models.transporters import Transporters
from datetime import datetime
from utils.pdf import generate_order_pdf
from utils.cancel_order import cancel_order_pdf
import os
from flask import send_from_directory
from utils.email import send_gmail_with_attachment, send_custom_email_with_attachment
orders_bp = Blueprint("orders", __name__, url_prefix="/orders")
@orders_bp.route("/", methods=["POST"])
@jwt_required()
def create_order_route():
user_id = get_jwt_identity()
orders = OrdersOut()
incoming_data = request.json
#here we need to first implement the order pdf
users = Users()
user = users.get_user_by_id(user_id)
logo_filename = user.get('logo_filename')
logo_path = None
if logo_filename:
logo_path = os.path.abspath(
os.path.join(os.path.dirname(__file__), "..", "..", "client", "assets", "images", logo_filename)
)
transporters = Transporters()
transporter = transporters.get_transporter_by_id(incoming_data["transporter_id"])
generate_order_pdf(order=incoming_data, user_data=user, transporter_data=transporter, logo_path=logo_path)
#
#try:
order_data = {
'user_id': user_id,
'client_id': incoming_data["client_id"],
'transporter_id': incoming_data["transporter_id"],
'received_price': incoming_data["received_price"],
'paid_price': incoming_data["paid_price"],
'order_number': incoming_data["order_number"],
'created_at': datetime.now(),
'ldb_quantity': incoming_data["ldb_quantity"],
'kg_quantity': incoming_data["kg_quantity"],
'track_reg_number': incoming_data["track_reg_number"],
'trailer_reg_number': incoming_data["trailer_reg_number"],
'products_description': incoming_data["products_description"],
}
order_id = orders.create_order(order_data)
for address in incoming_data["loading_addresses"]:
data = {
"order_id": order_id,
"destination_id": address['loading_address_id'],
"informatins": address['loading_informatins'],
"point_data": address['loading_date'],
"point_hour": address['loading_hour'],
"point_type": "loading"
}
orders.create_order_point(data)
for address in incoming_data["unloading_addresses"]:
data = {
"order_id": order_id,
"destination_id": address['unloading_address_id'],
"informatins": address['unloading_informatins'],
"point_data": address['unloading_date'],
"point_hour": address['unloading_hour'],
"point_type": "unloading"
}
orders.create_order_point(data)
return jsonify({"message": "Order created", "order_id": order_id}), 201
#except Exception as e:
# return jsonify({"error": str(e)}), 400
@orders_bp.route("/<int:order_id>", methods=["PUT"])
@jwt_required()
def update_order_route(order_id):
orders = OrdersOut()
data = request.json
user_id = get_jwt_identity()
order = orders.get_order_by_id(order_id)
if not order:
return jsonify({"error": "Order not found"}), 404
if str(order["user_id"]) != str(user_id):
return jsonify({"error": "Unauthorized"}), 403
try:
orders.update_order({
"id":data.get("id", order['id']),
"client_id": data.get("client_id", order["client_id"]),
"transporter_id": data.get("transporter_id", order["transporter_id"]),
"received_price": data.get("received_price", order["received_price"]),
"paid_price": data.get("paid_price", order["paid_price"]),
"order_number": data.get("order_number", order["order_number"]),
"ldb_quantity": data.get("ldb_quantity", order["ldb_quantity"]),
"kg_quantity": data.get("kg_quantity", order["kg_quantity"]),
"track_reg_number": data.get("track_reg_number", order["track_reg_number"]),
"trailer_reg_number": data.get("trailer_reg_number", order["trailer_reg_number"]),
"products_description": data.get("products_description", order["products_description"]),
})
orders.delete_points_by_order_id(order_id)
for address in data["loading_addresses"]:
loading_data = {
"order_id": order_id,
"destination_id": address['loading_address_id'],
"informatins": address['loading_informatins'],
"point_data": address['loading_date'],
"point_hour": address['loading_hour'],
"point_type": "loading"
}
orders.create_order_point(loading_data)
for address in data["unloading_addresses"]:
unloading_data = {
"order_id": order_id,
"destination_id": address['unloading_address_id'],
"informatins": address['unloading_informatins'],
"point_data": address['unloading_date'],
"point_hour": address['unloading_hour'],
"point_type": "unloading"
}
orders.create_order_point(unloading_data)
#regenerate pdf:
incoming_data = data
users = Users()
user = users.get_user_by_id(user_id)
transporters = Transporters()
transporter = transporters.get_transporter_by_id(incoming_data["transporter_id"])
logo_filename = user.get('logo_filename')
logo_path = None
if logo_filename:
logo_path = os.path.abspath(
os.path.join(os.path.dirname(__file__), "..", "..", "client", "assets", "images", logo_filename)
)
generate_order_pdf(order=incoming_data, user_data=user, transporter_data=transporter, logo_path=logo_path)
return jsonify({"message": "Order updated", "order_id": order_id}), 200
except Exception as e:
return jsonify({"error": str(e)}), 400
@orders_bp.route("/<int:order_id>", methods=["DELETE"])
@jwt_required()
def delete_order_route(order_id):
orders = OrdersOut()
user_id = get_jwt_identity()
order = orders.get_order_by_id(order_id)
if not order:
return jsonify({"error": "Order not found"}), 404
if order["user_id"] != user_id:
return jsonify({"error": "Unauthorized"}), 403
try:
orders.delete_points_by_order_id(order_id)
orders.delete_order(order_id)
return jsonify({"message": "Order deleted"}), 200
except Exception as e:
return jsonify({"error": str(e)}), 400
@orders_bp.route("/list", methods=["GET"])
@jwt_required()
def list_orders():
orders = OrdersOut()
user_id = get_jwt_identity()
try:
user_orders = orders.get_orders_by_user(user_id)
#result = [{"id": order["id"], "order_number": order["order_number"]} for order in user_orders]
return jsonify(user_orders), 200
except Exception as e:
return jsonify({"error": str(e)}), 400
@orders_bp.route("/<int:order_id>", methods=["GET"])
@jwt_required()
def get_order(order_id):
orders = OrdersOut()
user_id = get_jwt_identity()
order = orders.get_order_by_id(order_id)
points = orders.get_order_points_by_order(order['id'])
loading_points = []
unloading_points = []
for point in points:
if point['point_type'] == 'loading':
loading_points.append(point)
else:
unloading_points.append(point)
order['loading_points'] = loading_points
order['unloading_points'] = unloading_points
if not order:
return jsonify({"error": "Order not found"}), 404
print(f'{order["user_id"]} {user_id}')
print(f'{type(order["user_id"])} {type(user_id)}')
if order["user_id"] != int(user_id):
return jsonify({"error": "Unauthorized"}), 403
return jsonify(order), 200
@orders_bp.route("/pdfs/<path:filename>", methods=["GET"])
#@jwt_required()
def serve_order_pdf(filename):
pdf_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "generated_pdfs"))
print(pdf_folder)
print(filename)
return send_from_directory(pdf_folder, filename, mimetype="application/pdf")
@orders_bp.route("/send-email/gmail", methods=["POST"])
@jwt_required()
def send_email_with_gmail():
data = request.json
try:
to_email = data["to_email"]
subject = data["subject"]
body = data["body"]
filename = data["filename"]
attachment_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "uploads", filename))
send_gmail_with_attachment(to_email, subject, body, attachment_path)
return jsonify({"message": "Email sent successfully using Gmail"}), 200
except Exception as e:
return jsonify({"error": str(e)}), 400
@orders_bp.route("/send-email/custom", methods=["POST"])
@jwt_required()
def send_email_with_custom_smtp():
data = request.json
try:
to_email = data["to_email"]
subject = data["subject"]
body = data["body"]
filename = data["filename"]
smtp_host = data["smtp_host"]
smtp_port = data["smtp_port"]
smtp_user = data["smtp_user"]
smtp_pass = data["smtp_pass"]
attachment_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "uploads", filename))
send_custom_email_with_attachment(to_email, subject, body, attachment_path, smtp_host, smtp_port, smtp_user, smtp_pass)
return jsonify({"message": "Email sent successfully using custom SMTP"}), 200
except Exception as e:
return jsonify({"error": str(e)}), 400
@orders_bp.route("/cancel/<int:order_id>", methods=["DELETE"])
@jwt_required()
def cancel_order(order_id):
try:
orders = OrdersOut()
order = orders.get_order_by_id(order_id)
user_id = get_jwt_identity()
pdf_name = f'order_{user_id}_{order['order_number']}.pdf'
cancel_order_pdf(pdf_name)
orders.cancel_order(order_id)
return jsonify({"message": "The order was successfully canceled!"}), 200
except Exception as e:
return jsonify({"error": str(e)}), 500

View File

@@ -0,0 +1,163 @@
from flask import Blueprint, request, jsonify
from flask_jwt_extended import jwt_required, get_jwt_identity
from models.order_in import OrdersIn
from models.transporters import Transporters
from models.user import Users
from datetime import datetime
orders_in_bp = Blueprint("orders_in", __name__, url_prefix="/orders_in")
@orders_in_bp.route("/", methods=["POST"])
@jwt_required()
def create_order_in_route():
user_id = get_jwt_identity()
orders = OrdersIn()
incoming_data = request.json
try:
order_data = {
'user_id': user_id,
'client_id': incoming_data["client_id"],
'received_price': incoming_data["received_price"],
'order_number': incoming_data["order_number"],
'created_at': datetime.now(),
'ldb_quantity': incoming_data["ldb_quantity"],
'kg_quantity': incoming_data["kg_quantity"],
'track_reg_number': incoming_data["track_reg_number"],
'trailer_reg_number': incoming_data["trailer_reg_number"],
'products_description': incoming_data["products_description"],
}
order_id = orders.create_order(order_data)
for address in incoming_data["loading_addresses"]:
data = {
"order_id": order_id,
"destination_id": address['loading_address_id'],
"informatins": address['loading_informatins'],
"point_data": address['loading_date'],
"point_hour": address['loading_hour'],
"point_type": "loading"
}
orders.create_order_point(data)
for address in incoming_data["unloading_addresses"]:
data = {
"order_id": order_id,
"destination_id": address['unloading_address_id'],
"informatins": address['unloading_informatins'],
"point_data": address['unloading_date'],
"point_hour": address['unloading_hour'],
"point_type": "unloading"
}
orders.create_order_point(data)
return jsonify({"message": "Order in created", "order_id": order_id}), 201
except Exception as e:
return jsonify({"error": str(e)}), 400
@orders_in_bp.route("/<int:order_id>", methods=["PUT"])
@jwt_required()
def update_order_route(order_id):
orders = OrdersIn()
data = request.json
user_id = get_jwt_identity()
order = orders.get_order_by_id(order_id)
if not order:
return jsonify({"error": "Order in not found"}), 404
if str(order["user_id"]) != str(user_id):
return jsonify({"error": "Unauthorized"}), 403
try:
orders.update_order({
"id":data.get("id", order['id']),
"client_id": data.get("client_id", order["client_id"]),
"received_price": data.get("received_price", order["received_price"]),
"order_number": data.get("order_number", order["order_number"]),
"ldb_quantity": data.get("ldb_quantity", order["ldb_quantity"]),
"kg_quantity": data.get("kg_quantity", order["kg_quantity"]),
"track_reg_number": data.get("track_reg_number", order["track_reg_number"]),
"trailer_reg_number": data.get("trailer_reg_number", order["trailer_reg_number"]),
"products_description": data.get("products_description", order["products_description"]),
"user_id":user_id
})
orders.delete_points_by_order_id(order_id)
for address in data["loading_addresses"]:
loading_data = {
"order_id": order_id,
"destination_id": address['loading_address_id'],
"informatins": address['loading_informatins'],
"point_data": address['loading_date'],
"point_hour": address['loading_hour'],
"point_type": "loading"
}
orders.create_order_point(loading_data)
for address in data["unloading_addresses"]:
unloading_data = {
"order_id": order_id,
"destination_id": address['unloading_address_id'],
"informatins": address['unloading_informatins'],
"point_data": address['unloading_date'],
"point_hour": address['unloading_hour'],
"point_type": "unloading"
}
orders.create_order_point(unloading_data)
return jsonify({"message": "Order updated"}), 200
except Exception as e:
return jsonify({"error": str(e)}), 400
@orders_in_bp.route("/<int:order_id>", methods=["DELETE"])
@jwt_required()
def delete_order_route(order_id):
orders = OrdersIn()
user_id = get_jwt_identity()
order = orders.get_order_by_id(order_id)
if not order:
return jsonify({"error": "Order in not found"}), 404
if str(order["user_id"]) != str(user_id):
return jsonify({"error": "Unauthorized"}), 403
try:
orders.delete_points_by_order_id(order_id)
orders.delete_order(order_id)
return jsonify({"message": "Order in deleted"}), 200
except Exception as e:
return jsonify({"error": str(e)}), 400
@orders_in_bp.route("/list", methods=["GET"])
@jwt_required()
def list_orders():
orders = OrdersIn()
user_id = get_jwt_identity()
try:
user_orders = orders.get_orders_by_user(user_id)
#result = [{"id": order["id"], "order_number": order["order_number"]} for order in user_orders]
return jsonify(user_orders), 200
except Exception as e:
return jsonify({"error": str(e)}), 400
@orders_in_bp.route("/<int:order_id>", methods=["GET"])
@jwt_required()
def get_order(order_id):
orders = OrdersIn()
user_id = get_jwt_identity()
order = orders.get_order_by_id(order_id)
points = orders.get_order_points_by_order(order['id'])
loading_points = []
unloading_points = []
for point in points:
if point['point_type'] == 'loading':
loading_points.append(point)
else:
unloading_points.append(point)
order['loading_points'] = loading_points
order['unloading_points'] = unloading_points
if not order:
return jsonify({"error": "Order not found"}), 404
print(f'{order["user_id"]} {user_id}')
print(f'{type(order["user_id"])} {type(user_id)}')
if order["user_id"] != int(user_id):
return jsonify({"error": "Unauthorized"}), 403
return jsonify(order), 200

View File

@@ -0,0 +1,144 @@
import os
from werkzeug.utils import secure_filename
from flask import current_app
from flask import Blueprint, request, jsonify
from flask_jwt_extended import jwt_required, get_jwt_identity
from models.user import Users
profile_bp = Blueprint("profile", __name__)
@profile_bp.route("/", methods=["GET"])
@jwt_required()
def get_profile():
user_id = get_jwt_identity()
users = Users()
user = users.get_user_by_id(user_id) # Plain SQL method returning dict or None
if not user:
return jsonify({"error": "User not found"}), 404
return jsonify({
"id": user["id"],
"name": user["name"],
"contact_name": user["contact_name"],
"email": user["email"],
"address": user["address"],
"register_number": user["register_number"],
"phone": user["phone"],
"logo_filename": user["logo_filename"],
"terms": user["terms"],
"first_order_number": user["first_order_number"],
"user_role": user["user_role"],
"vat":user["vat"]
})
@profile_bp.route("/", methods=["PUT"])
@jwt_required()
def update_profile():
users = Users()
user_id = get_jwt_identity()
user = users.get_user_by_id(user_id)
if not user:
return jsonify({"error": "User not found"}), 404
data = request.get_json()
update_data = {
"name": data.get("name", user["name"]),
"contact_name": data.get("contact_name", user["contact_name"]),
"email": data.get("email", user["email"]),
"address": data.get("address", user["address"]),
"register_number": data.get("register_number", user["register_number"]),
"phone": data.get("phone", user["phone"]),
"logo_filename": data.get("logo_filename", user["logo_filename"]),
"terms": data.get("terms", user["terms"]),
"first_order_number": data.get("first_order_number", user["first_order_number"]),
"user_id": user_id,
"vat":data.get("vat", user["vat"]),
}
users.update_user(update_data)
return jsonify({"message": "Profile updated successfully"})
@profile_bp.route("/logo", methods=["POST"])
@jwt_required()
def upload_logo():
users = Users()
if 'logo' not in request.files:
return jsonify({"error": "Logo file is required"}), 400
file = request.files['logo']
if file.filename == '':
return jsonify({"error": "No selected file"}), 400
filename = secure_filename(file.filename)
upload_dir = os.path.join(current_app.root_path, '..', 'instance', 'logos')
os.makedirs(upload_dir, exist_ok=True)
filepath = os.path.join(upload_dir, filename)
file.save(filepath)
user_id = get_jwt_identity()
user = users.get_user_by_id(user_id)
if not user:
return jsonify({"error": "User not found"}), 404
# Update the logo filename in DB
users.update_user_logo(user_id, {"logo_filename": filename})
return jsonify({"message": "Logo uploaded", "filename": filename}), 200
@profile_bp.route('/email')
@jwt_required()
def get_email_credentials():
user_id = get_jwt_identity()
users = Users()
credentials = users.get_email_credentials(user_id)
if not credentials:
return jsonify({"error": "Credentials not found"}), 404
return jsonify({
'id': credentials['id'],
'user_id': credentials['user_id'],
'smtp_host': credentials['smtp_host'],
'smtp_port': credentials['smtp_port'],
'smtp_user': credentials['smtp_user'],
'created_at': credentials['created_at']
}), 200
@profile_bp.route('/email', methods=["POST"])
@jwt_required()
def insert_email_credentials():
users = Users()
user_id = get_jwt_identity()
data = request.get_json()
if not data:
return jsonify({"error": "Credentials not found"}), 404
smtp_host = data['smtp_host']
smtp_port = data['smtp_port']
smtp_user = data['smtp_user']
users.insert_email_credentials(user_id, smtp_host, smtp_port, smtp_user)
return jsonify({"message": "Credentials inserted successfully"}), 200
@profile_bp.route('/email', methods=["PUT"])
@jwt_required()
def update_email_credentials():
users = Users()
user_id = get_jwt_identity()
data = request.get_json()
if not data:
return jsonify({"error": "Credentials not found"}), 404
smtp_host = data['smtp_host']
smtp_port = data['smtp_port']
smtp_user = data['smtp_user']
users.update_email_credentials(user_id, smtp_host, smtp_port, smtp_user)
return jsonify({"message": "Credentials updated successfully"}), 200

View File

@@ -0,0 +1,42 @@
from flask import Blueprint, request, jsonify
from flask_jwt_extended import jwt_required, get_jwt_identity
from models.order_out import OrdersOut # Your plain SQL model
from datetime import datetime
report_bp = Blueprint("report", __name__, url_prefix="/report")
@report_bp.route("/profit", methods=["GET"])
@jwt_required()
def get_profit_report():
try:
user_id = get_jwt_identity()
# Get filters from query params
date_from = request.args.get("date_from")
date_to = request.args.get("date_to")
client_id = request.args.get("client_id")
transporter_id = request.args.get("transporter_id")
# Use the plain SQL method that returns filtered orders list
filters = {
"user_id": user_id,
"date_from": date_from,
"date_to": date_to,
"client_id": client_id,
"transporter_id": transporter_id
}
orders = OrdersOut.get_filtered_orders(filters) # Implement this method in your model
total_received = sum(float(o.get("price_received", 0) or 0) for o in orders)
total_paid = sum(float(o.get("price_paid", 0) or 0) for o in orders)
profit = total_received - total_paid
return jsonify({
"total_received": total_received,
"total_paid": total_paid,
"profit": profit,
"orders_count": len(orders)
})
except Exception as e:
return jsonify({"error": str(e)}), 500

View File

@@ -0,0 +1,65 @@
from flask import Blueprint, jsonify, request
from flask_jwt_extended import jwt_required, get_jwt_identity
from datetime import datetime, timedelta
from models.subscription import Subscription
from models.user import Users
from datetime import datetime, timedelta
subscription_bp = Blueprint("subscription", __name__, url_prefix="/subscription")
@subscription_bp.route("/", methods=["GET"])
@jwt_required()
def get_subscription():
user_id = get_jwt_identity()
subscription_model = Subscription()
subscriptions = subscription_model.get_by_user_id(user_id)
return jsonify(subscriptions), 200
@subscription_bp.route("/first_2_months", methods=["POST"])
@jwt_required()
def first_2_months_subscription():
user_id = get_jwt_identity()
users_model = Users()
user = users_model.get_user_by_id(user_id)
subscription_model = Subscription()
existing_sub = subscription_model.get_first_2_months_subscription_for_register_number(user["register_number"])
if existing_sub:
return jsonify({"error": "First 2 months subscription already used for this company."}), 400
start_date = datetime.now()
end_date = start_date + timedelta(days=60)
subscription_model.create(user_id, "first_2_months", start_date.isoformat(), end_date.isoformat(), user["register_number"])
return jsonify({"message": "First 2 months subscription created."}), 201
@subscription_bp.route("/one_month", methods=["POST"])
@jwt_required()
def one_month_subscription():
user_id = get_jwt_identity()
start_date = datetime.now()
end_date = start_date + timedelta(days=30)
users_model = Users()
user = users_model.get_user_by_id(user_id)
subscription_model = Subscription()
subscription_model.create(user_id, "monthly", start_date.isoformat(), end_date.isoformat(), user["register_number"])
return jsonify({"message": "1 month subscription created."}), 201
@subscription_bp.route("/one_year", methods=["POST"])
@jwt_required()
def one_year_subscription():
user_id = get_jwt_identity()
start_date = datetime.now()
end_date = start_date + timedelta(days=365)
users_model = Users()
user = users_model.get_user_by_id(user_id)
subscription_model = Subscription()
subscription_model.create(user_id, "yearly", start_date.isoformat(), end_date.isoformat(), user["register_number"])
return jsonify({"message": "1 year subscription created."}), 201

View File

@@ -0,0 +1,74 @@
from flask import Blueprint, request, jsonify
from flask_jwt_extended import jwt_required, get_jwt_identity
from models.transporters import Transporters
transporters_bp = Blueprint("transporters", __name__, url_prefix="/transporters")
@transporters_bp.route("/", methods=["GET"])
@jwt_required()
def list_transporters():
user_id = get_jwt_identity()
transporters_db = Transporters()
transporters = transporters_db.get_all_transporters_by_user(user_id)
return jsonify(transporters), 200
@transporters_bp.route("/", methods=["POST"])
@jwt_required()
def create_transporter():
transporters_db = Transporters()
data = request.get_json()
user_id = get_jwt_identity()
transporter_id = transporters_db.create_transporter(
user_id=user_id,
name=data.get("name"),
address=data.get("address"),
register_number=data.get("register_number"),
contact_person=data.get("contact_person"),
phone=data.get("phone"),
email=data.get("email"),
vat = data.get("vat")
)
transporter = transporters_db.get_transporter_by_id(transporter_id)
return jsonify(transporter), 201
@transporters_bp.route("/<int:transporter_id>", methods=["PUT"])
@jwt_required()
def update_transporter(transporter_id):
transporters_db = Transporters()
user_id = get_jwt_identity()
data = request.get_json()
transporter = transporters_db.get_transporter_by_id(transporter_id)
if not transporter:
return jsonify({"error": "Transporter not found"}), 404
transporters_db.update_transporter(
transporter_id=transporter_id,
name=data.get("name"),
address=data.get("address"),
register_number=data.get("register_number"),
contact_person=data.get("contact_person"),
phone=data.get("phone"),
email=data.get("email"),
vat=data.get("vat")
)
updated_transporter = transporters_db.get_transporter_by_id(transporter_id)
return jsonify(updated_transporter), 200
@transporters_bp.route("/<int:transporter_id>", methods=["DELETE"])
@jwt_required()
def delete_transporter(transporter_id):
transporters_db = Transporters()
user_id = get_jwt_identity()
transporter = transporters_db.get_transporter_by_id(transporter_id)
if not transporter:
return jsonify({"error": "Transporter not found"}), 404
transporters_db.delete_transporter(transporter_id)
return jsonify({"message": "Transporter deleted"}), 200
@transporters_bp.route("/<int:transporter_id>", methods=["GET"])
@jwt_required()
def get_transporter(transporter_id):
transporters_db = Transporters()
transporter = transporters_db.get_transporter_by_id(transporter_id)
if not transporter:
return jsonify({"error": "Transporter not found"}), 404
return jsonify(transporter), 200

View File

@@ -0,0 +1,146 @@
-- Users table
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
contact_name TEXT,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
phone TEXT,
register_number TEXT,
vat TEXT,
address TEXT,
logo_filename TEXT,
terms TEXT,
first_order_number INTEGER DEFAULT 1,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
otp_code TEXT,
otp_expiration TIMESTAMPTZ,
user_role TEXT NOT NULL DEFAULT 'user' CHECK (user_role IN ('user', 'admin'))
);
-- Clients table
CREATE TABLE IF NOT EXISTS clients (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
name TEXT NOT NULL,
address TEXT,
register_number TEXT,
contact_person TEXT,
phone TEXT,
email TEXT,
vat TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Transporters table
CREATE TABLE IF NOT EXISTS transporters (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
name TEXT NOT NULL,
address TEXT,
register_number TEXT,
contact_person TEXT,
phone TEXT,
email TEXT,
vat TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Destinations table
CREATE TABLE IF NOT EXISTS destinations (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
name TEXT NOT NULL,
address TEXT,
latitude TEXT,
longitude TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Orders out table
CREATE TABLE IF NOT EXISTS orders_out (
id SERIAL PRIMARY KEY,
order_number TEXT NOT NULL,
user_id INTEGER NOT NULL,
client_id INTEGER NOT NULL,
transporter_id INTEGER NOT NULL,
products_description TEXT,
ldb_quantity DOUBLE PRECISION,
kg_quantity DOUBLE PRECISION,
track_reg_number TEXT,
trailer_reg_number TEXT,
received_price DOUBLE PRECISION,
paid_price DOUBLE PRECISION,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
order_status TEXT NOT NULL DEFAULT 'active' CHECK (order_status IN ('active', 'inactive', 'cancelled')),
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY(client_id) REFERENCES clients(id) ON DELETE CASCADE,
FOREIGN KEY(transporter_id) REFERENCES transporters(id) ON DELETE CASCADE
);
-- Orders in table
CREATE TABLE IF NOT EXISTS orders_in (
id SERIAL PRIMARY KEY,
order_number TEXT NOT NULL,
user_id INTEGER NOT NULL,
client_id INTEGER NOT NULL,
products_description TEXT,
ldb_quantity DOUBLE PRECISION,
kg_quantity DOUBLE PRECISION,
track_reg_number TEXT,
trailer_reg_number TEXT,
received_price DOUBLE PRECISION,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY(client_id) REFERENCES clients(id) ON DELETE CASCADE
);
-- Order In Points (loading/unloading) table
CREATE TABLE IF NOT EXISTS order_in_points (
id SERIAL PRIMARY KEY,
order_id INTEGER NOT NULL,
destination_id INTEGER NOT NULL,
informatins TEXT,
point_data TEXT,
point_hour TEXT,
point_type TEXT NOT NULL CHECK (point_type IN ('loading', 'unloading')),
FOREIGN KEY(order_id) REFERENCES orders_in(id) ON DELETE CASCADE
);
-- Order In Points (loading/unloading) table
CREATE TABLE IF NOT EXISTS order_out_points (
id SERIAL PRIMARY KEY,
order_id INTEGER NOT NULL,
destination_id INTEGER NOT NULL,
informatins TEXT,
point_data TEXT,
point_hour TEXT,
point_type TEXT NOT NULL CHECK (point_type IN ('loading', 'unloading')),
FOREIGN KEY(order_id) REFERENCES orders_out(id) ON DELETE CASCADE
);
-- Subscriptions table
CREATE TABLE IF NOT EXISTS subscriptions (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
plan TEXT NOT NULL CHECK (plan IN ('first_2_months', 'monthly', 'yearly')),
start_date TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
end_date TIMESTAMPTZ NOT NULL,
status TEXT NOT NULL CHECK (status IN ('active', 'expired', 'cancelled')),
register_number TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS email (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
smtp_host TEXT,
smtp_port TEXT,
smtp_user TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);

View File

@@ -0,0 +1,146 @@
-- Users table
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
contact_name TEXT,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
phone TEXT,
register_number TEXT,
vat TEXT,
address TEXT,
logo_filename TEXT,
terms TEXT,
first_order_number INTEGER DEFAULT 1,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
otp_code TEXT,
otp_expiration TIMESTAMPTZ,
user_role TEXT NOT NULL DEFAULT 'user' CHECK (user_role IN ('user', 'admin'))
);
-- Clients table
CREATE TABLE IF NOT EXISTS clients (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
name TEXT NOT NULL,
address TEXT,
register_number TEXT,
contact_person TEXT,
phone TEXT,
email TEXT,
vat TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Transporters table
CREATE TABLE IF NOT EXISTS transporters (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
name TEXT NOT NULL,
address TEXT,
register_number TEXT,
contact_person TEXT,
phone TEXT,
email TEXT,
vat TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Destinations table
CREATE TABLE IF NOT EXISTS destinations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
name TEXT NOT NULL,
address TEXT,
latitude TEXT,
longitude TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Orders out table
CREATE TABLE IF NOT EXISTS orders_out (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_number TEXT NOT NULL,
user_id INTEGER NOT NULL,
client_id INTEGER NOT NULL,
transporter_id INTEGER NOT NULL,
products_description TEXT,
ldb_quantity DOUBLE PRECISION,
kg_quantity DOUBLE PRECISION,
track_reg_number TEXT,
trailer_reg_number TEXT,
received_price DOUBLE PRECISION,
paid_price DOUBLE PRECISION,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
order_status TEXT NOT NULL DEFAULT 'active' CHECK (order_status IN ('active', 'inactive', 'cancelled')),
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY(client_id) REFERENCES clients(id) ON DELETE CASCADE,
FOREIGN KEY(transporter_id) REFERENCES transporters(id) ON DELETE CASCADE
);
-- Orders in table
CREATE TABLE IF NOT EXISTS orders_in (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_number TEXT NOT NULL,
user_id INTEGER NOT NULL,
client_id INTEGER NOT NULL,
products_description TEXT,
ldb_quantity DOUBLE PRECISION,
kg_quantity DOUBLE PRECISION,
track_reg_number TEXT,
trailer_reg_number TEXT,
received_price DOUBLE PRECISION,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY(client_id) REFERENCES clients(id) ON DELETE CASCADE
);
-- Order In Points (loading/unloading) table
CREATE TABLE IF NOT EXISTS order_in_points (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER NOT NULL,
destination_id INTEGER NOT NULL,
informatins TEXT,
point_data TEXT,
point_hour TEXT,
point_type TEXT NOT NULL CHECK (point_type IN ('loading', 'unloading')),
FOREIGN KEY(order_id) REFERENCES orders_in(id) ON DELETE CASCADE
);
-- Order In Points (loading/unloading) table
CREATE TABLE IF NOT EXISTS order_out_points (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER NOT NULL,
destination_id INTEGER NOT NULL,
informatins TEXT,
point_data TEXT,
point_hour TEXT,
point_type TEXT NOT NULL CHECK (point_type IN ('loading', 'unloading')),
FOREIGN KEY(order_id) REFERENCES orders_out(id) ON DELETE CASCADE
);
-- Subscriptions table
CREATE TABLE IF NOT EXISTS subscriptions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
plan TEXT NOT NULL CHECK (plan IN ('first_2_months', 'monthly', 'yearly')),
start_date TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
end_date TIMESTAMPTZ NOT NULL,
status TEXT NOT NULL CHECK (status IN ('active', 'expired', 'cancelled')),
register_number TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS email (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
smtp_host TEXT,
smtp_port TEXT,
smtp_user TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);

View File

@@ -0,0 +1,62 @@
import os
from PyPDF2 import PdfReader, PdfWriter
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
GENERATED_FOLDER = os.path.abspath(
os.path.join(os.path.dirname(__file__), "..", "generated_pdfs")
)
def create_watermark_pdf(watermark_path, text="CANCELLED"):
c = canvas.Canvas(watermark_path, pagesize=A4)
c.setFont("Helvetica-Bold", 80)
c.setFillGray(0.6) # Light gray text
c.saveState()
c.translate(300, 400)
c.rotate(45)
c.drawCentredString(0, 0, text)
c.restoreState()
c.save()
def apply_watermark(input_pdf_path, output_pdf_path, watermark_pdf_path):
reader = PdfReader(input_pdf_path)
watermark = PdfReader(watermark_pdf_path).pages[0]
writer = PdfWriter()
for page in reader.pages:
page.merge_page(watermark)
writer.add_page(page)
with open(output_pdf_path, "wb") as f:
writer.write(f)
def cancel_order_pdf(order_filename):
# File paths
input_pdf_path = os.path.join(GENERATED_FOLDER, order_filename)
output_pdf_path = input_pdf_path
watermark_pdf_path = os.path.join(GENERATED_FOLDER, "temp_watermark.pdf")
print(watermark_pdf_path)
# Check if file exists
if not os.path.isfile(input_pdf_path):
raise FileNotFoundError(f"Original order PDF not found: {input_pdf_path}")
# Create watermark and apply it
create_watermark_pdf(watermark_pdf_path, text="CANCELLED")
apply_watermark(input_pdf_path, output_pdf_path, watermark_pdf_path)
# Optionally remove temp watermark
os.remove(watermark_pdf_path)
return output_pdf_path
# Example usage:
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("Usage: python cancel_order.py <order_filename>")
else:
try:
result_path = cancel_order_pdf(sys.argv[1])
print(f"Cancelled PDF created: {result_path}")
except Exception as e:
print(f"Error: {e}")

View File

@@ -0,0 +1,139 @@
import smtplib
from email.message import EmailMessage
import os
def send_email(to_email, subject, body):
smtp_host = os.environ.get("SMTP_HOST")
smtp_port = int(os.environ.get("SMTP_PORT", 587))
smtp_user = os.environ.get("SMTP_USER")
smtp_pass = os.environ.get("SMTP_PASS")
sender_email = os.environ.get("SMTP_FROM", smtp_user)
if not all([smtp_host, smtp_port, smtp_user, smtp_pass]):
raise ValueError("SMTP config incomplete in environment variables.")
msg = EmailMessage()
msg["Subject"] = subject
msg["From"] = sender_email
msg["To"] = to_email
msg.set_content(body)
with smtplib.SMTP(smtp_host, smtp_port) as server:
server.starttls()
server.login(smtp_user, smtp_pass)
server.send_message(msg)
# Send email with attachment
def send_email_with_attachment(to_email, subject, body, attachment_path):
smtp_host = os.environ.get("SMTP_HOST")
smtp_port = int(os.environ.get("SMTP_PORT", 587))
smtp_user = os.environ.get("SMTP_USER")
smtp_pass = os.environ.get("SMTP_PASS")
sender_email = os.environ.get("SMTP_FROM", smtp_user)
if not all([smtp_host, smtp_port, smtp_user, smtp_pass]):
raise ValueError("SMTP config incomplete in environment variables.")
msg = EmailMessage()
msg["Subject"] = subject
msg["From"] = sender_email
msg["To"] = to_email
msg.set_content(body)
if attachment_path and os.path.isfile(attachment_path):
with open(attachment_path, "rb") as f:
file_data = f.read()
file_name = os.path.basename(attachment_path)
msg.add_attachment(file_data, maintype="application", subtype="pdf", filename=file_name)
else:
raise FileNotFoundError(f"Attachment file not found: {attachment_path}")
with smtplib.SMTP(smtp_host, smtp_port) as server:
server.starttls()
server.login(smtp_user, smtp_pass)
server.send_message(msg)
# Send email using Gmail directly
def send_gmail(to_email, subject, body):
smtp_host = "smtp.gmail.com"
smtp_port = 587
smtp_user = 'macamete.robert@gmail.com'
smtp_pass = 'advx yqlv jkaa czvr'
sender_email = 'macamete.robert@gmail.com'
if not all([smtp_user, smtp_pass]):
raise ValueError("GMAIL_USER and GMAIL_PASS must be set in environment variables.")
msg = EmailMessage()
msg["Subject"] = subject
msg["From"] = sender_email
msg["To"] = to_email
msg.set_content(body)
with smtplib.SMTP(smtp_host, smtp_port) as server:
server.starttls()
server.login(smtp_user, smtp_pass)
server.send_message(msg)
# Send email with attachment using Gmail directly
def send_gmail_with_attachment(to_email, subject, body, attachment_path):
smtp_host = "smtp.gmail.com"
smtp_port = 587
smtp_user = os.environ.get("GMAIL_USER")
smtp_pass = os.environ.get("GMAIL_PASS")
sender_email = smtp_user
if not all([smtp_user, smtp_pass]):
raise ValueError("GMAIL_USER and GMAIL_PASS must be set in environment variables.")
msg = EmailMessage()
msg["Subject"] = subject
msg["From"] = sender_email
msg["To"] = to_email
msg.set_content(body)
if attachment_path and os.path.isfile(attachment_path):
with open(attachment_path, "rb") as f:
file_data = f.read()
file_name = os.path.basename(attachment_path)
msg.add_attachment(file_data, maintype="application", subtype="pdf", filename=file_name)
else:
raise FileNotFoundError(f"Attachment file not found: {attachment_path}")
with smtplib.SMTP(smtp_host, smtp_port) as server:
server.starttls()
server.login(smtp_user, smtp_pass)
server.send_message(msg)
# Send email with attachment
def send_custom_email_with_attachment(to_email, subject, body, attachment_path, SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS):
smtp_host = SMTP_HOST
smtp_port = int(SMTP_PORT)
smtp_user = SMTP_USER
smtp_pass = SMTP_PASS
sender_email = smtp_user
if not all([smtp_host, smtp_port, smtp_user, smtp_pass]):
raise ValueError("SMTP config incomplete in environment variables.")
msg = EmailMessage()
msg["Subject"] = subject
msg["From"] = sender_email
msg["To"] = to_email
msg.set_content(body)
if attachment_path and os.path.isfile(attachment_path):
with open(attachment_path, "rb") as f:
file_data = f.read()
file_name = os.path.basename(attachment_path)
msg.add_attachment(file_data, maintype="application", subtype="pdf", filename=file_name)
else:
raise FileNotFoundError(f"Attachment file not found: {attachment_path}")
with smtplib.SMTP(smtp_host, smtp_port) as server:
server.starttls()
server.login(smtp_user, smtp_pass)
server.send_message(msg)

View File

@@ -0,0 +1,28 @@
from geopy.geocoders import Nominatim
class AdressCoordinates:
def __init__(self, address):
self.addess = address
def open_Maps_by_address(self):
address = self.addess
if not address:
return
#try:
geolocator = Nominatim(user_agent="flet_Maps_app")
location = geolocator.geocode(address)
print(location)
if location:
latitude = location.latitude
longitude = location.longitude
Maps_url = f"https://www.google.com/maps/search/?api=1&query={latitude},{longitude}"
return {
'latitude' : latitude,
'longitude' : longitude,
'Maps_url': Maps_url
}
#except Exception as ex:
# print(ex)

View File

@@ -0,0 +1,199 @@
from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas
from reportlab.platypus import Table, TableStyle, Paragraph, SimpleDocTemplate, Spacer, Image
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet
import io
import os
import logging
# --- helpers ---------------------------------------------------------------
def _resolve_logo_path(logo_path: str | None) -> str | None:
"""Return a readable logo path or None. Tries multiple fallbacks.
This avoids crashes when a caller passes a path that exists only in the
client container (e.g., '/client/assets/...') while we're running in the
server container on Fly.
"""
here = os.path.dirname(__file__)
project_root = os.path.abspath(os.path.join(here, "..", ".."))
candidates = []
# 1) if caller provided a path, try it as-is
if logo_path:
candidates.append(logo_path)
# also try without a leading slash inside our image tree
stripped = logo_path.lstrip("/\\")
candidates.append(os.path.join(project_root, stripped))
# 2) Known locations in this repo layout
candidates.append(os.path.join(project_root, "client", "assets", "images", "truck_logo_black.png"))
candidates.append(os.path.join(project_root, "assets", "images", "truck_logo_black.png"))
# 3) Allow override via env
env_path = os.getenv("DEFAULT_LOGO_PATH")
if env_path:
candidates.insert(0, env_path)
for p in candidates:
try:
if p and os.path.isfile(p):
return p
except Exception:
# some paths may be malformed on certain platforms
continue
return None
def generate_order_pdf(order, user_data, transporter_data, logo_path, save_to_disk=True):
#print(order)
#print(f'user data: {user_data}')
if 'address' in user_data:
address = user_data.get("address")
if address:
address = address.replace(" %",", ")
else:
address = ''
else:
address=''
buffer = io.BytesIO()
doc = SimpleDocTemplate(buffer, pagesize=A4)
elements = []
styles = getSampleStyleSheet()
# Resolve logo path robustly across local/dev and Fly
logo_path = _resolve_logo_path(logo_path)
# Prepare texts
user_text = f"""<b>{user_data.get("name", "")}</b><br/>
{user_data.get("register_number", "")}<br/>
{user_data.get("vat", "")}<br/>
{address}<br/>
{""}<br/>
{user_data.get("contact_name", "")}<br/>
{user_data.get("phone", "")}<br/>
{user_data.get("email", "")}
"""
transporter_text = f"""<b>{transporter_data.get("name", "")}</b><br/>
{transporter_data.get("contact_person", "")}<br/>
{transporter_data.get("phone", "")}<br/>
{transporter_data.get("email", "")}
"""
# Logo (centered), tolerate missing file
logo = None
if logo_path:
try:
logo = Image(logo_path, width=120, mask='auto', height=60)
logo.hAlign = 'CENTER'
except Exception as e:
logging.warning("PDF: failed to load logo at %s: %s", logo_path, e)
logo = None
# Top section: transporter - logo - user
top_section = Table([
[
Paragraph(transporter_text, styles['Normal']),
logo,
Paragraph(user_text, styles['Normal'])
]
], colWidths=[200, 150, 200])
top_section.setStyle(TableStyle([
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("ALIGN", (0, 0), (0, 0), "LEFT"),
("ALIGN", (1, 0), (1, 0), "CENTER"),
("ALIGN", (2, 0), (2, 0), "RIGHT"),
("LEFTPADDING", (0, 0), (0, 0), 20),
]))
elements.append(top_section)
elements.append(Spacer(1, 12))
# Order number and current date (centered, vertically stacked)
from datetime import datetime
header_info = Table([
[Paragraph(f"<b>Loading Order</b>: {order['order_number']}", styles["Normal"])],
[Paragraph(f"Date: {datetime.now().strftime('%d/%m/%Y')}", styles["Normal"])]
], colWidths=[450])
header_info.setStyle(TableStyle([
("ALIGN", (0, 0), (-1, -1), "CENTER")
]))
elements.append(header_info)
elements.append(Spacer(1, 12))
# Order Summary Table
elements.append(Paragraph("Summary", styles['Heading3']))
summary_data = [
["Details", Paragraph("Values", styles["Normal"])],
["Truck Reg. No.", Paragraph(str(order["track_reg_number"]), styles["Normal"])],
["Trailer Reg. No.", Paragraph(str(order["trailer_reg_number"]), styles["Normal"])],
["Product Description", Paragraph(str(order["products_description"]), styles["Normal"])],
["LDM", Paragraph(str(order["ldb_quantity"]), styles["Normal"])],
["KG", Paragraph(str(order["kg_quantity"]), styles["Normal"])],
["Price", Paragraph(str(order["paid_price"]), styles["Normal"])],
]
summary_table = Table(summary_data, colWidths=[150, 350])
summary_table.setStyle(TableStyle([
("GRID", (0, 0), (-1, -1), 0.5, colors.black),
("BACKGROUND", (0, 0), (-1, 0), colors.lightgrey),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("WORDWRAP", (0, 0), (-1, -1), "CJK"),
("ALIGN", (0, 0), (-1, -1), "LEFT"),
]))
elements.append(summary_table)
elements.append(Spacer(1, 24))
# Loading Points
elements.append(Paragraph("Loading Sequence", styles['Heading3']))
loading_data = [[Paragraph("Address", styles["Normal"]), Paragraph("Date & Hour", styles["Normal"]), Paragraph("Instructions", styles["Normal"])]]
for l in order["loading_addresses"]:
loading_data.append([
Paragraph(f"{l["loading_address_name"]}: {l["loading_address"]}", styles["Normal"]),
Paragraph(f"{l['loading_date']} {l['loading_hour']}", styles["Normal"]),
Paragraph(str(l["loading_informatins"]), styles["Normal"])
])
loading_table = Table(loading_data, colWidths=[200, 100, 200])
loading_table.setStyle(TableStyle([
("GRID", (0, 0), (-1, -1), 0.5, colors.black),
("BACKGROUND", (0, 0), (-1, 0), colors.lightgrey),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("WORDWRAP", (0, 0), (-1, -1), "CJK"),
("ALIGN", (0, 0), (-1, -1), "LEFT"),
]))
elements.append(loading_table)
elements.append(Spacer(1, 24))
# Unloading Points
elements.append(Paragraph("Unloading Sequence", styles['Heading3']))
unloading_data = [[Paragraph("Address", styles["Normal"]), Paragraph("Date & Hour", styles["Normal"]), Paragraph("Instructions", styles["Normal"])]]
for u in order["unloading_addresses"]:
unloading_data.append([
Paragraph(f"{u["unloading_address_name"]}: {u["unloading_address"]}", styles["Normal"]),
Paragraph(f"{u['unloading_date']} {u['unloading_hour']}", styles["Normal"]),
Paragraph(str(u["unloading_informatins"]), styles["Normal"])
])
unloading_table = Table(unloading_data, colWidths=[200, 100, 200])
unloading_table.setStyle(TableStyle([
("GRID", (0, 0), (-1, -1), 0.5, colors.black),
("BACKGROUND", (0, 0), (-1, 0), colors.lightgrey),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("WORDWRAP", (0, 0), (-1, -1), "CJK"),
("ALIGN", (0, 0), (-1, -1), "LEFT"),
]))
elements.append(unloading_table)
elements.append(Spacer(1, 24))
elements.append(Paragraph("Terms and Conditions", styles["Heading3"]),)
elements.append(
Paragraph(str(order["terms"]), styles["Normal"])
)
doc.build(elements)
buffer.seek(0)
if save_to_disk:
save_path=f"generated_pdfs/order_{order['user_id']}_{order['order_number']}.pdf"
with open(save_path, "wb") as f:
f.write(buffer.getvalue())
return buffer