diff --git a/app/__init__.py b/app/__init__.py index 17faa32..8c55bb1 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -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 diff --git a/app/models.py b/app/models.py index 9570b25..0ed7e83 100644 --- a/app/models.py +++ b/app/models.py @@ -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""" diff --git a/app/routes/calibrations.py b/app/routes/calibrations.py index 1412ba2..0763bcd 100644 --- a/app/routes/calibrations.py +++ b/app/routes/calibrations.py @@ -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/') 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//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) diff --git a/app/routes/locations.py b/app/routes/locations.py index 758a775..e15f72c 100644 --- a/app/routes/locations.py +++ b/app/routes/locations.py @@ -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//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) diff --git a/app/routes/probes.py b/app/routes/probes.py index ab3eaeb..5402a8e 100644 --- a/app/routes/probes.py +++ b/app/routes/probes.py @@ -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) diff --git a/app/routes/standards.py b/app/routes/standards.py new file mode 100644 index 0000000..1837794 --- /dev/null +++ b/app/routes/standards.py @@ -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('/') +@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) diff --git a/app/routes/work_orders.py b/app/routes/work_orders.py index 1ed0872..548fb53 100644 --- a/app/routes/work_orders.py +++ b/app/routes/work_orders.py @@ -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('//status', methods=['POST']) @role_required('review') diff --git a/project_plan.md b/project_plan.md index b690f72..f195b8b 100644 --- a/project_plan.md +++ b/project_plan.md @@ -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 diff --git a/templates/auth/login.html b/templates/auth/login.html index a262ccf..0afff23 100644 --- a/templates/auth/login.html +++ b/templates/auth/login.html @@ -3,7 +3,7 @@ - Login - SmartScan Probe Track + Login - CIMTechniques Probe Tracker @@ -12,7 +12,7 @@
-

SmartScan Login

+

CIMTechniques Login

{% if error %} diff --git a/templates/base.html b/templates/base.html index 582f4ce..ba827d1 100644 --- a/templates/base.html +++ b/templates/base.html @@ -3,7 +3,7 @@ - {% block title %}SmartScan Probe Track{% endblock %} + {% block title %}CIMTechniques Probe Tracker{% endblock %} {% block head %}{% endblock %} @@ -11,9 +11,25 @@ diff --git a/templates/calibration_form.html b/templates/calibration_form.html index 070b894..378f5fa 100644 --- a/templates/calibration_form.html +++ b/templates/calibration_form.html @@ -1,6 +1,7 @@ {% extends "base.html" %} {% block head %} + {% endblock %} @@ -9,7 +10,8 @@

New Calibration Batch

-
+ +
Batch Information
@@ -40,15 +42,32 @@
-
- - +
+ +
+
+ + +
+
+
+
-
+ {% endblock %} diff --git a/templates/index.html b/templates/index.html index bbf9603..0a58128 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,24 +1,6 @@ - - - - - - SmartScan Probe Track - - - - +{% extends "base.html" %} +{% block content %} @@ -40,7 +23,7 @@ Dashboard
-
Welcome to SmartScan Probe Track
+
Welcome to CIMTechniques Probe Tracker

You are logged in as {{ user.user_name }}.

{% if user.can_calibrate %} Calibrator @@ -53,7 +36,4 @@
- - - - +{% endblock %} diff --git a/templates/location_form.html b/templates/location_form.html new file mode 100644 index 0000000..c748051 --- /dev/null +++ b/templates/location_form.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} + +{% block content %} +
+

Add New Location

+ + {% if error %} +
+ {{ error }} +
+ {% endif %} + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + Cancel +
+
+{% endblock %} diff --git a/templates/location_timeline.html b/templates/location_timeline.html index 907d603..3669035 100644 --- a/templates/location_timeline.html +++ b/templates/location_timeline.html @@ -2,17 +2,62 @@ {% block content %}
-

Location History for Probe {{ probe_id }}

+ {% if probe_id %} +

Location History for Probe {{ probe_id }}

+ {% else %} +

All Locations

+ +

Select a probe to view its location history

+ {% endif %} + + {% if locations and not probe_id %} +
+
+
Locations List
+
+
+ + + + + + + + + + + + {% for location in locations %} + + + + + + + + {% endfor %} + +
NameAddressContactPhoneEmail
{{ location.name }}{{ location.address }}{{ location.contact_name }}{{ location.contact_phone }}{{ location.contact_email }}
+
+
+ {% endif %}
Assignment Timeline
- + {% if chart_data_json %} + + {% else %} +

No location data available

+ {% endif %}
+ {% if location_data %}
Location Details
@@ -40,6 +85,7 @@
+ {% endif %}
{% block scripts %} diff --git a/templates/probe_list.html b/templates/probe_list.html index f491cce..61b46fa 100644 --- a/templates/probe_list.html +++ b/templates/probe_list.html @@ -8,8 +8,14 @@
-
+
All Probes
+
+ + +
{% if probes %} @@ -17,6 +23,7 @@ + @@ -25,23 +32,33 @@ - {% for probe in probes %} - - - - - - - + {% set current_model = none %} + {% for probe in probes|sort(attribute='model_name') %} + {% if probe.model_name != current_model %} + + + + {% set current_model = probe.model_name %} + {% endif %} + + + + + + + + {% endfor %}
Model Serial Number Description Created
{{ probe.serial_number }}{{ probe.description }}{{ probe.created_at.strftime('%Y-%m-%d') }} - {% if probe.retired_at %} - Retired - {% else %} - Active - {% endif %} - - View -
+ {{ probe.model_name }} +
{{ probe.serial_number }}{{ probe.description }}{{ probe.created_at.strftime('%Y-%m-%d') }} + {% if probe.retired_at %} + Retired + {% else %} + Active + {% endif %} + + View +
@@ -52,4 +69,21 @@
+ + {% endblock %} diff --git a/templates/standards_form.html b/templates/standards_form.html new file mode 100644 index 0000000..99b3054 --- /dev/null +++ b/templates/standards_form.html @@ -0,0 +1,66 @@ +{% extends "base.html" %} + +{% block content %} +
+

Add New Standard

+
+
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+
+
Support Information
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+{% endblock %} diff --git a/templates/standards_list.html b/templates/standards_list.html new file mode 100644 index 0000000..120a843 --- /dev/null +++ b/templates/standards_list.html @@ -0,0 +1,51 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

Standards Management

+ Add New Standard +
+ +
+
+
All Standards
+
+
+ {% if standards %} +
+ + + + + + + + + + + + + {% for standard in standards %} + + + + + + + + + {% endfor %} + +
MakeModelDescriptionLast CalibrationNext Calibration DueActions
{{ standard.make }}{{ standard.model }}{{ standard.description }}{{ standard.calibrated_on.strftime('%Y-%m-%d') }}{{ standard.calibration_due.strftime('%Y-%m-%d') }} + View +
+
+ {% else %} +
No standards found. Click "Add New Standard" to create one.
+ {% endif %} +
+
+
+{% endblock %} diff --git a/templates/standards_view.html b/templates/standards_view.html new file mode 100644 index 0000000..b8c4bd4 --- /dev/null +++ b/templates/standards_view.html @@ -0,0 +1,71 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+
+

{{ standard.make }} {{ standard.model }}

+ Standard +
+
+
+
+
+
Standard Details
+
+
Make
+
{{ standard.make }}
+ +
Model
+
{{ standard.model }}
+ +
Description
+
{{ standard.description }}
+ +
Uncertainty
+
{{ standard.uncertainty }}
+
+
+ +
+
Calibration Dates
+
+
Last Calibration
+
{{ standard.calibrated_on.strftime('%Y-%m-%d') }}
+ +
Next Calibration Due
+
{{ standard.calibration_due.strftime('%Y-%m-%d') }}
+
+
+
+ +
+
+
Support Information
+
+
+
+
Contact Name
+
{{ standard.support_name }}
+ +
Email
+
{{ standard.support_email }}
+ +
Phone
+
{{ standard.support_phone }}
+ +
Address
+
{{ standard.support_address }}
+
+
+
+ + +
+
+
+{% endblock %} diff --git a/verify_schema.py b/verify_schema.py new file mode 100644 index 0000000..adc36e3 --- /dev/null +++ b/verify_schema.py @@ -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)