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 from app.routes.channels import channels_bp
app.register_blueprint(channels_bp, url_prefix='/channels') 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 return app

View File

@@ -35,6 +35,7 @@ class Probe:
description: str description: str
created_at: datetime created_at: datetime
retired_at: Optional[datetime] = None retired_at: Optional[datetime] = None
model_name: str = '' # Added to match dynamic attribute assignment
@classmethod @classmethod
def get_by_id(cls, probe_id: str): def get_by_id(cls, probe_id: str):
@@ -50,16 +51,33 @@ class Probe:
return cls(**row) return cls(**row)
@classmethod @classmethod
def get_all(cls, limit: int = 100): def get_all(cls, limit: int = 100, active_only: bool = True):
"""List all probes with optional limit""" """List all probes with optional limit and active filter"""
supabase = get_supabase() 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 = [] probes = []
for row in result.data: 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 # Parse datetime strings
row['created_at'] = datetime.fromisoformat(row['created_at']) if row['created_at'] else None 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 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 return probes
@classmethod @classmethod
@@ -217,30 +235,22 @@ class Calibration:
@classmethod @classmethod
def get_all(cls, limit: int = 100): def get_all(cls, limit: int = 100):
"""List all work orders with optional limit""" """List all calibrations with optional limit"""
supabase = get_supabase() supabase = get_supabase()
result = supabase.table('work_orders').select("*").limit(limit).execute() result = supabase.table('calibrations').select("*").limit(limit).execute()
work_orders = [] calibrations = []
for row in result.data if result.data else []: for row in result.data if result.data else []:
try: try:
# Debug print raw data # Parse datetime strings
print(f"Raw work order data: {row}") 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 calibrations.append(cls(**row))
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)
except Exception as e: except Exception as e:
print(f"Error parsing work order data: {e}") print(f"Error parsing calibration data: {e}")
continue continue
return work_orders return calibrations
@classmethod @classmethod
def get_by_work_order(cls, work_order_id: str): def get_by_work_order(cls, work_order_id: str):
@@ -317,7 +327,45 @@ class Standard:
"""List all standards with optional limit""" """List all standards with optional limit"""
supabase = get_supabase() supabase = get_supabase()
result = supabase.table('standards').select("*").limit(limit).execute() 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 @dataclass
class ProbeModel: class ProbeModel:
@@ -384,6 +432,22 @@ class Location:
result = supabase.table('locations').select("*").limit(limit).execute() result = supabase.table('locations').select("*").limit(limit).execute()
return [cls(**row) for row in result.data] if result.data else [] 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 @dataclass
class ProbeLocation: class ProbeLocation:
"""ProbeLocation model representing probe assignments to locations""" """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 from datetime import datetime
import re import re
from typing import Optional
from app.models import Calibration, Channel, WorkOrder, Standard, User from app.models import Calibration, Channel, WorkOrder, Standard, User
from app.supabase_client import get_supabase from app.supabase_client import get_supabase
@@ -19,7 +20,7 @@ def create_calibrations():
return redirect(request.referrer) return redirect(request.referrer)
try: try:
date = datetime.strptime(date_str, '%Y-%m-%d').date() date = datetime.strptime(str(date_str), '%Y-%m-%d').date()
except ValueError: except ValueError:
flash('Invalid date format', 'error') flash('Invalid date format', 'error')
return redirect(request.referrer) return redirect(request.referrer)
@@ -55,10 +56,20 @@ def create_calibrations():
# Process each calibration # Process each calibration
for i in range(len(channel_serials)): for i in range(len(channel_serials)):
# Get channel ID from serial # Get channel ID from serial with detailed error handling
channel = supabase.table('channels').select('id').eq('serial_number', channel_serials[i]).execute() try:
if not channel.data: print(f"Looking up channel with serial: {channel_serials[i]}")
flash(f'Channel not found: {channel_serials[i]}', 'error') 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 continue
calibration_data = { calibration_data = {
@@ -80,10 +91,18 @@ def create_calibrations():
'passed': bool(passed_list[i] if i < len(passed_list) else False) 'passed': bool(passed_list[i] if i < len(passed_list) else False)
} }
# Insert calibration # Insert calibration with error handling
result = supabase.table('calibrations').insert(calibration_data).execute() try:
if not result.data: result = supabase.table('calibrations').insert(calibration_data).execute()
flash(f'Failed to create calibration for {channel_serials[i]}', 'error') 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') flash('Calibrations processed', 'success')
return redirect(url_for('calibrations.index')) return redirect(url_for('calibrations.index'))
@@ -144,7 +163,8 @@ def review_calibration(calibration_id):
return redirect(url_for('calibrations.index')) return redirect(url_for('calibrations.index'))
return render_template('calibration_review.html', return render_template('calibration_review.html',
calibration=calibration.data[0]) calibration=calibration.data[0],
user=session)
@calibrations_bp.route('/new') @calibrations_bp.route('/new')
def new_calibration(): def new_calibration():
@@ -160,21 +180,64 @@ def new_calibration():
# Get all standards # Get all standards
standards = supabase.table('standards').select('id, make, model').execute() 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() 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', return render_template('calibration_form.html',
work_orders=work_orders.data, work_orders=work_orders.data,
standards=standards.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('/') @calibrations_bp.route('/')
def index(): def index():
supabase = get_supabase() supabase = get_supabase()
# Get summary statistics # Get summary statistics
total = supabase.table('calibrations').select('count', count='exact').execute().count total_result = supabase.table('calibrations').select('*').execute()
passed = supabase.table('calibrations').select('count', count='exact').eq('passed', True).execute().count 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 failed = total - passed
# Get recent calibrations (last 10) # Get recent calibrations (last 10)
@@ -191,7 +254,8 @@ def index():
'passed_calibrations': passed, 'passed_calibrations': passed,
'failed_calibrations': failed 'failed_calibrations': failed
}, },
recent_calibrations=recent_calibrations.data) recent_calibrations=recent_calibrations.data,
user=session)
@calibrations_bp.route('/probe/<probe_id>') @calibrations_bp.route('/probe/<probe_id>')
def calibrations_for_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', return render_template('calibration_history.html',
calibrations=calibrations.data, 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') @calibrations_bp.route('/probe/<probe_id>/trends')
def calibration_trends(probe_id): def calibration_trends(probe_id):
@@ -280,4 +345,5 @@ def view_calibration(calibration_id):
return redirect(url_for('calibrations.index')) return redirect(url_for('calibrations.index'))
return render_template('calibration_view.html', 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 flask import Blueprint, render_template, request, session, redirect
from app.models import ProbeLocation from app.models import ProbeLocation, Location
locations_bp = Blueprint('locations', __name__) locations_bp = Blueprint('locations', __name__)
from datetime import datetime from datetime import datetime
import json 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') @locations_bp.route('/probe/<probe_id>/locations')
def probe_location_history(probe_id): def probe_location_history(probe_id):
"""Show timeline of location assignments for a probe""" """Show timeline of location assignments for a probe"""
@@ -35,4 +74,5 @@ def probe_location_history(probe_id):
probe_id=probe_id, probe_id=probe_id,
location_data=location_data, location_data=location_data,
chart_data_json=json.dumps(chart_data), chart_data_json=json.dumps(chart_data),
now=now) now=now,
user=session)

View File

@@ -1,5 +1,5 @@
import re 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.models import Probe, ProbeModel, Channel, Parameter, ProbeLocation
from app.routes.auth import role_required from app.routes.auth import role_required
@@ -8,9 +8,13 @@ probes_bp = Blueprint('probes', __name__)
@probes_bp.route('/') @probes_bp.route('/')
@role_required('review') @role_required('review')
def list_probes(): def list_probes():
"""List all probes""" """List all probes with optional active filter"""
probes = Probe.get_all() active_only = request.args.get('active_only', 'true').lower() == 'true'
return render_template('probe_list.html', probes=probes) 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') @probes_bp.route('/new')
@role_required('review') @role_required('review')
@@ -20,7 +24,8 @@ def new_probe():
parameters = Parameter.get_all() parameters = Parameter.get_all()
return render_template('probe_form.html', return render_template('probe_form.html',
probe_models=probe_models, probe_models=probe_models,
parameters=parameters) parameters=parameters,
user=session)
@probes_bp.route('/', methods=['POST']) @probes_bp.route('/', methods=['POST'])
@role_required('review') @role_required('review')
@@ -77,4 +82,5 @@ def view_probe(probe_id):
return render_template('probe_view.html', return render_template('probe_view.html',
probe=probe, probe=probe,
channels=channels, 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.models import WorkOrder, Customer, User, CalibrationType, Calibration
from app.supabase_client import get_supabase from app.supabase_client import get_supabase
from app.routes.auth import role_required from app.routes.auth import role_required
@@ -12,7 +12,9 @@ def list_work_orders():
work_orders = WorkOrder.get_all() work_orders = WorkOrder.get_all()
if not work_orders: if not work_orders:
flash('No work orders found', 'info') 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') @work_orders_bp.route('/new')
@role_required('review') @role_required('review')
@@ -24,7 +26,8 @@ def new_work_order():
return render_template('work_order_form.html', return render_template('work_order_form.html',
customers=customers, customers=customers,
users=users, users=users,
calibration_types=calibration_types) calibration_types=calibration_types,
user=session)
@work_orders_bp.route('/', methods=['POST']) @work_orders_bp.route('/', methods=['POST'])
@role_required('review') @role_required('review')
@@ -58,7 +61,8 @@ def view_work_order(work_order_id):
calibrations = Calibration.get_by_work_order(work_order_id) calibrations = Calibration.get_by_work_order(work_order_id)
return render_template('work_order_view.html', return render_template('work_order_view.html',
work_order=work_order, work_order=work_order,
calibrations=calibrations) calibrations=calibrations,
user=session)
@work_orders_bp.route('/<work_order_id>/status', methods=['POST']) @work_orders_bp.route('/<work_order_id>/status', methods=['POST'])
@role_required('review') @role_required('review')

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <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 --> <!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
{% block head %}{% endblock %} {% block head %}{% endblock %}
@@ -11,9 +11,25 @@
<body> <body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid"> <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"> <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>
</div> </div>
</nav> </nav>

View File

@@ -1,6 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block head %} {% 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" /> <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> <script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
{% endblock %} {% endblock %}
@@ -9,7 +10,8 @@
<div class="container mt-4"> <div class="container mt-4">
<h2>New Calibration Batch</h2> <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 mb-4">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">Batch Information</h5> <h5 class="card-title">Batch Information</h5>
@@ -40,15 +42,32 @@
<input type="date" class="form-control" id="date" name="date" required> <input type="date" class="form-control" id="date" name="date" required>
</div> </div>
<div class="col-md-6"> <div class="col-md-3">
<label for="calibrated_by" class="form-label">Calibrated By</label> <label for="probe_model" class="form-label">Probe Model</label>
<input type="text" class="form-control" id="calibrated_by" name="calibrated_by" required> <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>
<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>
</div> </div>
<div class="card mb-4"> <div class="card mb-4" id="channelsSection" style="display: none;">
<div class="card-body"> <div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="card-title mb-0">Channels</h5> <h5 class="card-title mb-0">Channels</h5>
@@ -58,56 +77,55 @@
</div> </div>
<div id="channelRows"> <div id="channelRows">
<!-- Channel rows will be added here -->
<div class="channel-row mb-3"> <div class="channel-row mb-3">
<div class="row g-3"> <div class="row g-3">
<div class="col-md-4"> <div class="col-md-3">
<label class="form-label">Channel</label> <label class="form-label">Channel</label>
<select class="form-select select2" name="channel_serial[]" required> <select class="form-select select2" name="channel_serial[]" required>
<option value="">Select Channel</option> <option value="">Select Channel</option>
{% for channel in channels %} {% for channel in channels %}
<option value="{{ channel.serial_number }}">{{ channel.serial_number }}</option> <option value="{{ channel.serial_number }}">{{ channel.serial_number }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="col-md-2"> <div class="col-md-1">
<label class="form-label">Scale</label> <label class="form-label">Scale</label>
<input type="number" step="0.001" class="form-control" name="scale[]" required> <input type="number" step="0.001" class="form-control" name="scale[]" required>
</div> </div>
<div class="col-md-2"> <div class="col-md-1">
<label class="form-label">Offset</label> <label class="form-label">Offset</label>
<input type="number" step="0.001" class="form-control" name="offset[]" required> <input type="number" step="0.001" class="form-control" name="offset[]" required>
</div> </div>
<div class="col-md-2"> <div class="col-md-1">
<label class="form-label">Deviation High</label> <label class="form-label">Dev Low</label>
<input type="number" step="0.001" class="form-control" name="deviation_high[]" required> <input type="number" step="0.001" class="form-control" name="deviation_low[]" required>
</div> </div>
<div class="col-md-2"> <div class="col-md-1">
<label class="form-label">Deviation Mid</label> <label class="form-label">Dev Mid</label>
<input type="number" step="0.001" class="form-control" name="deviation_mid[]" required> <input type="number" step="0.001" class="form-control" name="deviation_mid[]" required>
</div> </div>
<div class="col-md-2"> <div class="col-md-1">
<label class="form-label">Deviation Low</label> <label class="form-label">Dev High</label>
<input type="number" step="0.001" class="form-control" name="deviation_low[]" required> <input type="number" step="0.001" class="form-control" name="deviation_high[]" required>
</div> </div>
<div class="col-md-2"> <div class="col-md-1">
<label class="form-label">Set High</label> <label class="form-label">Set Low</label>
<input type="number" step="0.001" class="form-control" name="set_high[]" required> <input type="number" step="0.001" class="form-control" name="set_low[]" required>
</div> </div>
<div class="col-md-2"> <div class="col-md-1">
<label class="form-label">Set Mid</label> <label class="form-label">Set Mid</label>
<input type="number" step="0.001" class="form-control" name="set_mid[]" required> <input type="number" step="0.001" class="form-control" name="set_mid[]" required>
</div> </div>
<div class="col-md-2"> <div class="col-md-1">
<label class="form-label">Set Low</label> <label class="form-label">Set High</label>
<input type="number" step="0.001" class="form-control" name="set_low[]" required> <input type="number" step="0.001" class="form-control" name="set_high[]" required>
</div> </div>
<div class="col-md-2"> <div class="col-md-1">
<div class="form-check form-switch mt-4"> <div class="form-check form-switch mt-4">
<input class="form-check-input" type="checkbox" name="passed[]" checked> <input class="form-check-input" type="checkbox" name="passed[]" checked>
<label class="form-check-label">Passed</label> <label class="form-check-label">Passed</label>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@@ -115,63 +133,152 @@
</div> </div>
<div class="text-end"> <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> </div>
</form> </form>
</div> </div>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { $(function() {
// Initialize Select2 // Initialize Select2 and confirm button
$('.select2').select2(); $('.select2').select2();
// Add channel row console.log('Initializing calibration form...');
$('#addChannel').click(function() {
// 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 = ` const newRow = `
<div class="channel-row mb-3"> <div class="channel-row mb-3">
<div class="row g-3"> <div class="row g-3">
<div class="col-md-4"> <div class="col-md-3">
<label class="form-label">Channel</label> <label class="form-label">Channel</label>
<select class="form-select select2" name="channel_serial[]" required> <select class="form-select select2" name="channel_serial[]" required>
<option value="">Select Channel</option> <option value="">Select Channel</option>
{% for channel in channels %} ${channelOptions}
<option value="{{ channel.serial_number }}">{{ channel.serial_number }}</option>
{% endfor %}
</select> </select>
</div> </div>
<div class="col-md-2"> <div class="col-md-1">
<label class="form-label">Scale</label> <label class="form-label">Scale</label>
<input type="number" step="0.001" class="form-control" name="scale[]" required> <input type="number" step="0.001" class="form-control" name="scale[]" required>
</div> </div>
<div class="col-md-2"> <div class="col-md-1">
<label class="form-label">Offset</label> <label class="form-label">Offset</label>
<input type="number" step="0.001" class="form-control" name="offset[]" required> <input type="number" step="0.001" class="form-control" name="offset[]" required>
</div> </div>
<div class="col-md-2"> <div class="col-md-1">
<label class="form-label">Deviation High</label> <label class="form-label">Dev Low</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> <input type="number" step="0.001" class="form-control" name="deviation_low[]" required>
</div> </div>
<div class="col-md-2"> <div class="col-md-1">
<label class="form-label">Set High</label> <label class="form-label">Dev Mid</label>
<input type="number" step="0.001" class="form-control" name="set_high[]" required> <input type="number" step="0.001" class="form-control" name="deviation_mid[]" required>
</div> </div>
<div class="col-md-2"> <div class="col-md-1">
<label class="form-label">Set Mid</label> <label class="form-label">Dev High</label>
<input type="number" step="0.001" class="form-control" name="set_mid[]" required> <input type="number" step="0.001" class="form-control" name="deviation_high[]" required>
</div> </div>
<div class="col-md-2"> <div class="col-md-1">
<label class="form-label">Set Low</label> <label class="form-label">Set Low</label>
<input type="number" step="0.001" class="form-control" name="set_low[]" required> <input type="number" step="0.001" class="form-control" name="set_low[]" required>
</div> </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"> <div class="form-check form-switch mt-4">
<input class="form-check-input" type="checkbox" name="passed[]" checked> <input class="form-check-input" type="checkbox" name="passed[]" checked>
<label class="form-check-label">Passed</label> <label class="form-check-label">Passed</label>
@@ -179,9 +286,59 @@ document.addEventListener('DOMContentLoaded', function() {
</div> </div>
</div> </div>
</div>`; </div>`;
$('#channelRows').append(newRow); $('#channelRows').append(newRow);
$('.select2').select2(); $('.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> </script>
{% endblock %} {% endblock %}

View File

@@ -1,24 +1,6 @@
<!DOCTYPE html> {% extends "base.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>
{% block content %}
<div class="container mt-4"> <div class="container mt-4">
<div class="row"> <div class="row">
<div class="col-md-3"> <div class="col-md-3">
@@ -31,6 +13,7 @@
<a href="/calibrations" class="list-group-item list-group-item-action">Calibrations</a> <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="/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="/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> </div>
</div> </div>
@@ -40,7 +23,7 @@
Dashboard Dashboard
</div> </div>
<div class="card-body"> <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> <p class="card-text">You are logged in as {{ user.user_name }}.</p>
{% if user.can_calibrate %} {% if user.can_calibrate %}
<span class="badge bg-success">Calibrator</span> <span class="badge bg-success">Calibrator</span>
@@ -53,7 +36,4 @@
</div> </div>
</div> </div>
</div> </div>
{% endblock %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

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 %} {% block content %}
<div class="container mt-4"> <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 mt-4">
<div class="card-header"> <div class="card-header">
<h5>Assignment Timeline</h5> <h5>Assignment Timeline</h5>
</div> </div>
<div class="card-body"> <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>
</div> </div>
{% if location_data %}
<div class="card mt-4"> <div class="card mt-4">
<div class="card-header"> <div class="card-header">
<h5>Location Details</h5> <h5>Location Details</h5>
@@ -40,6 +85,7 @@
</table> </table>
</div> </div>
</div> </div>
{% endif %}
</div> </div>
{% block scripts %} {% block scripts %}

View File

@@ -8,8 +8,14 @@
</div> </div>
<div class="card"> <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> <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>
<div class="card-body"> <div class="card-body">
{% if probes %} {% if probes %}
@@ -17,6 +23,7 @@
<table class="table table-striped table-hover"> <table class="table table-striped table-hover">
<thead> <thead>
<tr> <tr>
<th>Model</th>
<th>Serial Number</th> <th>Serial Number</th>
<th>Description</th> <th>Description</th>
<th>Created</th> <th>Created</th>
@@ -25,23 +32,33 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for probe in probes %} {% set current_model = none %}
<tr> {% for probe in probes|sort(attribute='model_name') %}
<td>{{ probe.serial_number }}</td> {% if probe.model_name != current_model %}
<td>{{ probe.description }}</td> <tr class="table-info">
<td>{{ probe.created_at.strftime('%Y-%m-%d') }}</td> <td colspan="6">
<td> <strong>{{ probe.model_name }}</strong>
{% if probe.retired_at %} </td>
<span class="badge bg-secondary">Retired</span> </tr>
{% else %} {% set current_model = probe.model_name %}
<span class="badge bg-success">Active</span> {% endif %}
{% endif %} <tr class="probe-row" data-active="{{ 'true' if not probe.retired_at else 'false' }}">
</td> <td></td> <!-- Empty cell under model header -->
<td> <td>{{ probe.serial_number }}</td>
<a href="{{ url_for('probes.view_probe', probe_id=probe.id) }}" <td>{{ probe.description }}</td>
class="btn btn-sm btn-outline-primary">View</a> <td>{{ probe.created_at.strftime('%Y-%m-%d') }}</td>
</td> <td>
</tr> {% 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 %} {% endfor %}
</tbody> </tbody>
</table> </table>
@@ -52,4 +69,21 @@
</div> </div>
</div> </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 %} {% 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)