Implement standards management and update calibration workflow

This commit is contained in:
Andrew Conlon
2025-07-28 16:41:53 -04:00
parent 133a935d90
commit 7e62e61124
19 changed files with 976 additions and 198 deletions

View File

@@ -33,4 +33,8 @@ def create_app():
from app.routes.channels import channels_bp
app.register_blueprint(channels_bp, url_prefix='/channels')
# Register standards blueprint
from app.routes.standards import standards_bp
app.register_blueprint(standards_bp, url_prefix='/standards')
return app

View File

@@ -35,6 +35,7 @@ class Probe:
description: str
created_at: datetime
retired_at: Optional[datetime] = None
model_name: str = '' # Added to match dynamic attribute assignment
@classmethod
def get_by_id(cls, probe_id: str):
@@ -50,16 +51,33 @@ class Probe:
return cls(**row)
@classmethod
def get_all(cls, limit: int = 100):
"""List all probes with optional limit"""
def get_all(cls, limit: int = 100, active_only: bool = True):
"""List all probes with optional limit and active filter"""
supabase = get_supabase()
result = supabase.table('probes').select("*").limit(limit).execute()
query = supabase.table('probes') \
.select('*, probe_models(model_name)') \
.limit(limit)
if active_only:
query = query.is_('retired_at', 'null')
result = query.execute()
probes = []
for row in result.data:
# Extract model_name before creating Probe instance
model_name = row['probe_models']['model_name'] if row.get('probe_models') else 'Unknown'
# Remove probe_models from row to avoid dataclass init error
if 'probe_models' in row:
del row['probe_models']
# Parse datetime strings
row['created_at'] = datetime.fromisoformat(row['created_at']) if row['created_at'] else None
row['retired_at'] = datetime.fromisoformat(row['retired_at']) if row['retired_at'] else None
probes.append(cls(**row))
# Create probe instance
probe = cls(**row)
# Add model_name as dynamic attribute for template use
probe.model_name = model_name
probes.append(probe)
return probes
@classmethod
@@ -217,30 +235,22 @@ class Calibration:
@classmethod
def get_all(cls, limit: int = 100):
"""List all work orders with optional limit"""
"""List all calibrations with optional limit"""
supabase = get_supabase()
result = supabase.table('work_orders').select("*").limit(limit).execute()
work_orders = []
result = supabase.table('calibrations').select("*").limit(limit).execute()
calibrations = []
for row in result.data if result.data else []:
try:
# Debug print raw data
print(f"Raw work order data: {row}")
# Parse datetime strings
for date_field in ['std_cal_date', 'std_cal_due', 'date']:
if row.get(date_field):
row[date_field] = datetime.fromisoformat(row[date_field])
# Ensure due_date is properly parsed
if 'due_date' in row and row['due_date']:
if isinstance(row['due_date'], str):
row['due_date'] = datetime.fromisoformat(row['due_date'])
elif not isinstance(row['due_date'], datetime):
row['due_date'] = None
# Create instance and verify date type
wo = cls(**row)
print(f"WorkOrder instance due_date type: {type(wo.due_date)}")
work_orders.append(wo)
calibrations.append(cls(**row))
except Exception as e:
print(f"Error parsing work order data: {e}")
print(f"Error parsing calibration data: {e}")
continue
return work_orders
return calibrations
@classmethod
def get_by_work_order(cls, work_order_id: str):
@@ -317,7 +327,45 @@ class Standard:
"""List all standards with optional limit"""
supabase = get_supabase()
result = supabase.table('standards').select("*").limit(limit).execute()
return [cls(**row) for row in result.data] if result.data else []
standards = []
for row in result.data if result.data else []:
# Parse datetime strings
for date_field in ['calibrated_on', 'calibration_due']:
if row.get(date_field):
row[date_field] = datetime.fromisoformat(row[date_field])
standards.append(cls(**row))
return standards
@classmethod
def create(cls, make: str, model: str, description: str, uncertainty: str,
calibrated_on: str, calibration_due: str, support_name: str,
support_email: str, support_phone: str, support_address: str):
"""Create a new standard"""
supabase = get_supabase()
# Convert date strings to ISO format
calibrated_on_iso = datetime.fromisoformat(calibrated_on).isoformat()
calibration_due_iso = datetime.fromisoformat(calibration_due).isoformat()
result = supabase.table('standards').insert({
'make': make,
'model': model,
'description': description,
'uncertainty': uncertainty,
'calibrated_on': calibrated_on_iso,
'calibration_due': calibration_due_iso,
'support_name': support_name,
'support_email': support_email,
'support_phone': support_phone,
'support_address': support_address
}).execute()
if not result.data:
raise Exception('Failed to create standard')
# Parse dates in the returned data
row = result.data[0]
row['calibrated_on'] = datetime.fromisoformat(row['calibrated_on']) if row['calibrated_on'] else None
row['calibration_due'] = datetime.fromisoformat(row['calibration_due']) if row['calibration_due'] else None
return cls(**row)
@dataclass
class ProbeModel:
@@ -384,6 +432,22 @@ class Location:
result = supabase.table('locations').select("*").limit(limit).execute()
return [cls(**row) for row in result.data] if result.data else []
@classmethod
def create(cls, name: str, address: str, contact_name: str,
contact_email: str, contact_phone: str):
"""Create a new location"""
supabase = get_supabase()
result = supabase.table('locations').insert({
'name': name,
'address': address,
'contact_name': contact_name,
'contact_email': contact_email,
'contact_phone': contact_phone
}).execute()
if not result.data:
raise Exception('Failed to create location')
return cls(**result.data[0])
@dataclass
class ProbeLocation:
"""ProbeLocation model representing probe assignments to locations"""

View File

