Implement standards management and update calibration workflow
This commit is contained in:
@@ -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
|
||||||
|
|||||||
110
app/models.py
110
app/models.py
@@ -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"""
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
60
app/routes/standards.py
Normal 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)
|
||||||
@@ -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')
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|||||||
43
templates/location_form.html
Normal file
43
templates/location_form.html
Normal 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 %}
|
||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
66
templates/standards_form.html
Normal file
66
templates/standards_form.html
Normal 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 %}
|
||||||
51
templates/standards_list.html
Normal file
51
templates/standards_list.html
Normal 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 %}
|
||||||
71
templates/standards_view.html
Normal file
71
templates/standards_view.html
Normal 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
60
verify_schema.py
Normal 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)
|
||||||
Reference in New Issue
Block a user