feat(pos): fleet management module — vehicles, maintenance schedules, alerts
Add full fleet management (Feature 13): database migration for fleet_vehicles, fleet_maintenance_schedules, and fleet_maintenance_logs tables; REST API blueprint with CRUD, scheduling, logging, alerts, and stats endpoints; frontend with tabbed UI (vehicles grid, maintenance schedules, history, overdue alerts); sidebar nav entry. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
540
pos/blueprints/fleet_bp.py
Normal file
540
pos/blueprints/fleet_bp.py
Normal file
@@ -0,0 +1,540 @@
|
||||
# /home/Autopartes/pos/blueprints/fleet_bp.py
|
||||
"""Fleet management blueprint: vehicles, maintenance schedules, logs, alerts."""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from flask import Blueprint, request, jsonify, g
|
||||
from middleware import require_auth
|
||||
from tenant_db import get_tenant_conn
|
||||
from services.audit import log_action
|
||||
|
||||
fleet_bp = Blueprint('fleet', __name__, url_prefix='/pos/api/fleet')
|
||||
|
||||
|
||||
# ─── Vehicles CRUD ─────────────────────────────
|
||||
|
||||
@fleet_bp.route('/vehicles', methods=['GET'])
|
||||
@require_auth()
|
||||
def list_vehicles():
|
||||
"""List fleet vehicles with pagination and search.
|
||||
|
||||
Query params:
|
||||
q: search string (matches plate, make, model, owner_name)
|
||||
page: page number (default 1)
|
||||
per_page: items per page (default 50, max 200)
|
||||
active_only: filter active only (default true)
|
||||
"""
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
cur = conn.cursor()
|
||||
|
||||
page = int(request.args.get('page', 1))
|
||||
per_page = min(int(request.args.get('per_page', 50)), 200)
|
||||
search = request.args.get('q', '').strip()
|
||||
active_only = request.args.get('active_only', 'true').lower() != 'false'
|
||||
|
||||
where_clauses = []
|
||||
params = []
|
||||
|
||||
if active_only:
|
||||
where_clauses.append("v.is_active = true")
|
||||
if g.branch_id:
|
||||
where_clauses.append("v.branch_id = %s")
|
||||
params.append(g.branch_id)
|
||||
if search:
|
||||
where_clauses.append(
|
||||
"(v.plate ILIKE %s OR v.make ILIKE %s OR v.model ILIKE %s OR v.owner_name ILIKE %s OR v.vin ILIKE %s)"
|
||||
)
|
||||
params.extend([f'%{search}%'] * 5)
|
||||
|
||||
where = " AND ".join(where_clauses) if where_clauses else "true"
|
||||
|
||||
# Count
|
||||
cur.execute(f"SELECT count(*) FROM fleet_vehicles v WHERE {where}", params)
|
||||
total = cur.fetchone()[0]
|
||||
|
||||
# Fetch
|
||||
cur.execute(f"""
|
||||
SELECT v.id, v.branch_id, v.plate, v.vin, v.make, v.model, v.year,
|
||||
v.current_mileage, v.fuel_type, v.color, v.owner_name,
|
||||
v.notes, v.is_active, v.created_at
|
||||
FROM fleet_vehicles v
|
||||
WHERE {where}
|
||||
ORDER BY v.created_at DESC
|
||||
LIMIT %s OFFSET %s
|
||||
""", params + [per_page, (page - 1) * per_page])
|
||||
|
||||
vehicles = []
|
||||
for r in cur.fetchall():
|
||||
vehicles.append({
|
||||
'id': r[0], 'branch_id': r[1], 'plate': r[2], 'vin': r[3],
|
||||
'make': r[4], 'model': r[5], 'year': r[6],
|
||||
'current_mileage': r[7], 'fuel_type': r[8], 'color': r[9],
|
||||
'owner_name': r[10], 'notes': r[11], 'is_active': r[12],
|
||||
'created_at': str(r[13]) if r[13] else None,
|
||||
})
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
total_pages = (total + per_page - 1) // per_page
|
||||
return jsonify({
|
||||
'data': vehicles,
|
||||
'pagination': {'page': page, 'per_page': per_page, 'total': total, 'total_pages': total_pages}
|
||||
})
|
||||
|
||||
|
||||
@fleet_bp.route('/vehicles/<int:vehicle_id>', methods=['GET'])
|
||||
@require_auth()
|
||||
def get_vehicle(vehicle_id):
|
||||
"""Vehicle detail with maintenance schedules and recent logs."""
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute("""
|
||||
SELECT id, branch_id, plate, vin, make, model, year,
|
||||
current_mileage, fuel_type, color, owner_name,
|
||||
notes, is_active, created_at
|
||||
FROM fleet_vehicles WHERE id = %s
|
||||
""", (vehicle_id,))
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'error': 'Vehicle not found'}), 404
|
||||
|
||||
vehicle = {
|
||||
'id': row[0], 'branch_id': row[1], 'plate': row[2], 'vin': row[3],
|
||||
'make': row[4], 'model': row[5], 'year': row[6],
|
||||
'current_mileage': row[7], 'fuel_type': row[8], 'color': row[9],
|
||||
'owner_name': row[10], 'notes': row[11], 'is_active': row[12],
|
||||
'created_at': str(row[13]) if row[13] else None,
|
||||
}
|
||||
|
||||
# Maintenance schedules
|
||||
cur.execute("""
|
||||
SELECT id, maintenance_type, interval_km, interval_months,
|
||||
last_done_at, last_done_km, next_due_at, next_due_km,
|
||||
notes, is_active
|
||||
FROM fleet_maintenance_schedules
|
||||
WHERE vehicle_id = %s AND is_active = true
|
||||
ORDER BY next_due_at NULLS LAST, next_due_km NULLS LAST
|
||||
""", (vehicle_id,))
|
||||
vehicle['schedules'] = []
|
||||
for r in cur.fetchall():
|
||||
vehicle['schedules'].append({
|
||||
'id': r[0], 'maintenance_type': r[1], 'interval_km': r[2],
|
||||
'interval_months': r[3],
|
||||
'last_done_at': str(r[4]) if r[4] else None,
|
||||
'last_done_km': r[5],
|
||||
'next_due_at': str(r[6]) if r[6] else None,
|
||||
'next_due_km': r[7],
|
||||
'notes': r[8], 'is_active': r[9],
|
||||
})
|
||||
|
||||
# Recent logs (last 20)
|
||||
cur.execute("""
|
||||
SELECT l.id, l.schedule_id, l.maintenance_type, l.mileage_at,
|
||||
l.cost, l.parts_used, l.employee_id, l.notes, l.created_at,
|
||||
e.name as employee_name
|
||||
FROM fleet_maintenance_logs l
|
||||
LEFT JOIN employees e ON l.employee_id = e.id
|
||||
WHERE l.vehicle_id = %s
|
||||
ORDER BY l.created_at DESC
|
||||
LIMIT 20
|
||||
""", (vehicle_id,))
|
||||
vehicle['recent_logs'] = []
|
||||
for r in cur.fetchall():
|
||||
vehicle['recent_logs'].append({
|
||||
'id': r[0], 'schedule_id': r[1], 'maintenance_type': r[2],
|
||||
'mileage_at': r[3], 'cost': float(r[4]) if r[4] else 0,
|
||||
'parts_used': r[5], 'employee_id': r[6], 'notes': r[7],
|
||||
'created_at': str(r[8]) if r[8] else None,
|
||||
'employee_name': r[9],
|
||||
})
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
return jsonify(vehicle)
|
||||
|
||||
|
||||
@fleet_bp.route('/vehicles', methods=['POST'])
|
||||
@require_auth()
|
||||
def create_vehicle():
|
||||
"""Create a fleet vehicle.
|
||||
|
||||
Body: {plate, vin, make, model, year, current_mileage, fuel_type, color, owner_name, notes}
|
||||
"""
|
||||
data = request.get_json() or {}
|
||||
if not data.get('plate') and not data.get('vin'):
|
||||
return jsonify({'error': 'plate or vin is required'}), 400
|
||||
|
||||
branch_id = data.get('branch_id', g.branch_id)
|
||||
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
cur.execute("""
|
||||
INSERT INTO fleet_vehicles
|
||||
(branch_id, plate, vin, make, model, year,
|
||||
current_mileage, fuel_type, color, owner_name, notes)
|
||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
||||
RETURNING id
|
||||
""", (
|
||||
branch_id, data.get('plate'), data.get('vin'),
|
||||
data.get('make'), data.get('model'), data.get('year'),
|
||||
data.get('current_mileage', 0), data.get('fuel_type', 'gasolina'),
|
||||
data.get('color'), data.get('owner_name'), data.get('notes'),
|
||||
))
|
||||
vehicle_id = cur.fetchone()[0]
|
||||
|
||||
log_action(conn, 'FLEET_VEHICLE_CREATE', 'fleet_vehicle', vehicle_id,
|
||||
new_value={'plate': data.get('plate'), 'make': data.get('make'),
|
||||
'model': data.get('model')})
|
||||
|
||||
conn.commit()
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'id': vehicle_id, 'message': 'Vehicle created'}), 201
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@fleet_bp.route('/vehicles/<int:vehicle_id>', methods=['PUT'])
|
||||
@require_auth()
|
||||
def update_vehicle(vehicle_id):
|
||||
"""Update vehicle fields including mileage.
|
||||
|
||||
Body: any subset of {plate, vin, make, model, year, current_mileage, fuel_type,
|
||||
color, owner_name, notes, is_active}
|
||||
"""
|
||||
data = request.get_json() or {}
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute("SELECT id FROM fleet_vehicles WHERE id = %s", (vehicle_id,))
|
||||
if not cur.fetchone():
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'error': 'Vehicle not found'}), 404
|
||||
|
||||
allowed = ['plate', 'vin', 'make', 'model', 'year', 'current_mileage',
|
||||
'fuel_type', 'color', 'owner_name', 'notes', 'is_active', 'branch_id']
|
||||
sets = []
|
||||
vals = []
|
||||
for field in allowed:
|
||||
if field in data:
|
||||
sets.append(f"{field} = %s")
|
||||
vals.append(data[field])
|
||||
|
||||
if not sets:
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'error': 'No fields to update'}), 400
|
||||
|
||||
vals.append(vehicle_id)
|
||||
try:
|
||||
cur.execute(f"UPDATE fleet_vehicles SET {', '.join(sets)} WHERE id = %s", vals)
|
||||
log_action(conn, 'FLEET_VEHICLE_UPDATE', 'fleet_vehicle', vehicle_id,
|
||||
new_value=data)
|
||||
conn.commit()
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'message': 'Vehicle updated'})
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@fleet_bp.route('/vehicles/<int:vehicle_id>', methods=['DELETE'])
|
||||
@require_auth()
|
||||
def deactivate_vehicle(vehicle_id):
|
||||
"""Soft-delete: set is_active = false."""
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute("SELECT id FROM fleet_vehicles WHERE id = %s", (vehicle_id,))
|
||||
if not cur.fetchone():
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'error': 'Vehicle not found'}), 404
|
||||
|
||||
cur.execute("UPDATE fleet_vehicles SET is_active = false WHERE id = %s", (vehicle_id,))
|
||||
log_action(conn, 'FLEET_VEHICLE_DEACTIVATE', 'fleet_vehicle', vehicle_id)
|
||||
conn.commit()
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'message': 'Vehicle deactivated'})
|
||||
|
||||
|
||||
# ─── Maintenance Schedules ─────────────────────────────
|
||||
|
||||
@fleet_bp.route('/vehicles/<int:vehicle_id>/schedules', methods=['GET'])
|
||||
@require_auth()
|
||||
def list_schedules(vehicle_id):
|
||||
"""Maintenance schedules for a vehicle."""
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute("""
|
||||
SELECT id, maintenance_type, interval_km, interval_months,
|
||||
last_done_at, last_done_km, next_due_at, next_due_km,
|
||||
notes, is_active
|
||||
FROM fleet_maintenance_schedules
|
||||
WHERE vehicle_id = %s AND is_active = true
|
||||
ORDER BY next_due_at NULLS LAST, next_due_km NULLS LAST
|
||||
""", (vehicle_id,))
|
||||
|
||||
schedules = []
|
||||
for r in cur.fetchall():
|
||||
schedules.append({
|
||||
'id': r[0], 'maintenance_type': r[1], 'interval_km': r[2],
|
||||
'interval_months': r[3],
|
||||
'last_done_at': str(r[4]) if r[4] else None,
|
||||
'last_done_km': r[5],
|
||||
'next_due_at': str(r[6]) if r[6] else None,
|
||||
'next_due_km': r[7],
|
||||
'notes': r[8], 'is_active': r[9],
|
||||
})
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
return jsonify({'data': schedules})
|
||||
|
||||
|
||||
@fleet_bp.route('/vehicles/<int:vehicle_id>/schedules', methods=['POST'])
|
||||
@require_auth()
|
||||
def create_schedule(vehicle_id):
|
||||
"""Create maintenance schedule for a vehicle.
|
||||
|
||||
Body: {maintenance_type, interval_km, interval_months, next_due_at, next_due_km, notes}
|
||||
"""
|
||||
data = request.get_json() or {}
|
||||
if not data.get('maintenance_type'):
|
||||
return jsonify({'error': 'maintenance_type is required'}), 400
|
||||
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
cur = conn.cursor()
|
||||
|
||||
# Verify vehicle exists
|
||||
cur.execute("SELECT id FROM fleet_vehicles WHERE id = %s", (vehicle_id,))
|
||||
if not cur.fetchone():
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'error': 'Vehicle not found'}), 404
|
||||
|
||||
try:
|
||||
cur.execute("""
|
||||
INSERT INTO fleet_maintenance_schedules
|
||||
(vehicle_id, maintenance_type, interval_km, interval_months,
|
||||
next_due_at, next_due_km, notes)
|
||||
VALUES (%s,%s,%s,%s,%s,%s,%s)
|
||||
RETURNING id
|
||||
""", (
|
||||
vehicle_id, data['maintenance_type'],
|
||||
data.get('interval_km'), data.get('interval_months'),
|
||||
data.get('next_due_at'), data.get('next_due_km'),
|
||||
data.get('notes'),
|
||||
))
|
||||
schedule_id = cur.fetchone()[0]
|
||||
conn.commit()
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'id': schedule_id, 'message': 'Schedule created'}), 201
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
# ─── Maintenance Logs ─────────────────────────────
|
||||
|
||||
@fleet_bp.route('/vehicles/<int:vehicle_id>/log', methods=['POST'])
|
||||
@require_auth()
|
||||
def record_maintenance(vehicle_id):
|
||||
"""Record maintenance done. Updates schedule next_due if schedule_id provided.
|
||||
|
||||
Body: {schedule_id, maintenance_type, mileage_at, cost, parts_used, notes}
|
||||
"""
|
||||
data = request.get_json() or {}
|
||||
if not data.get('maintenance_type'):
|
||||
return jsonify({'error': 'maintenance_type is required'}), 400
|
||||
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
cur = conn.cursor()
|
||||
|
||||
# Verify vehicle exists
|
||||
cur.execute("SELECT id, current_mileage FROM fleet_vehicles WHERE id = %s", (vehicle_id,))
|
||||
veh = cur.fetchone()
|
||||
if not veh:
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'error': 'Vehicle not found'}), 404
|
||||
|
||||
mileage_at = data.get('mileage_at', veh[1] or 0)
|
||||
schedule_id = data.get('schedule_id')
|
||||
|
||||
try:
|
||||
# Insert log
|
||||
cur.execute("""
|
||||
INSERT INTO fleet_maintenance_logs
|
||||
(vehicle_id, schedule_id, maintenance_type, mileage_at,
|
||||
cost, parts_used, employee_id, notes)
|
||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s)
|
||||
RETURNING id
|
||||
""", (
|
||||
vehicle_id, schedule_id, data['maintenance_type'],
|
||||
mileage_at, data.get('cost', 0), data.get('parts_used'),
|
||||
getattr(g, 'employee_id', None), data.get('notes'),
|
||||
))
|
||||
log_id = cur.fetchone()[0]
|
||||
|
||||
# Update vehicle mileage if new mileage is higher
|
||||
if mileage_at > (veh[1] or 0):
|
||||
cur.execute("UPDATE fleet_vehicles SET current_mileage = %s WHERE id = %s",
|
||||
(mileage_at, vehicle_id))
|
||||
|
||||
# Update schedule next_due if schedule_id provided
|
||||
if schedule_id:
|
||||
cur.execute("""
|
||||
SELECT interval_km, interval_months
|
||||
FROM fleet_maintenance_schedules WHERE id = %s
|
||||
""", (schedule_id,))
|
||||
sched = cur.fetchone()
|
||||
if sched:
|
||||
interval_km, interval_months = sched
|
||||
now = datetime.utcnow()
|
||||
|
||||
next_due_km = (mileage_at + interval_km) if interval_km else None
|
||||
next_due_at = None
|
||||
if interval_months:
|
||||
next_due_at = now + timedelta(days=interval_months * 30)
|
||||
|
||||
cur.execute("""
|
||||
UPDATE fleet_maintenance_schedules
|
||||
SET last_done_at = %s, last_done_km = %s,
|
||||
next_due_at = %s, next_due_km = %s
|
||||
WHERE id = %s
|
||||
""", (now, mileage_at, next_due_at, next_due_km, schedule_id))
|
||||
|
||||
log_action(conn, 'FLEET_MAINTENANCE_LOG', 'fleet_vehicle', vehicle_id,
|
||||
new_value={'maintenance_type': data['maintenance_type'],
|
||||
'mileage_at': mileage_at, 'cost': data.get('cost', 0)})
|
||||
|
||||
conn.commit()
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'id': log_id, 'message': 'Maintenance recorded'}), 201
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
# ─── Alerts ─────────────────────────────
|
||||
|
||||
@fleet_bp.route('/alerts', methods=['GET'])
|
||||
@require_auth()
|
||||
def fleet_alerts():
|
||||
"""Vehicles with overdue maintenance (next_due_at < NOW() or next_due_km < current_mileage)."""
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
cur = conn.cursor()
|
||||
|
||||
branch_filter = ""
|
||||
params = []
|
||||
if g.branch_id:
|
||||
branch_filter = "AND v.branch_id = %s"
|
||||
params.append(g.branch_id)
|
||||
|
||||
cur.execute(f"""
|
||||
SELECT v.id, v.plate, v.make, v.model, v.year, v.current_mileage,
|
||||
s.id as schedule_id, s.maintenance_type,
|
||||
s.next_due_at, s.next_due_km
|
||||
FROM fleet_maintenance_schedules s
|
||||
JOIN fleet_vehicles v ON s.vehicle_id = v.id
|
||||
WHERE s.is_active = true AND v.is_active = true
|
||||
{branch_filter}
|
||||
AND (
|
||||
(s.next_due_at IS NOT NULL AND s.next_due_at < NOW())
|
||||
OR (s.next_due_km IS NOT NULL AND s.next_due_km <= v.current_mileage)
|
||||
)
|
||||
ORDER BY s.next_due_at NULLS LAST
|
||||
""", params)
|
||||
|
||||
alerts = []
|
||||
for r in cur.fetchall():
|
||||
alerts.append({
|
||||
'vehicle_id': r[0], 'plate': r[1], 'make': r[2], 'model': r[3],
|
||||
'year': r[4], 'current_mileage': r[5],
|
||||
'schedule_id': r[6], 'maintenance_type': r[7],
|
||||
'next_due_at': str(r[8]) if r[8] else None,
|
||||
'next_due_km': r[9],
|
||||
})
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
return jsonify({'data': alerts, 'total': len(alerts)})
|
||||
|
||||
|
||||
# ─── Stats ─────────────────────────────
|
||||
|
||||
@fleet_bp.route('/stats', methods=['GET'])
|
||||
@require_auth()
|
||||
def fleet_stats():
|
||||
"""Fleet summary: total vehicles, overdue count, upcoming this month, total cost this month."""
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
cur = conn.cursor()
|
||||
|
||||
branch_filter = ""
|
||||
params = []
|
||||
if g.branch_id:
|
||||
branch_filter = "AND v.branch_id = %s"
|
||||
params.append(g.branch_id)
|
||||
|
||||
# Total active vehicles
|
||||
cur.execute(f"""
|
||||
SELECT count(*) FROM fleet_vehicles v
|
||||
WHERE v.is_active = true {branch_filter}
|
||||
""", params)
|
||||
total_vehicles = cur.fetchone()[0]
|
||||
|
||||
# Overdue count
|
||||
cur.execute(f"""
|
||||
SELECT count(DISTINCT v.id)
|
||||
FROM fleet_maintenance_schedules s
|
||||
JOIN fleet_vehicles v ON s.vehicle_id = v.id
|
||||
WHERE s.is_active = true AND v.is_active = true
|
||||
{branch_filter}
|
||||
AND (
|
||||
(s.next_due_at IS NOT NULL AND s.next_due_at < NOW())
|
||||
OR (s.next_due_km IS NOT NULL AND s.next_due_km <= v.current_mileage)
|
||||
)
|
||||
""", params)
|
||||
overdue_count = cur.fetchone()[0]
|
||||
|
||||
# Upcoming this month
|
||||
cur.execute(f"""
|
||||
SELECT count(*)
|
||||
FROM fleet_maintenance_schedules s
|
||||
JOIN fleet_vehicles v ON s.vehicle_id = v.id
|
||||
WHERE s.is_active = true AND v.is_active = true
|
||||
{branch_filter}
|
||||
AND s.next_due_at IS NOT NULL
|
||||
AND s.next_due_at >= NOW()
|
||||
AND s.next_due_at < date_trunc('month', NOW()) + interval '1 month'
|
||||
""", params)
|
||||
upcoming_this_month = cur.fetchone()[0]
|
||||
|
||||
# Total cost this month
|
||||
cur.execute(f"""
|
||||
SELECT COALESCE(SUM(l.cost), 0)
|
||||
FROM fleet_maintenance_logs l
|
||||
JOIN fleet_vehicles v ON l.vehicle_id = v.id
|
||||
WHERE v.is_active = true
|
||||
{branch_filter}
|
||||
AND l.created_at >= date_trunc('month', NOW())
|
||||
""", params)
|
||||
total_cost_month = float(cur.fetchone()[0])
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
return jsonify({
|
||||
'total_vehicles': total_vehicles,
|
||||
'overdue_count': overdue_count,
|
||||
'upcoming_this_month': upcoming_this_month,
|
||||
'total_cost_month': total_cost_month,
|
||||
})
|
||||
Reference in New Issue
Block a user