@@ -1,6 +1,7 @@
from flask import Blueprint, request, redirect, url_for, flash, render_template
from flask import Blueprint, request, redirect, url_for, flash, render_template, session, jsonify
from datetime import datetime
import re
from typing import Optional
from app.models import Calibration, Channel, WorkOrder, Standard, User
from app.supabase_client import get_supabase
@@ -19,7 +20,7 @@ def create_calibrations():
return redirect(request.referrer)
try:
date = datetime.strptime(date_str, '%Y-%m-%d').date()
date = datetime.strptime(str(date_str), '%Y-%m-%d').date()
except ValueError:
flash('Invalid date format', 'error')
return redirect(request.referrer)
@@ -55,10 +56,20 @@ def create_calibrations():
# Process each calibration
for i in range(len(channel_serials)):
# Get channel ID from serial
channel = supabase.table('channels').select('id').eq('serial_number', channel_serials[i]).execute()
if not channel.data:
flash(f'Channel not found: {channel_serials[i]}', 'error')
# Get channel ID from serial with detailed error handling
try:
print(f"Looking up channel with serial: {channel_serials[i]}")
channel = supabase.table('channels').select('id').eq('serial_number', channel_serials[i]).execute()
if not channel.data:
error_msg = f'Channel not found: {channel_serials[i]}'
flash(error_msg, 'error')
print(error_msg)
continue
print(f"Found channel ID: {channel.data[0]['id']}")
except Exception as e:
error_msg = f'Error looking up channel {channel_serials[i]}: {str(e)}'
flash(error_msg, 'error')
print(error_msg)
continue
calibration_data = {
@@ -80,10 +91,18 @@ def create_calibrations():
'passed': bool(passed_list[i] if i < len(passed_list) else False)
}
# Insert calibration
result = supabase.table('calibrations').insert(calibration_data).execute()
if not result.data:
flash(f'Failed to create calibration for {channel_serials[i]}', 'error')
# Insert calibration with error handling
try:
result = supabase.table('calibrations').insert(calibration_data).execute()
if not result.data:
flash(f'Failed to create calibration for {channel_serials[i]}', 'error')
print(f"Supabase insert failed for channel {channel_serials[i]}: {result}")
else:
print(f"Successfully created calibration for channel {channel_serials[i]}: {result.data}")
except Exception as e:
flash(f'Error creating calibration for {channel_serials[i]}: {str(e)}', 'error')
print(f"Exception creating calibration for {channel_serials[i]}: {str(e)}")
continue
flash('Calibrations processed', 'success')
return redirect(url_for('calibrations.index'))
@@ -144,7 +163,8 @@ def review_calibration(calibration_id):
return redirect(url_for('calibrations.index'))
return render_template('calibration_review.html',
calibration=calibration.data[0])
calibration=calibration.data[0],
user=session)
@calibrations_bp.route('/new')
def new_calibration():
@@ -160,21 +180,64 @@ def new_calibration():
# Get all standards
standards = supabase.table('standards').select('id, make, model').execute()
# Get all channels
# Get all channels (just serial numbers for now)
channels = supabase.table('channels').select('serial_number').execute()
# Get all probe models (using exact column name)
probe_models = supabase.table('probe_models').select('id, model_name').execute()
# Get all parameters (using correct column name)
parameters = supabase.table('parameters').select('id, parameter_name').execute()
return render_template('calibration_form.html',
work_orders=work_orders.data,
standards=standards.data,
channels=channels.data)
channels=channels.data,
probe_models=probe_models.data,
parameters=parameters.data,
user=session)
@calibrations_bp.route('/filtered-channels')
def get_filtered_channels():
supabase = get_supabase()
probe_model_id = request.args.get('probe_model')
parameter_id = request.args.get('parameter')
if not probe_model_id or not parameter_id:
return jsonify([])
# Get channels filtered by both probe model and parameter
channels = supabase.table('channels').select('''
serial_number,
probes:probe_id(model_id),
parameter_id
''').eq('parameter_id', parameter_id).execute()
# Filter channels by probe model (client-side fallback)
filtered_channels = []
for c in channels.data:
try:
if c['probes']['model_id'] == probe_model_id:
filtered_channels.append({
'serial_number': c['serial_number']
})
except (KeyError, TypeError):
continue
print(f"Filtered channels for model {probe_model_id} and parameter {parameter_id}: {filtered_channels}")
return jsonify(filtered_channels)
@calibrations_bp.route('/')
def index():
supabase = get_supabase()
# Get summary statistics
total = supabase.table('calibrations').select('count', count='exact').execute().count
passed = supabase.table('calibrations').select('count', count='exact').eq('passed', True).execute().count
total_result = supabase.table('calibrations').select('*').execute()
passed_result = supabase.table('calibrations').select('*').eq('passed', True).execute()
total = len(total_result.data) if total_result.data else 0
passed = len(passed_result.data) if passed_result.data else 0
failed = total - passed
# Get recent calibrations (last 10)
@@ -191,7 +254,8 @@ def index():
'passed_calibrations': passed,
'failed_calibrations': failed
},
recent_calibrations=recent_calibrations.data)
recent_calibrations=recent_calibrations.data,
user=session)
@calibrations_bp.route('/probe/<probe_id>')
def calibrations_for_probe(probe_id):
@@ -216,7 +280,8 @@ def calibrations_for_probe(probe_id):
return render_template('calibration_history.html',
calibrations=calibrations.data,
probe=probe.data[0] if probe.data else None)
probe=probe.data[0] if probe.data else None,
user=session)
@calibrations_bp.route('/probe/<probe_id>/trends')
def calibration_trends(probe_id):
@@ -280,4 +345,5 @@ def view_calibration(calibration_id):
return redirect(url_for('calibrations.index'))
return render_template('calibration_view.html',
calibration=calibration.data[0])
calibration=calibration.data[0],
user=session)

View File

@@ -1,11 +1,50 @@
from flask import Blueprint, render_template
from app.models import ProbeLocation
from flask import Blueprint, render_template, request, session, redirect
from app.models import ProbeLocation, Location
locations_bp = Blueprint('locations', __name__)
from datetime import datetime
import json
@locations_bp.route('/')
def locations_index():
"""Show all locations"""
locations = Location.get_all()
return render_template('location_timeline.html',
locations=locations,
user=session)
@locations_bp.route('/new', methods=['GET', 'POST'])
def new_location():
"""Create a new location"""
if request.method == 'POST':
name = request.form.get('name', '').strip()
address = request.form.get('address', '').strip()
contact_name = request.form.get('contact_name', '').strip()
contact_email = request.form.get('contact_email', '').strip()
contact_phone = request.form.get('contact_phone', '').strip()
if not name:
return render_template('location_form.html',
error='Name is required',
user=session)
try:
location = Location.create(
name=name,
address=address,
contact_name=contact_name,
contact_email=contact_email,
contact_phone=contact_phone
)
return redirect('/locations/')
except Exception as e:
return render_template('location_form.html',
error=str(e),
user=session)
return render_template('location_form.html', user=session)
@locations_bp.route('/probe/<probe_id>/locations')
def probe_location_history(probe_id):
"""Show timeline of location assignments for a probe"""
@@ -35,4 +74,5 @@ def probe_location_history(probe_id):
probe_id=probe_id,
location_data=location_data,
chart_data_json=json.dumps(chart_data),
now=now)
now=now,
user=session)

View File

@@ -1,5 +1,5 @@
import re
from flask import Blueprint, render_template, request, redirect, url_for, flash
from flask import Blueprint, render_template, request, redirect, url_for, flash, session
from app.models import Probe, ProbeModel, Channel, Parameter, ProbeLocation
from app.routes.auth import role_required
@@ -8,9 +8,13 @@ probes_bp = Blueprint('probes', __name__)
@probes_bp.route('/')
@role_required('review')
def list_probes():
"""List all probes"""
probes = Probe.get_all()
return render_template('probe_list.html', probes=probes)
"""List all probes with optional active filter"""
active_only = request.args.get('active_only', 'true').lower() == 'true'
probes = Probe.get_all(active_only=active_only)
return render_template('probe_list.html',
probes=probes,
active_only=active_only,
user=session)
@probes_bp.route('/new')
@role_required('review')
@@ -20,7 +24,8 @@ def new_probe():
parameters = Parameter.get_all()
return render_template('probe_form.html',
probe_models=probe_models,
parameters=parameters)
parameters=parameters,
user=session)
@probes_bp.route('/', methods=['POST'])
@role_required('review')
@@ -77,4 +82,5 @@ def view_probe(probe_id):
return render_template('probe_view.html',
probe=probe,
channels=channels,
locations=locations)
locations=locations,
user=session)

60
app/routes/standards.py Normal file
View File

@@ -0,0 +1,60 @@
import re
from flask import Blueprint, render_template, request, redirect, url_for, flash, session
from app.models import Standard
from app.routes.auth import role_required
standards_bp = Blueprint('standards', __name__)
@standards_bp.route('/')
@role_required('review')
def list_standards():
"""List all standards"""
standards = Standard.get_all()
return render_template('standards_list.html',
standards=standards,
user=session)
@standards_bp.route('/new')
@role_required('review')
def new_standard():
"""Display form to create new standard"""
return render_template('standards_form.html',
user=session)
@standards_bp.route('/', methods=['POST'])
@role_required('review')
def create_standard():
"""Handle new standard creation"""
try:
# Create the standard
standard = Standard.create(
make=request.form['make'],
model=request.form['model'],
description=request.form['description'],
uncertainty=request.form['uncertainty'],
calibrated_on=request.form['calibrated_on'],
calibration_due=request.form['calibration_due'],
support_name=request.form['support_name'],
support_email=request.form['support_email'],
support_phone=request.form['support_phone'],
support_address=request.form['support_address']
)
flash('Standard created successfully', 'success')
return redirect(url_for('standards.list_standards'))
except Exception as e:
flash(f'Error creating standard: {str(e)}', 'danger')
return redirect(url_for('standards.new_standard'))
@standards_bp.route('/<standard_id>')
@role_required('review')
def view_standard(standard_id):
"""View details of a single standard"""
standard = Standard.get_by_id(standard_id)
if not standard:
flash('Standard not found', 'danger')
return redirect(url_for('standards.list_standards'))
return render_template('standards_view.html',
standard=standard,
user=session)

View File

@@ -1,4 +1,4 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash
from flask import Blueprint, render_template, request, redirect, url_for, flash, session
from app.models import WorkOrder, Customer, User, CalibrationType, Calibration
from app.supabase_client import get_supabase
from app.routes.auth import role_required
@@ -12,7 +12,9 @@ def list_work_orders():
work_orders = WorkOrder.get_all()
if not work_orders:
flash('No work orders found', 'info')
return render_template('work_order_list.html', work_orders=work_orders)
return render_template('work_order_list.html',
work_orders=work_orders,
user=session)
@work_orders_bp.route('/new')
@role_required('review')
@@ -24,7 +26,8 @@ def new_work_order():
return render_template('work_order_form.html',
customers=customers,
users=users,
calibration_types=calibration_types)
calibration_types=calibration_types,
user=session)
@work_orders_bp.route('/', methods=['POST'])
@role_required('review')
@@ -58,7 +61,8 @@ def view_work_order(work_order_id):
calibrations = Calibration.get_by_work_order(work_order_id)
return render_template('work_order_view.html',
work_order=work_order,
calibrations=calibrations)
calibrations=calibrations,
user=session)
@work_orders_bp.route('/<work_order_id>/status', methods=['POST'])
@role_required('review')

View File

@@ -13,8 +13,18 @@
## Database Schema
### Tables
0. **calibration_audit**
- id (bigserial, PK)
- calibration_id (uuid, FK → calibrations.id)
- user_id (uuid, FK → users.id)
- action (text)
- old_values (jsonb)
- new_values (jsonb)
- timestamp (timestamptz)
- ip_address (text)
1. **probes**
- id (uuid, PK)
- id (uuid, PK, DEFAULT gen_random_uuid())
- model_id (uuid, FK → probe_models.id)
- serial_number (text)
- description (text)
@@ -22,14 +32,14 @@
- retired_at (timestamp)
2. **channels**
- id (uuid, PK)
- id (uuid, PK, DEFAULT gen_random_uuid())
- probe_id (uuid, FK → probes.id)
- serial_number (varchar(16), regex: [0-9A-F]{16})
- parameter_id (uuid, FK → parameters.id)
- created_at (timestamp)
3. **calibrations**
- id (uuid, PK)
- id (uuid, PK, DEFAULT gen_random_uuid())
- channel_id (uuid, FK → channels.id)
- work_order_id (uuid, FK → work_orders.id)
- calibrated_by (uuid, FK → users.id)
@@ -49,7 +59,7 @@
- passed (boolean)
4. **locations**
- id (uuid, PK)
- id (uuid, PK, DEFAULT gen_random_uuid())
- name (text)
- address (text)
- contact_name (text)
@@ -57,19 +67,19 @@
- contact_phone (text)
5. **probe_locations**
- id (uuid, PK)
- id (uuid, PK, DEFAULT gen_random_uuid())
- probe_id (uuid, FK → probes.id)
- location_id (uuid, FK → locations.id)
- start_date (date)
- end_date (date)
6. **probe_models**
- id (uuid, PK)
- id (uuid, PK, DEFAULT gen_random_uuid())
- model_name (text, unique)
- specifications (jsonb)
7. **work_orders**
- id (uuid, PK)
- id (uuid, PK, DEFAULT gen_random_uuid())
- order_number (text, unique)
- customer_id (uuid, FK → customers.id)
- assigned_to (uuid, FK → users.id)
@@ -79,7 +89,7 @@
- cal_type (uuid, FK → calibration_types.id)
8. **users**
- id (uuid, PK)
- id (uuid, PK, DEFAULT gen_random_uuid())
- name (text)
- email (text, unique)
- can_calibrate (boolean)
@@ -87,7 +97,7 @@
- signature_image (text)
10. **standards**
- id (uuid, PK)
- id (uuid, PK, DEFAULT gen_random_uuid())
- make (text)
- model (text)
- description (text)
@@ -100,11 +110,11 @@
- support_address (text)
11. **calibration_types**
- id (uuid, PK)
- id (uuid, PK, DEFAULT gen_random_uuid())
- type_name (text)
12. **parameters**
- id (uuid, PK)
- id (uuid, PK, DEFAULT gen_random_uuid())
- parameter_name (text)
### Relationships

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - SmartScan Probe Track</title>
<title>Login - CIMTechniques Probe Tracker</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
@@ -12,7 +12,7 @@
<div class="col-md-6 col-lg-4">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">SmartScan Login</h4>
<h4 class="mb-0">CIMTechniques Login</h4>
</div>
<div class="card-body">
{% if error %}

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}SmartScan Probe Track{% endblock %}</title>
<title>{% block title %}CIMTechniques Probe Tracker{% endblock %}</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
{% block head %}{% endblock %}
@@ -11,9 +11,25 @@
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/">SmartScan Probe Track</a>
<a class="navbar-brand" href="/">CIMTechniques Probe Tracker</a>
<div class="navbar-nav">
<a class="nav-link" href="/probes/">Probes</a>
{% if user %}
<a class="nav-link" href="/probes">Probes</a>
<a class="nav-link" href="/calibrations">Calibrations</a>
<a class="nav-link" href="/work_orders">Work Orders</a>
<a class="nav-link" href="/locations">Locations</a>
<a class="nav-link" href="{{ url_for('standards.list_standards') }}">Standards</a>
<span class="nav-link">
{% if user.user_name %}
{{ user.user_name }}
{% else %}
{{ user.get('user_name', '') }}
{% endif %}
</span>
<a class="nav-link" href="/auth/logout">Logout</a>
{% else %}
<a class="nav-link" href="/auth/login">Login</a>
{% endif %}
</div>
</div>
</nav>

View File

@@ -1,6 +1,7 @@
{% extends "base.html" %}
{% block head %}
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
{% endblock %}
@@ -9,7 +10,8 @@
<div class="container mt-4">
<h2>New Calibration Batch</h2>
<form method="POST" action="{{ url_for('calibrations.create_calibrations') }}">
<form method="POST" action="{{ url_for('calibrations.create_calibrations') }}" onsubmit="return validateForm()">
<input type="hidden" name="calibrated_by" value="{{ user.id }}">
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title">Batch Information</h5>
@@ -40,15 +42,32 @@
<input type="date" class="form-control" id="date" name="date" required>
</div>
<div class="col-md-6">
<label for="calibrated_by" class="form-label">Calibrated By</label>
<input type="text" class="form-control" id="calibrated_by" name="calibrated_by" required>
<div class="col-md-3">
<label for="probe_model" class="form-label">Probe Model</label>
<select class="form-select select2" id="probe_model" name="probe_model" required>
<option value="">Select Model</option>
{% for model in probe_models %}
<option value="{{ model.id }}">{{ model.model_name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-3">
<label for="parameter" class="form-label">Parameter</label>
<select class="form-select select2" id="parameter" name="parameter" required>
<option value="">Select Parameter</option>
{% for param in parameters %}
<option value="{{ param.id }}">{{ param.parameter_name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="text-end mt-3">
<button type="button" id="confirmBatch" class="btn btn-success">Confirm Batch Info</button>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card mb-4" id="channelsSection" style="display: none;">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="card-title mb-0">Channels</h5>
@@ -58,56 +77,55 @@
</div>
<div id="channelRows">
<!-- Channel rows will be added here -->
<div class="channel-row mb-3">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Channel</label>
<select class="form-select select2" name="channel_serial[]" required>
<option value="">Select Channel</option>
{% for channel in channels %}
<option value="{{ channel.serial_number }}">{{ channel.serial_number }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<label class="form-label">Scale</label>
<input type="number" step="0.001" class="form-control" name="scale[]" required>
</div>
<div class="col-md-2">
<label class="form-label">Offset</label>
<input type="number" step="0.001" class="form-control" name="offset[]" required>
</div>
<div class="col-md-2">
<label class="form-label">Deviation High</label>
<input type="number" step="0.001" class="form-control" name="deviation_high[]" required>
</div>
<div class="col-md-2">
<label class="form-label">Deviation Mid</label>
<input type="number" step="0.001" class="form-control" name="deviation_mid[]" required>
</div>
<div class="col-md-2">
<label class="form-label">Deviation Low</label>
<input type="number" step="0.001" class="form-control" name="deviation_low[]" required>
</div>
<div class="col-md-2">
<label class="form-label">Set High</label>
<input type="number" step="0.001" class="form-control" name="set_high[]" required>
</div>
<div class="col-md-2">
<label class="form-label">Set Mid</label>
<input type="number" step="0.001" class="form-control" name="set_mid[]" required>
</div>
<div class="col-md-2">
<label class="form-label">Set Low</label>
<input type="number" step="0.001" class="form-control" name="set_low[]" required>
</div>
<div class="col-md-2">
<div class="form-check form-switch mt-4">
<input class="form-check-input" type="checkbox" name="passed[]" checked>
<label class="form-check-label">Passed</label>
</div>
</div>
<div class="col-md-3">
<label class="form-label">Channel</label>
<select class="form-select select2" name="channel_serial[]" required>
<option value="">Select Channel</option>
{% for channel in channels %}
<option value="{{ channel.serial_number }}">{{ channel.serial_number }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-1">
<label class="form-label">Scale</label>
<input type="number" step="0.001" class="form-control" name="scale[]" required>
</div>
<div class="col-md-1">
<label class="form-label">Offset</label>
<input type="number" step="0.001" class="form-control" name="offset[]" required>
</div>
<div class="col-md-1">
<label class="form-label">Dev Low</label>
<input type="number" step="0.001" class="form-control" name="deviation_low[]" required>
</div>
<div class="col-md-1">
<label class="form-label">Dev Mid</label>
<input type="number" step="0.001" class="form-control" name="deviation_mid[]" required>
</div>
<div class="col-md-1">
<label class="form-label">Dev High</label>
<input type="number" step="0.001" class="form-control" name="deviation_high[]" required>
</div>
<div class="col-md-1">
<label class="form-label">Set Low</label>
<input type="number" step="0.001" class="form-control" name="set_low[]" required>
</div>
<div class="col-md-1">
<label class="form-label">Set Mid</label>
<input type="number" step="0.001" class="form-control" name="set_mid[]" required>
</div>
<div class="col-md-1">
<label class="form-label">Set High</label>
<input type="number" step="0.001" class="form-control" name="set_high[]" required>
</div>
<div class="col-md-1">
<div class="form-check form-switch mt-4">
<input class="form-check-input" type="checkbox" name="passed[]" checked>
<label class="form-check-label">Passed</label>
</div>
</div>
</div>
</div>
</div>
@@ -115,63 +133,152 @@
</div>
<div class="text-end">
<button type="submit" class="btn btn-primary">Save Calibration Batch</button>
<button type="submit" class="btn btn-primary" id="saveBatch" disabled>Save Calibration Batch</button>
</div>
</form>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize Select2
$(function() {
// Initialize Select2 and confirm button
$('.select2').select2();
// Add channel row
$('#addChannel').click(function() {
console.log('Initializing calibration form...');
// Handle batch confirmation
$('#confirmBatch').on('click', function(e) {
e.preventDefault();
console.log('Confirm Batch clicked');
// Debug: Check if jQuery is working
console.log('jQuery version:', $.fn.jquery);
// Validate required fields
if (!$('#work_order_id').val() || !$('#std_used').val() ||
!$('#date').val() || !$('#probe_model').val() || !$('#parameter').val()) {
alert('Please complete all Batch Information fields');
return;
}
// Validate date is not in future
const selectedDate = new Date($('#date').val());
const today = new Date();
if (selectedDate > today) {
alert('Calibration date cannot be in the future');
return;
}
// Disable and gray out batch section
$('.card-body', $(this).closest('.card')).find('input, select').prop('disabled', true);
$(this).closest('.card').addClass('bg-light');
$('#confirmBatch').prop('disabled', true).removeClass('btn-success').addClass('btn-secondary');
// Show channels section with filtered options
$('#channelsSection').show();
updateChannelDropdowns($('#probe_model').val(), $('#parameter').val());
$('#saveBatch').prop('disabled', false);
// Remove the initial empty channel row if present
if ($('.channel-row').length === 1 && !$('select[name="channel_serial[]"]').val()) {
$('.channel-row').remove();
}
// Add first channel row with filtered options
addChannelRow();
});
// Function to update channel dropdowns
function updateChannelDropdowns(probeModelId, parameterId) {
if (!probeModelId || !parameterId) {
return;
}
$.get('/calibrations/filtered-channels', {
probe_model: probeModelId,
parameter: parameterId
}, function(data) {
window.filteredChannels = data; // Cache the filtered channels
updateAllChannelDropdowns(data);
});
}
function updateAllChannelDropdowns(channels) {
$('select[name="channel_serial[]"]').each(function() {
const $select = $(this);
const currentVal = $select.val();
$select.empty().append('<option value="">Select Channel</option>');
channels.forEach(function(channel) {
$select.append($('<option>', {
value: channel.serial_number,
text: channel.serial_number
}));
});
if (currentVal && channels.some(c => c.serial_number === currentVal)) {
$select.val(currentVal);
}
$select.trigger('change');
});
}
// Watch for changes in probe model and parameter
$('#probe_model, #parameter').on('change', function() {
const probeModelId = $('#probe_model').val();
const parameterId = $('#parameter').val();
updateChannelDropdowns(probeModelId, parameterId);
});
// Add channel row with current filter
function addChannelRow() {
const channelOptions = window.filteredChannels ?
window.filteredChannels.map(c =>
`<option value="${c.serial_number}">${c.serial_number}</option>`
).join('') :
'';
const newRow = `
<div class="channel-row mb-3">
<div class="row g-3">
<div class="col-md-4">
<div class="col-md-3">
<label class="form-label">Channel</label>
<select class="form-select select2" name="channel_serial[]" required>
<option value="">Select Channel</option>
{% for channel in channels %}
<option value="{{ channel.serial_number }}">{{ channel.serial_number }}</option>
{% endfor %}
${channelOptions}
</select>
</div>
<div class="col-md-2">
<div class="col-md-1">
<label class="form-label">Scale</label>
<input type="number" step="0.001" class="form-control" name="scale[]" required>
</div>
<div class="col-md-2">
<div class="col-md-1">
<label class="form-label">Offset</label>
<input type="number" step="0.001" class="form-control" name="offset[]" required>
</div>
<div class="col-md-2">
<label class="form-label">Deviation High</label>
<input type="number" step="0.001" class="form-control" name="deviation_high[]" required>
</div>
<div class="col-md-2">
<label class="form-label">Deviation Mid</label>
<input type="number" step="0.001" class="form-control" name="deviation_mid[]" required>
</div>
<div class="col-md-2">
<label class="form-label">Deviation Low</label>
<div class="col-md-1">
<label class="form-label">Dev Low</label>
<input type="number" step="0.001" class="form-control" name="deviation_low[]" required>
</div>
<div class="col-md-2">
<label class="form-label">Set High</label>
<input type="number" step="0.001" class="form-control" name="set_high[]" required>
<div class="col-md-1">
<label class="form-label">Dev Mid</label>
<input type="number" step="0.001" class="form-control" name="deviation_mid[]" required>
</div>
<div class="col-md-2">
<label class="form-label">Set Mid</label>
<input type="number" step="0.001" class="form-control" name="set_mid[]" required>
<div class="col-md-1">
<label class="form-label">Dev High</label>
<input type="number" step="0.001" class="form-control" name="deviation_high[]" required>
</div>
<div class="col-md-2">
<div class="col-md-1">
<label class="form-label">Set Low</label>
<input type="number" step="0.001" class="form-control" name="set_low[]" required>
</div>
<div class="col-md-2">
<div class="col-md-1">
<label class="form-label">Set Mid</label>
<input type="number" step="0.001" class="form-control" name="set_mid[]" required>
</div>
<div class="col-md-1">
<label class="form-label">Set High</label>
<input type="number" step="0.001" class="form-control" name="set_high[]" required>
</div>
<div class="col-md-1">
<div class="form-check form-switch mt-4">
<input class="form-check-input" type="checkbox" name="passed[]" checked>
<label class="form-check-label">Passed</label>
@@ -179,9 +286,59 @@ document.addEventListener('DOMContentLoaded', function() {
</div>
</div>
</div>`;
$('#channelRows').append(newRow);
$('.select2').select2();
});
}
$('#addChannel').click(addChannelRow);
// Initialize with any existing selections
if ($('#probe_model').val() && $('#parameter').val()) {
updateChannelDropdowns($('#probe_model').val(), $('#parameter').val());
}
// Form validation before submission
function validateForm() {
// Validate channel selections
const serialPattern = /^[0-9A-F]{16}$/;
const serials = $('select[name="channel_serial[]"]');
let valid = true;
serials.each(function() {
const serial = $(this).val();
if (!serial || !serialPattern.test(serial)) {
alert('Invalid channel serial number: ' + serial);
$(this).focus();
valid = false;
return false; // break loop
}
});
if (!valid) return false;
// Validate numeric fields
const numericFields = [
'scale[]', 'offset[]', 'deviation_low[]',
'deviation_mid[]', 'deviation_high[]',
'set_low[]', 'set_mid[]', 'set_high[]'
];
for (const field of numericFields) {
$(`input[name="${field}"]`).each(function() {
const val = parseFloat($(this).val());
if (isNaN(val)) {
alert('Please enter valid numbers for all fields');
$(this).focus();
valid = false;
return false;
}
});
if (!valid) break;
}
return valid;
}
});
</script>
{% endblock %}

View File

@@ -1,24 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SmartScan Probe Track</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="#">SmartScan Probe Track</a>
<div class="navbar-nav ms-auto">
<span class="navbar-text me-3">
Logged in as: {{ user.user_name }}
</span>
<a href="/auth/logout" class="btn btn-outline-light">Logout</a>
</div>
</div>
</nav>
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<div class="row">
<div class="col-md-3">
@@ -31,6 +13,7 @@
<a href="/calibrations" class="list-group-item list-group-item-action">Calibrations</a>
<a href="/work_orders" class="list-group-item list-group-item-action">Work Orders</a>
<a href="/locations" class="list-group-item list-group-item-action">Locations</a>
<a href="{{ url_for('standards.list_standards') }}" class="list-group-item list-group-item-action">Standards</a>
</div>
</div>
</div>
@@ -40,7 +23,7 @@
Dashboard
</div>
<div class="card-body">
<h5 class="card-title">Welcome to SmartScan Probe Track</h5>
<h5 class="card-title">Welcome to CIMTechniques Probe Tracker</h5>
<p class="card-text">You are logged in as {{ user.user_name }}.</p>
{% if user.can_calibrate %}
<span class="badge bg-success">Calibrator</span>
@@ -53,7 +36,4 @@
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
{% endblock %}

View File

@@ -0,0 +1,43 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<h2>Add New Location</h2>
{% if error %}
<div class="alert alert-danger">
{{ error }}
</div>
{% endif %}
<form method="POST" action="/locations/new">
<div class="mb-3">
<label for="name" class="form-label">Location Name*</label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="mb-3">
<label for="address" class="form-label">Address</label>
<input type="text" class="form-control" id="address" name="address">
</div>
<div class="mb-3">
<label for="contact_name" class="form-label">Contact Name</label>
<input type="text" class="form-control" id="contact_name" name="contact_name">
</div>
<div class="mb-3">
<label for="contact_email" class="form-label">Contact Email</label>
<input type="email" class="form-control" id="contact_email" name="contact_email">
</div>
<div class="mb-3">
<label for="contact_phone" class="form-label">Contact Phone</label>
<input type="tel" class="form-control" id="contact_phone" name="contact_phone">
</div>
<button type="submit" class="btn btn-primary">Save Location</button>
<a href="/locations/" class="btn btn-secondary">Cancel</a>
</form>
</div>
{% endblock %}

View File

@@ -2,17 +2,62 @@
{% block content %}
<div class="container mt-4">
<h2>Location History for Probe {{ probe_id }}</h2>
{% if probe_id %}
<h2>Location History for Probe {{ probe_id }}</h2>
{% else %}
<h2>All Locations</h2>
<div class="mb-3">
<a href="/locations/new" class="btn btn-primary">Add New Location</a>
</div>
<p>Select a probe to view its location history</p>
{% endif %}
{% if locations and not probe_id %}
<div class="card mt-4">
<div class="card-header">
<h5>Locations List</h5>
</div>
<div class="card-body">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Address</th>
<th>Contact</th>
<th>Phone</th>
<th>Email</th>
</tr>
</thead>
<tbody>
{% for location in locations %}
<tr>
<td>{{ location.name }}</td>
<td>{{ location.address }}</td>
<td>{{ location.contact_name }}</td>
<td>{{ location.contact_phone }}</td>
<td>{{ location.contact_email }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<div class="card mt-4">
<div class="card-header">
<h5>Assignment Timeline</h5>
</div>
<div class="card-body">
<canvas id="timelineChart" height="100"></canvas>
{% if chart_data_json %}
<canvas id="timelineChart" height="100"></canvas>
{% else %}
<p>No location data available</p>
{% endif %}
</div>
</div>
{% if location_data %}
<div class="card mt-4">
<div class="card-header">
<h5>Location Details</h5>
@@ -40,6 +85,7 @@
</table>
</div>
</div>
{% endif %}
</div>
{% block scripts %}

View File

@@ -8,8 +8,14 @@
</div>
<div class="card">
<div class="card-header">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">All Probes</h5>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="activeFilter"
{% if active_only %}checked{% endif %}
onchange="window.location.search = '?active_only=' + this.checked">
<label class="form-check-label" for="activeFilter">Only show active probes</label>
</div>
</div>
<div class="card-body">
{% if probes %}
@@ -17,6 +23,7 @@
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Model</th>
<th>Serial Number</th>
<th>Description</th>
<th>Created</th>
@@ -25,23 +32,33 @@
</tr>
</thead>
<tbody>
{% for probe in probes %}
<tr>
<td>{{ probe.serial_number }}</td>
<td>{{ probe.description }}</td>
<td>{{ probe.created_at.strftime('%Y-%m-%d') }}</td>
<td>
{% if probe.retired_at %}
<span class="badge bg-secondary">Retired</span>
{% else %}
<span class="badge bg-success">Active</span>
{% endif %}
</td>
<td>
<a href="{{ url_for('probes.view_probe', probe_id=probe.id) }}"
class="btn btn-sm btn-outline-primary">View</a>
</td>
</tr>
{% set current_model = none %}
{% for probe in probes|sort(attribute='model_name') %}
{% if probe.model_name != current_model %}
<tr class="table-info">
<td colspan="6">
<strong>{{ probe.model_name }}</strong>
</td>
</tr>
{% set current_model = probe.model_name %}
{% endif %}
<tr class="probe-row" data-active="{{ 'true' if not probe.retired_at else 'false' }}">
<td></td> <!-- Empty cell under model header -->
<td>{{ probe.serial_number }}</td>
<td>{{ probe.description }}</td>
<td>{{ probe.created_at.strftime('%Y-%m-%d') }}</td>
<td>
{% if probe.retired_at %}
<span class="badge bg-secondary">Retired</span>
{% else %}
<span class="badge bg-success">Active</span>
{% endif %}
</td>
<td>
<a href="{{ url_for('probes.view_probe', probe_id=probe.id) }}"
class="btn btn-sm btn-outline-primary">View</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
@@ -52,4 +69,21 @@
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const activeFilter = document.getElementById('activeFilter');
function filterProbes() {
const showActiveOnly = activeFilter.checked;
document.querySelectorAll('.probe-row').forEach(row => {
const isActive = row.dataset.active === 'true';
row.style.display = (showActiveOnly && !isActive) ? 'none' : '';
});
}
// Initial filter on page load
filterProbes();
});
</script>
{% endblock %}

View File

@@ -0,0 +1,66 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<h2>Add New Standard</h2>
<form method="POST" action="{{ url_for('standards.create_standard') }}">
<div class="row mb-3">
<div class="col-md-6">
<label for="make" class="form-label">Make</label>
<input type="text" class="form-control" id="make" name="make" required>
</div>
<div class="col-md-6">
<label for="model" class="form-label">Model</label>
<input type="text" class="form-control" id="model" name="model" required>
</div>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<input type="text" class="form-control" id="description" name="description">
</div>
<div class="mb-3">
<label for="uncertainty" class="form-label">Uncertainty</label>
<input type="text" class="form-control" id="uncertainty" name="uncertainty" required>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="calibrated_on" class="form-label">Last Calibration Date</label>
<input type="date" class="form-control" id="calibrated_on" name="calibrated_on" required>
</div>
<div class="col-md-6">
<label for="calibration_due" class="form-label">Next Calibration Due</label>
<input type="date" class="form-control" id="calibration_due" name="calibration_due" required>
</div>
</div>
<div class="card mb-3">
<div class="card-header">
<h5>Support Information</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label for="support_name" class="form-label">Support Contact Name</label>
<input type="text" class="form-control" id="support_name" name="support_name" required>
</div>
<div class="mb-3">
<label for="support_email" class="form-label">Support Email</label>
<input type="email" class="form-control" id="support_email" name="support_email" required>
</div>
<div class="mb-3">
<label for="support_phone" class="form-label">Support Phone</label>
<input type="tel" class="form-control" id="support_phone" name="support_phone" required>
</div>
<div class="mb-3">
<label for="support_address" class="form-label">Support Address</label>
<textarea class="form-control" id="support_address" name="support_address" rows="3" required></textarea>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">Save Standard</button>
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,51 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Standards Management</h2>
<a href="{{ url_for('standards.new_standard') }}" class="btn btn-primary">Add New Standard</a>
</div>
<div class="card">
<div class="card-header">
<h5 class="mb-0">All Standards</h5>
</div>
<div class="card-body">
{% if standards %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Make</th>
<th>Model</th>
<th>Description</th>
<th>Last Calibration</th>
<th>Next Calibration Due</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for standard in standards %}
<tr>
<td>{{ standard.make }}</td>
<td>{{ standard.model }}</td>
<td>{{ standard.description }}</td>
<td>{{ standard.calibrated_on.strftime('%Y-%m-%d') }}</td>
<td>{{ standard.calibration_due.strftime('%Y-%m-%d') }}</td>
<td>
<a href="{{ url_for('standards.view_standard', standard_id=standard.id) }}"
class="btn btn-sm btn-outline-primary">View</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">No standards found. Click "Add New Standard" to create one.</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,71 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<div class="card">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<h2 class="mb-0">{{ standard.make }} {{ standard.model }}</h2>
<span class="badge bg-info">Standard</span>
</div>
</div>
<div class="card-body">
<div class="row mb-4">
<div class="col-md-6">
<h5>Standard Details</h5>
<dl class="row">
<dt class="col-sm-4">Make</dt>
<dd class="col-sm-8">{{ standard.make }}</dd>
<dt class="col-sm-4">Model</dt>
<dd class="col-sm-8">{{ standard.model }}</dd>
<dt class="col-sm-4">Description</dt>
<dd class="col-sm-8">{{ standard.description }}</dd>
<dt class="col-sm-4">Uncertainty</dt>
<dd class="col-sm-8">{{ standard.uncertainty }}</dd>
</dl>
</div>
<div class="col-md-6">
<h5>Calibration Dates</h5>
<dl class="row">
<dt class="col-sm-4">Last Calibration</dt>
<dd class="col-sm-8">{{ standard.calibrated_on.strftime('%Y-%m-%d') }}</dd>
<dt class="col-sm-4">Next Calibration Due</dt>
<dd class="col-sm-8">{{ standard.calibration_due.strftime('%Y-%m-%d') }}</dd>
</dl>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h5>Support Information</h5>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-3">Contact Name</dt>
<dd class="col-sm-9">{{ standard.support_name }}</dd>
<dt class="col-sm-3">Email</dt>
<dd class="col-sm-9">{{ standard.support_email }}</dd>
<dt class="col-sm-3">Phone</dt>
<dd class="col-sm-9">{{ standard.support_phone }}</dd>
<dt class="col-sm-3">Address</dt>
<dd class="col-sm-9">{{ standard.support_address }}</dd>
</dl>
</div>
</div>
<div class="mt-4">
<a href="{{ url_for('standards.list_standards') }}"
class="btn btn-outline-secondary">Back to List</a>
</div>
</div>
</div>
</div>
{% endblock %}

60
verify_schema.py Normal file
View File

@@ -0,0 +1,60 @@
import os
import psycopg2
from dotenv import load_dotenv
def get_supabase_schema():
load_dotenv()
conn = psycopg2.connect(os.getenv('DATABASE_URL'))
cursor = conn.cursor()
# Get tables and columns
cursor.execute("""
SELECT table_name, column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_schema = 'public'
ORDER BY table_name, ordinal_position
""")
schema = {}
for table, column, dtype, nullable, default in cursor.fetchall():
if table not in schema:
schema[table] = []
schema[table].append({
'column': column,
'type': dtype,
'nullable': nullable == 'YES',
'default': default
})
# Get foreign keys
cursor.execute("""
SELECT
tc.table_name,
kcu.column_name,
ccu.table_name AS foreign_table_name,
ccu.column_name AS foreign_column_name
FROM
information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu
ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage AS ccu
ON ccu.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
""")
fks = cursor.fetchall()
cursor.close()
conn.close()
return schema, fks
def compare_with_plan(schema, fks):
# TODO: Implement comparison with project_plan.md
# For now just print the schema
print("Current Supabase Schema:")
for table, columns in schema.items():
print(f"\nTable: {table}")
for col in columns:
print(f" {col['column']}: {col['type']} {'(nullable)' if col['nullable'] else ''}")
if __name__ == "__main__":
schema, fks = get_supabase_schema()
compare_with_plan(schema, fks)