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
|
||||
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
|
||||
|
||||
110
app/models.py
110
app/models.py
@@ -35,6 +35,7 @@ class Probe:
|
||||
description: str
|
||||
created_at: datetime
|
||||
retired_at: Optional[datetime] = None
|
||||
model_name: str = '' # Added to match dynamic attribute assignment
|
||||
|
||||
@classmethod
|
||||
def get_by_id(cls, probe_id: str):
|
||||
@@ -50,16 +51,33 @@ class Probe:
|
||||
return cls(**row)
|
||||
|
||||
@classmethod
|
||||
def get_all(cls, limit: int = 100):
|
||||
"""List all probes with optional limit"""
|
||||
def get_all(cls, limit: int = 100, active_only: bool = True):
|
||||
"""List all probes with optional limit and active filter"""
|
||||
supabase = get_supabase()
|
||||
result = supabase.table('probes').select("*").limit(limit).execute()
|
||||
query = supabase.table('probes') \
|
||||
.select('*, probe_models(model_name)') \
|
||||
.limit(limit)
|
||||
|
||||
if active_only:
|
||||
query = query.is_('retired_at', 'null')
|
||||
|
||||
result = query.execute()
|
||||
|
||||
probes = []
|
||||
for row in result.data:
|
||||
# Extract model_name before creating Probe instance
|
||||
model_name = row['probe_models']['model_name'] if row.get('probe_models') else 'Unknown'
|
||||
# Remove probe_models from row to avoid dataclass init error
|
||||
if 'probe_models' in row:
|
||||
del row['probe_models']
|
||||
# Parse datetime strings
|
||||
row['created_at'] = datetime.fromisoformat(row['created_at']) if row['created_at'] else None
|
||||
row['retired_at'] = datetime.fromisoformat(row['retired_at']) if row['retired_at'] else None
|
||||
probes.append(cls(**row))
|
||||
# Create probe instance
|
||||
probe = cls(**row)
|
||||
# Add model_name as dynamic attribute for template use
|
||||
probe.model_name = model_name
|
||||
probes.append(probe)
|
||||
return probes
|
||||
|
||||
@classmethod
|
||||
@@ -217,30 +235,22 @@ class Calibration:
|
||||
|
||||
@classmethod
|
||||
def get_all(cls, limit: int = 100):
|
||||
"""List all work orders with optional limit"""
|
||||
"""List all calibrations with optional limit"""
|
||||
supabase = get_supabase()
|
||||
result = supabase.table('work_orders').select("*").limit(limit).execute()
|
||||
work_orders = []
|
||||
result = supabase.table('calibrations').select("*").limit(limit).execute()
|
||||
calibrations = []
|
||||
for row in result.data if result.data else []:
|
||||
try:
|
||||
# Debug print raw data
|
||||
print(f"Raw work order data: {row}")
|
||||
# Parse datetime strings
|
||||
for date_field in ['std_cal_date', 'std_cal_due', 'date']:
|
||||
if row.get(date_field):
|
||||
row[date_field] = datetime.fromisoformat(row[date_field])
|
||||
|
||||
# Ensure due_date is properly parsed
|
||||
if 'due_date' in row and row['due_date']:
|
||||
if isinstance(row['due_date'], str):
|
||||
row['due_date'] = datetime.fromisoformat(row['due_date'])
|
||||
elif not isinstance(row['due_date'], datetime):
|
||||
row['due_date'] = None
|
||||
|
||||
# Create instance and verify date type
|
||||
wo = cls(**row)
|
||||
print(f"WorkOrder instance due_date type: {type(wo.due_date)}")
|
||||
work_orders.append(wo)
|
||||
calibrations.append(cls(**row))
|
||||
except Exception as e:
|
||||
print(f"Error parsing work order data: {e}")
|
||||
print(f"Error parsing calibration data: {e}")
|
||||
continue
|
||||
return work_orders
|
||||
return calibrations
|
||||
|
||||
@classmethod
|
||||
def get_by_work_order(cls, work_order_id: str):
|
||||
@@ -317,7 +327,45 @@ class Standard:
|
||||
"""List all standards with optional limit"""
|
||||
supabase = get_supabase()
|
||||
result = supabase.table('standards').select("*").limit(limit).execute()
|
||||
return [cls(**row) for row in result.data] if result.data else []
|
||||
standards = []
|
||||
for row in result.data if result.data else []:
|
||||
# Parse datetime strings
|
||||
for date_field in ['calibrated_on', 'calibration_due']:
|
||||
if row.get(date_field):
|
||||
row[date_field] = datetime.fromisoformat(row[date_field])
|
||||
standards.append(cls(**row))
|
||||
return standards
|
||||
|
||||
@classmethod
|
||||
def create(cls, make: str, model: str, description: str, uncertainty: str,
|
||||
calibrated_on: str, calibration_due: str, support_name: str,
|
||||
support_email: str, support_phone: str, support_address: str):
|
||||
"""Create a new standard"""
|
||||
supabase = get_supabase()
|
||||
# Convert date strings to ISO format
|
||||
calibrated_on_iso = datetime.fromisoformat(calibrated_on).isoformat()
|
||||
calibration_due_iso = datetime.fromisoformat(calibration_due).isoformat()
|
||||
|
||||
result = supabase.table('standards').insert({
|
||||
'make': make,
|
||||
'model': model,
|
||||
'description': description,
|
||||
'uncertainty': uncertainty,
|
||||
'calibrated_on': calibrated_on_iso,
|
||||
'calibration_due': calibration_due_iso,
|
||||
'support_name': support_name,
|
||||
'support_email': support_email,
|
||||
'support_phone': support_phone,
|
||||
'support_address': support_address
|
||||
}).execute()
|
||||
if not result.data:
|
||||
raise Exception('Failed to create standard')
|
||||
|
||||
# Parse dates in the returned data
|
||||
row = result.data[0]
|
||||
row['calibrated_on'] = datetime.fromisoformat(row['calibrated_on']) if row['calibrated_on'] else None
|
||||
row['calibration_due'] = datetime.fromisoformat(row['calibration_due']) if row['calibration_due'] else None
|
||||
return cls(**row)
|
||||
|
||||
@dataclass
|
||||
class ProbeModel:
|
||||
@@ -384,6 +432,22 @@ class Location:
|
||||
result = supabase.table('locations').select("*").limit(limit).execute()
|
||||
return [cls(**row) for row in result.data] if result.data else []
|
||||
|
||||
@classmethod
|
||||
def create(cls, name: str, address: str, contact_name: str,
|
||||
contact_email: str, contact_phone: str):
|
||||
"""Create a new location"""
|
||||
supabase = get_supabase()
|
||||
result = supabase.table('locations').insert({
|
||||
'name': name,
|
||||
'address': address,
|
||||
'contact_name': contact_name,
|
||||
'contact_email': contact_email,
|
||||
'contact_phone': contact_phone
|
||||
}).execute()
|
||||
if not result.data:
|
||||
raise Exception('Failed to create location')
|
||||
return cls(**result.data[0])
|
||||
|
||||
@dataclass
|
||||
class ProbeLocation:
|
||||
"""ProbeLocation model representing probe assignments to locations"""
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from flask import Blueprint, request, redirect, url_for, flash, render_template
|
||||
from flask import Blueprint, request, redirect, url_for, flash, render_template, session, jsonify
|
||||
from datetime import datetime
|
||||
import re
|
||||
from typing import Optional
|
||||
from app.models import Calibration, Channel, WorkOrder, Standard, User
|
||||
from app.supabase_client import get_supabase
|
||||
|
||||
@@ -19,7 +20,7 @@ def create_calibrations():
|
||||
return redirect(request.referrer)
|
||||
|
||||
try:
|
||||
date = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
date = datetime.strptime(str(date_str), '%Y-%m-%d').date()
|
||||
except ValueError:
|
||||
flash('Invalid date format', 'error')
|
||||
return redirect(request.referrer)
|
||||
@@ -55,10 +56,20 @@ def create_calibrations():
|
||||
|
||||
# Process each calibration
|
||||
for i in range(len(channel_serials)):
|
||||
# Get channel ID from serial
|
||||
channel = supabase.table('channels').select('id').eq('serial_number', channel_serials[i]).execute()
|
||||
if not channel.data:
|
||||
flash(f'Channel not found: {channel_serials[i]}', 'error')
|
||||
# Get channel ID from serial with detailed error handling
|
||||
try:
|
||||
print(f"Looking up channel with serial: {channel_serials[i]}")
|
||||
channel = supabase.table('channels').select('id').eq('serial_number', channel_serials[i]).execute()
|
||||
if not channel.data:
|
||||
error_msg = f'Channel not found: {channel_serials[i]}'
|
||||
flash(error_msg, 'error')
|
||||
print(error_msg)
|
||||
continue
|
||||
print(f"Found channel ID: {channel.data[0]['id']}")
|
||||
except Exception as e:
|
||||
error_msg = f'Error looking up channel {channel_serials[i]}: {str(e)}'
|
||||
flash(error_msg, 'error')
|
||||
print(error_msg)
|
||||
continue
|
||||
|
||||
calibration_data = {
|
||||
@@ -80,10 +91,18 @@ def create_calibrations():
|
||||
'passed': bool(passed_list[i] if i < len(passed_list) else False)
|
||||
}
|
||||
|
||||
# Insert calibration
|
||||
result = supabase.table('calibrations').insert(calibration_data).execute()
|
||||
if not result.data:
|
||||
flash(f'Failed to create calibration for {channel_serials[i]}', 'error')
|
||||
# Insert calibration with error handling
|
||||
try:
|
||||
result = supabase.table('calibrations').insert(calibration_data).execute()
|
||||
if not result.data:
|
||||
flash(f'Failed to create calibration for {channel_serials[i]}', 'error')
|
||||
print(f"Supabase insert failed for channel {channel_serials[i]}: {result}")
|
||||
else:
|
||||
print(f"Successfully created calibration for channel {channel_serials[i]}: {result.data}")
|
||||
except Exception as e:
|
||||
flash(f'Error creating calibration for {channel_serials[i]}: {str(e)}', 'error')
|
||||
print(f"Exception creating calibration for {channel_serials[i]}: {str(e)}")
|
||||
continue
|
||||
|
||||
flash('Calibrations processed', 'success')
|
||||
return redirect(url_for('calibrations.index'))
|
||||
@@ -144,7 +163,8 @@ def review_calibration(calibration_id):
|
||||
return redirect(url_for('calibrations.index'))
|
||||
|
||||
return render_template('calibration_review.html',
|
||||
calibration=calibration.data[0])
|
||||
calibration=calibration.data[0],
|
||||
user=session)
|
||||
|
||||
@calibrations_bp.route('/new')
|
||||
def new_calibration():
|
||||
@@ -160,21 +180,64 @@ def new_calibration():
|
||||
# Get all standards
|
||||
standards = supabase.table('standards').select('id, make, model').execute()
|
||||
|
||||
# Get all channels
|
||||
# Get all channels (just serial numbers for now)
|
||||
channels = supabase.table('channels').select('serial_number').execute()
|
||||
|
||||
# Get all probe models (using exact column name)
|
||||
probe_models = supabase.table('probe_models').select('id, model_name').execute()
|
||||
|
||||
# Get all parameters (using correct column name)
|
||||
parameters = supabase.table('parameters').select('id, parameter_name').execute()
|
||||
|
||||
return render_template('calibration_form.html',
|
||||
work_orders=work_orders.data,
|
||||
standards=standards.data,
|
||||
channels=channels.data)
|
||||
channels=channels.data,
|
||||
probe_models=probe_models.data,
|
||||
parameters=parameters.data,
|
||||
user=session)
|
||||
|
||||
@calibrations_bp.route('/filtered-channels')
|
||||
def get_filtered_channels():
|
||||
supabase = get_supabase()
|
||||
|
||||
probe_model_id = request.args.get('probe_model')
|
||||
parameter_id = request.args.get('parameter')
|
||||
|
||||
if not probe_model_id or not parameter_id:
|
||||
return jsonify([])
|
||||
|
||||
# Get channels filtered by both probe model and parameter
|
||||
channels = supabase.table('channels').select('''
|
||||
serial_number,
|
||||
probes:probe_id(model_id),
|
||||
parameter_id
|
||||
''').eq('parameter_id', parameter_id).execute()
|
||||
|
||||
# Filter channels by probe model (client-side fallback)
|
||||
filtered_channels = []
|
||||
for c in channels.data:
|
||||
try:
|
||||
if c['probes']['model_id'] == probe_model_id:
|
||||
filtered_channels.append({
|
||||
'serial_number': c['serial_number']
|
||||
})
|
||||
except (KeyError, TypeError):
|
||||
continue
|
||||
|
||||
print(f"Filtered channels for model {probe_model_id} and parameter {parameter_id}: {filtered_channels}")
|
||||
|
||||
return jsonify(filtered_channels)
|
||||
|
||||
@calibrations_bp.route('/')
|
||||
def index():
|
||||
supabase = get_supabase()
|
||||
|
||||
# Get summary statistics
|
||||
total = supabase.table('calibrations').select('count', count='exact').execute().count
|
||||
passed = supabase.table('calibrations').select('count', count='exact').eq('passed', True).execute().count
|
||||
total_result = supabase.table('calibrations').select('*').execute()
|
||||
passed_result = supabase.table('calibrations').select('*').eq('passed', True).execute()
|
||||
total = len(total_result.data) if total_result.data else 0
|
||||
passed = len(passed_result.data) if passed_result.data else 0
|
||||
failed = total - passed
|
||||
|
||||
# Get recent calibrations (last 10)
|
||||
@@ -191,7 +254,8 @@ def index():
|
||||
'passed_calibrations': passed,
|
||||
'failed_calibrations': failed
|
||||
},
|
||||
recent_calibrations=recent_calibrations.data)
|
||||
recent_calibrations=recent_calibrations.data,
|
||||
user=session)
|
||||
|
||||
@calibrations_bp.route('/probe/<probe_id>')
|
||||
def calibrations_for_probe(probe_id):
|
||||
@@ -216,7 +280,8 @@ def calibrations_for_probe(probe_id):
|
||||
|
||||
return render_template('calibration_history.html',
|
||||
calibrations=calibrations.data,
|
||||
probe=probe.data[0] if probe.data else None)
|
||||
probe=probe.data[0] if probe.data else None,
|
||||
user=session)
|
||||
|
||||
@calibrations_bp.route('/probe/<probe_id>/trends')
|
||||
def calibration_trends(probe_id):
|
||||
@@ -280,4 +345,5 @@ def view_calibration(calibration_id):
|
||||
return redirect(url_for('calibrations.index'))
|
||||
|
||||
return render_template('calibration_view.html',
|
||||
calibration=calibration.data[0])
|
||||
calibration=calibration.data[0],
|
||||
user=session)
|
||||
|
||||
@@ -1,11 +1,50 @@
|
||||
from flask import Blueprint, render_template
|
||||
from app.models import ProbeLocation
|
||||
from flask import Blueprint, render_template, request, session, redirect
|
||||
from app.models import ProbeLocation, Location
|
||||
|
||||
locations_bp = Blueprint('locations', __name__)
|
||||
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
@locations_bp.route('/')
|
||||
def locations_index():
|
||||
"""Show all locations"""
|
||||
locations = Location.get_all()
|
||||
return render_template('location_timeline.html',
|
||||
locations=locations,
|
||||
user=session)
|
||||
|
||||
@locations_bp.route('/new', methods=['GET', 'POST'])
|
||||
def new_location():
|
||||
"""Create a new location"""
|
||||
if request.method == 'POST':
|
||||
name = request.form.get('name', '').strip()
|
||||
address = request.form.get('address', '').strip()
|
||||
contact_name = request.form.get('contact_name', '').strip()
|
||||
contact_email = request.form.get('contact_email', '').strip()
|
||||
contact_phone = request.form.get('contact_phone', '').strip()
|
||||
|
||||
if not name:
|
||||
return render_template('location_form.html',
|
||||
error='Name is required',
|
||||
user=session)
|
||||
|
||||
try:
|
||||
location = Location.create(
|
||||
name=name,
|
||||
address=address,
|
||||
contact_name=contact_name,
|
||||
contact_email=contact_email,
|
||||
contact_phone=contact_phone
|
||||
)
|
||||
return redirect('/locations/')
|
||||
except Exception as e:
|
||||
return render_template('location_form.html',
|
||||
error=str(e),
|
||||
user=session)
|
||||
|
||||
return render_template('location_form.html', user=session)
|
||||
|
||||
@locations_bp.route('/probe/<probe_id>/locations')
|
||||
def probe_location_history(probe_id):
|
||||
"""Show timeline of location assignments for a probe"""
|
||||
@@ -35,4 +74,5 @@ def probe_location_history(probe_id):
|
||||
probe_id=probe_id,
|
||||
location_data=location_data,
|
||||
chart_data_json=json.dumps(chart_data),
|
||||
now=now)
|
||||
now=now,
|
||||
user=session)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import re
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, session
|
||||
from app.models import Probe, ProbeModel, Channel, Parameter, ProbeLocation
|
||||
from app.routes.auth import role_required
|
||||
|
||||
@@ -8,9 +8,13 @@ probes_bp = Blueprint('probes', __name__)
|
||||
@probes_bp.route('/')
|
||||
@role_required('review')
|
||||
def list_probes():
|
||||
"""List all probes"""
|
||||
probes = Probe.get_all()
|
||||
return render_template('probe_list.html', probes=probes)
|
||||
"""List all probes with optional active filter"""
|
||||
active_only = request.args.get('active_only', 'true').lower() == 'true'
|
||||
probes = Probe.get_all(active_only=active_only)
|
||||
return render_template('probe_list.html',
|
||||
probes=probes,
|
||||
active_only=active_only,
|
||||
user=session)
|
||||
|
||||
@probes_bp.route('/new')
|
||||
@role_required('review')
|
||||
@@ -20,7 +24,8 @@ def new_probe():
|
||||
parameters = Parameter.get_all()
|
||||
return render_template('probe_form.html',
|
||||
probe_models=probe_models,
|
||||
parameters=parameters)
|
||||
parameters=parameters,
|
||||
user=session)
|
||||
|
||||
@probes_bp.route('/', methods=['POST'])
|
||||
@role_required('review')
|
||||
@@ -77,4 +82,5 @@ def view_probe(probe_id):
|
||||
return render_template('probe_view.html',
|
||||
probe=probe,
|
||||
channels=channels,
|
||||
locations=locations)
|
||||
locations=locations,
|
||||
user=session)
|
||||
|
||||
60
app/routes/standards.py
Normal file
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.supabase_client import get_supabase
|
||||
from app.routes.auth import role_required
|
||||
@@ -12,7 +12,9 @@ def list_work_orders():
|
||||
work_orders = WorkOrder.get_all()
|
||||
if not work_orders:
|
||||
flash('No work orders found', 'info')
|
||||
return render_template('work_order_list.html', work_orders=work_orders)
|
||||
return render_template('work_order_list.html',
|
||||
work_orders=work_orders,
|
||||
user=session)
|
||||
|
||||
@work_orders_bp.route('/new')
|
||||
@role_required('review')
|
||||
@@ -24,7 +26,8 @@ def new_work_order():
|
||||
return render_template('work_order_form.html',
|
||||
customers=customers,
|
||||
users=users,
|
||||
calibration_types=calibration_types)
|
||||
calibration_types=calibration_types,
|
||||
user=session)
|
||||
|
||||
@work_orders_bp.route('/', methods=['POST'])
|
||||
@role_required('review')
|
||||
@@ -58,7 +61,8 @@ def view_work_order(work_order_id):
|
||||
calibrations = Calibration.get_by_work_order(work_order_id)
|
||||
return render_template('work_order_view.html',
|
||||
work_order=work_order,
|
||||
calibrations=calibrations)
|
||||
calibrations=calibrations,
|
||||
user=session)
|
||||
|
||||
@work_orders_bp.route('/<work_order_id>/status', methods=['POST'])
|
||||
@role_required('review')
|
||||
|
||||
@@ -13,8 +13,18 @@
|
||||
|
||||
## Database Schema
|
||||
### Tables
|
||||
0. **calibration_audit**
|
||||
- id (bigserial, PK)
|
||||
- calibration_id (uuid, FK → calibrations.id)
|
||||
- user_id (uuid, FK → users.id)
|
||||
- action (text)
|
||||
- old_values (jsonb)
|
||||
- new_values (jsonb)
|
||||
- timestamp (timestamptz)
|
||||
- ip_address (text)
|
||||
|
||||
1. **probes**
|
||||
- id (uuid, PK)
|
||||
- id (uuid, PK, DEFAULT gen_random_uuid())
|
||||
- model_id (uuid, FK → probe_models.id)
|
||||
- serial_number (text)
|
||||
- description (text)
|
||||
@@ -22,14 +32,14 @@
|
||||
- retired_at (timestamp)
|
||||
|
||||
2. **channels**
|
||||
- id (uuid, PK)
|
||||
- id (uuid, PK, DEFAULT gen_random_uuid())
|
||||
- probe_id (uuid, FK → probes.id)
|
||||
- serial_number (varchar(16), regex: [0-9A-F]{16})
|
||||
- parameter_id (uuid, FK → parameters.id)
|
||||
- created_at (timestamp)
|
||||
|
||||
3. **calibrations**
|
||||
- id (uuid, PK)
|
||||
- id (uuid, PK, DEFAULT gen_random_uuid())
|
||||
- channel_id (uuid, FK → channels.id)
|
||||
- work_order_id (uuid, FK → work_orders.id)
|
||||
- calibrated_by (uuid, FK → users.id)
|
||||
@@ -49,7 +59,7 @@
|
||||
- passed (boolean)
|
||||
|
||||
4. **locations**
|
||||
- id (uuid, PK)
|
||||
- id (uuid, PK, DEFAULT gen_random_uuid())
|
||||
- name (text)
|
||||
- address (text)
|
||||
- contact_name (text)
|
||||
@@ -57,19 +67,19 @@
|
||||
- contact_phone (text)
|
||||
|
||||
5. **probe_locations**
|
||||
- id (uuid, PK)
|
||||
- id (uuid, PK, DEFAULT gen_random_uuid())
|
||||
- probe_id (uuid, FK → probes.id)
|
||||
- location_id (uuid, FK → locations.id)
|
||||
- start_date (date)
|
||||
- end_date (date)
|
||||
|
||||
6. **probe_models**
|
||||
- id (uuid, PK)
|
||||
- id (uuid, PK, DEFAULT gen_random_uuid())
|
||||
- model_name (text, unique)
|
||||
- specifications (jsonb)
|
||||
|
||||
7. **work_orders**
|
||||
- id (uuid, PK)
|
||||
- id (uuid, PK, DEFAULT gen_random_uuid())
|
||||
- order_number (text, unique)
|
||||
- customer_id (uuid, FK → customers.id)
|
||||
- assigned_to (uuid, FK → users.id)
|
||||
@@ -79,7 +89,7 @@
|
||||
- cal_type (uuid, FK → calibration_types.id)
|
||||
|
||||
8. **users**
|
||||
- id (uuid, PK)
|
||||
- id (uuid, PK, DEFAULT gen_random_uuid())
|
||||
- name (text)
|
||||
- email (text, unique)
|
||||
- can_calibrate (boolean)
|
||||
@@ -87,7 +97,7 @@
|
||||
- signature_image (text)
|
||||
|
||||
10. **standards**
|
||||
- id (uuid, PK)
|
||||
- id (uuid, PK, DEFAULT gen_random_uuid())
|
||||
- make (text)
|
||||
- model (text)
|
||||
- description (text)
|
||||
@@ -100,11 +110,11 @@
|
||||
- support_address (text)
|
||||
|
||||
11. **calibration_types**
|
||||
- id (uuid, PK)
|
||||
- id (uuid, PK, DEFAULT gen_random_uuid())
|
||||
- type_name (text)
|
||||
|
||||
12. **parameters**
|
||||
- id (uuid, PK)
|
||||
- id (uuid, PK, DEFAULT gen_random_uuid())
|
||||
- parameter_name (text)
|
||||
|
||||
### Relationships
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - SmartScan Probe Track</title>
|
||||
<title>Login - CIMTechniques Probe Tracker</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card shadow">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h4 class="mb-0">SmartScan Login</h4>
|
||||
<h4 class="mb-0">CIMTechniques Login</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if error %}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}SmartScan Probe Track{% endblock %}</title>
|
||||
<title>{% block title %}CIMTechniques Probe Tracker{% endblock %}</title>
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
{% block head %}{% endblock %}
|
||||
@@ -11,9 +11,25 @@
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/">SmartScan Probe Track</a>
|
||||
<a class="navbar-brand" href="/">CIMTechniques Probe Tracker</a>
|
||||
<div class="navbar-nav">
|
||||
<a class="nav-link" href="/probes/">Probes</a>
|
||||
{% if user %}
|
||||
<a class="nav-link" href="/probes">Probes</a>
|
||||
<a class="nav-link" href="/calibrations">Calibrations</a>
|
||||
<a class="nav-link" href="/work_orders">Work Orders</a>
|
||||
<a class="nav-link" href="/locations">Locations</a>
|
||||
<a class="nav-link" href="{{ url_for('standards.list_standards') }}">Standards</a>
|
||||
<span class="nav-link">
|
||||
{% if user.user_name %}
|
||||
{{ user.user_name }}
|
||||
{% else %}
|
||||
{{ user.get('user_name', '') }}
|
||||
{% endif %}
|
||||
</span>
|
||||
<a class="nav-link" href="/auth/logout">Logout</a>
|
||||
{% else %}
|
||||
<a class="nav-link" href="/auth/login">Login</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block head %}
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
{% endblock %}
|
||||
@@ -9,7 +10,8 @@
|
||||
<div class="container mt-4">
|
||||
<h2>New Calibration Batch</h2>
|
||||
|
||||
<form method="POST" action="{{ url_for('calibrations.create_calibrations') }}">
|
||||
<form method="POST" action="{{ url_for('calibrations.create_calibrations') }}" onsubmit="return validateForm()">
|
||||
<input type="hidden" name="calibrated_by" value="{{ user.id }}">
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Batch Information</h5>
|
||||
@@ -40,15 +42,32 @@
|
||||
<input type="date" class="form-control" id="date" name="date" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="calibrated_by" class="form-label">Calibrated By</label>
|
||||
<input type="text" class="form-control" id="calibrated_by" name="calibrated_by" required>
|
||||
<div class="col-md-3">
|
||||
<label for="probe_model" class="form-label">Probe Model</label>
|
||||
<select class="form-select select2" id="probe_model" name="probe_model" required>
|
||||
<option value="">Select Model</option>
|
||||
{% for model in probe_models %}
|
||||
<option value="{{ model.id }}">{{ model.model_name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label for="parameter" class="form-label">Parameter</label>
|
||||
<select class="form-select select2" id="parameter" name="parameter" required>
|
||||
<option value="">Select Parameter</option>
|
||||
{% for param in parameters %}
|
||||
<option value="{{ param.id }}">{{ param.parameter_name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-end mt-3">
|
||||
<button type="button" id="confirmBatch" class="btn btn-success">Confirm Batch Info</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card mb-4" id="channelsSection" style="display: none;">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="card-title mb-0">Channels</h5>
|
||||
@@ -58,56 +77,55 @@
|
||||
</div>
|
||||
|
||||
<div id="channelRows">
|
||||
<!-- Channel rows will be added here -->
|
||||
<div class="channel-row mb-3">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Channel</label>
|
||||
<select class="form-select select2" name="channel_serial[]" required>
|
||||
<option value="">Select Channel</option>
|
||||
{% for channel in channels %}
|
||||
<option value="{{ channel.serial_number }}">{{ channel.serial_number }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Scale</label>
|
||||
<input type="number" step="0.001" class="form-control" name="scale[]" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Offset</label>
|
||||
<input type="number" step="0.001" class="form-control" name="offset[]" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Deviation High</label>
|
||||
<input type="number" step="0.001" class="form-control" name="deviation_high[]" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Deviation Mid</label>
|
||||
<input type="number" step="0.001" class="form-control" name="deviation_mid[]" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Deviation Low</label>
|
||||
<input type="number" step="0.001" class="form-control" name="deviation_low[]" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Set High</label>
|
||||
<input type="number" step="0.001" class="form-control" name="set_high[]" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Set Mid</label>
|
||||
<input type="number" step="0.001" class="form-control" name="set_mid[]" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Set Low</label>
|
||||
<input type="number" step="0.001" class="form-control" name="set_low[]" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="form-check form-switch mt-4">
|
||||
<input class="form-check-input" type="checkbox" name="passed[]" checked>
|
||||
<label class="form-check-label">Passed</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Channel</label>
|
||||
<select class="form-select select2" name="channel_serial[]" required>
|
||||
<option value="">Select Channel</option>
|
||||
{% for channel in channels %}
|
||||
<option value="{{ channel.serial_number }}">{{ channel.serial_number }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<label class="form-label">Scale</label>
|
||||
<input type="number" step="0.001" class="form-control" name="scale[]" required>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<label class="form-label">Offset</label>
|
||||
<input type="number" step="0.001" class="form-control" name="offset[]" required>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<label class="form-label">Dev Low</label>
|
||||
<input type="number" step="0.001" class="form-control" name="deviation_low[]" required>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<label class="form-label">Dev Mid</label>
|
||||
<input type="number" step="0.001" class="form-control" name="deviation_mid[]" required>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<label class="form-label">Dev High</label>
|
||||
<input type="number" step="0.001" class="form-control" name="deviation_high[]" required>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<label class="form-label">Set Low</label>
|
||||
<input type="number" step="0.001" class="form-control" name="set_low[]" required>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<label class="form-label">Set Mid</label>
|
||||
<input type="number" step="0.001" class="form-control" name="set_mid[]" required>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<label class="form-label">Set High</label>
|
||||
<input type="number" step="0.001" class="form-control" name="set_high[]" required>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<div class="form-check form-switch mt-4">
|
||||
<input class="form-check-input" type="checkbox" name="passed[]" checked>
|
||||
<label class="form-check-label">Passed</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -115,63 +133,152 @@
|
||||
</div>
|
||||
|
||||
<div class="text-end">
|
||||
<button type="submit" class="btn btn-primary">Save Calibration Batch</button>
|
||||
<button type="submit" class="btn btn-primary" id="saveBatch" disabled>Save Calibration Batch</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize Select2
|
||||
$(function() {
|
||||
// Initialize Select2 and confirm button
|
||||
$('.select2').select2();
|
||||
|
||||
// Add channel row
|
||||
$('#addChannel').click(function() {
|
||||
console.log('Initializing calibration form...');
|
||||
|
||||
// Handle batch confirmation
|
||||
$('#confirmBatch').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
console.log('Confirm Batch clicked');
|
||||
|
||||
// Debug: Check if jQuery is working
|
||||
console.log('jQuery version:', $.fn.jquery);
|
||||
// Validate required fields
|
||||
if (!$('#work_order_id').val() || !$('#std_used').val() ||
|
||||
!$('#date').val() || !$('#probe_model').val() || !$('#parameter').val()) {
|
||||
alert('Please complete all Batch Information fields');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate date is not in future
|
||||
const selectedDate = new Date($('#date').val());
|
||||
const today = new Date();
|
||||
if (selectedDate > today) {
|
||||
alert('Calibration date cannot be in the future');
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable and gray out batch section
|
||||
$('.card-body', $(this).closest('.card')).find('input, select').prop('disabled', true);
|
||||
$(this).closest('.card').addClass('bg-light');
|
||||
$('#confirmBatch').prop('disabled', true).removeClass('btn-success').addClass('btn-secondary');
|
||||
|
||||
// Show channels section with filtered options
|
||||
$('#channelsSection').show();
|
||||
updateChannelDropdowns($('#probe_model').val(), $('#parameter').val());
|
||||
$('#saveBatch').prop('disabled', false);
|
||||
|
||||
// Remove the initial empty channel row if present
|
||||
if ($('.channel-row').length === 1 && !$('select[name="channel_serial[]"]').val()) {
|
||||
$('.channel-row').remove();
|
||||
}
|
||||
|
||||
// Add first channel row with filtered options
|
||||
addChannelRow();
|
||||
});
|
||||
|
||||
// Function to update channel dropdowns
|
||||
function updateChannelDropdowns(probeModelId, parameterId) {
|
||||
if (!probeModelId || !parameterId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.get('/calibrations/filtered-channels', {
|
||||
probe_model: probeModelId,
|
||||
parameter: parameterId
|
||||
}, function(data) {
|
||||
window.filteredChannels = data; // Cache the filtered channels
|
||||
updateAllChannelDropdowns(data);
|
||||
});
|
||||
}
|
||||
|
||||
function updateAllChannelDropdowns(channels) {
|
||||
$('select[name="channel_serial[]"]').each(function() {
|
||||
const $select = $(this);
|
||||
const currentVal = $select.val();
|
||||
$select.empty().append('<option value="">Select Channel</option>');
|
||||
|
||||
channels.forEach(function(channel) {
|
||||
$select.append($('<option>', {
|
||||
value: channel.serial_number,
|
||||
text: channel.serial_number
|
||||
}));
|
||||
});
|
||||
|
||||
if (currentVal && channels.some(c => c.serial_number === currentVal)) {
|
||||
$select.val(currentVal);
|
||||
}
|
||||
$select.trigger('change');
|
||||
});
|
||||
}
|
||||
|
||||
// Watch for changes in probe model and parameter
|
||||
$('#probe_model, #parameter').on('change', function() {
|
||||
const probeModelId = $('#probe_model').val();
|
||||
const parameterId = $('#parameter').val();
|
||||
updateChannelDropdowns(probeModelId, parameterId);
|
||||
});
|
||||
|
||||
// Add channel row with current filter
|
||||
function addChannelRow() {
|
||||
const channelOptions = window.filteredChannels ?
|
||||
window.filteredChannels.map(c =>
|
||||
`<option value="${c.serial_number}">${c.serial_number}</option>`
|
||||
).join('') :
|
||||
'';
|
||||
|
||||
const newRow = `
|
||||
<div class="channel-row mb-3">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Channel</label>
|
||||
<select class="form-select select2" name="channel_serial[]" required>
|
||||
<option value="">Select Channel</option>
|
||||
{% for channel in channels %}
|
||||
<option value="{{ channel.serial_number }}">{{ channel.serial_number }}</option>
|
||||
{% endfor %}
|
||||
${channelOptions}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="col-md-1">
|
||||
<label class="form-label">Scale</label>
|
||||
<input type="number" step="0.001" class="form-control" name="scale[]" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="col-md-1">
|
||||
<label class="form-label">Offset</label>
|
||||
<input type="number" step="0.001" class="form-control" name="offset[]" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Deviation High</label>
|
||||
<input type="number" step="0.001" class="form-control" name="deviation_high[]" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Deviation Mid</label>
|
||||
<input type="number" step="0.001" class="form-control" name="deviation_mid[]" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Deviation Low</label>
|
||||
<div class="col-md-1">
|
||||
<label class="form-label">Dev Low</label>
|
||||
<input type="number" step="0.001" class="form-control" name="deviation_low[]" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Set High</label>
|
||||
<input type="number" step="0.001" class="form-control" name="set_high[]" required>
|
||||
<div class="col-md-1">
|
||||
<label class="form-label">Dev Mid</label>
|
||||
<input type="number" step="0.001" class="form-control" name="deviation_mid[]" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Set Mid</label>
|
||||
<input type="number" step="0.001" class="form-control" name="set_mid[]" required>
|
||||
<div class="col-md-1">
|
||||
<label class="form-label">Dev High</label>
|
||||
<input type="number" step="0.001" class="form-control" name="deviation_high[]" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="col-md-1">
|
||||
<label class="form-label">Set Low</label>
|
||||
<input type="number" step="0.001" class="form-control" name="set_low[]" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="col-md-1">
|
||||
<label class="form-label">Set Mid</label>
|
||||
<input type="number" step="0.001" class="form-control" name="set_mid[]" required>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<label class="form-label">Set High</label>
|
||||
<input type="number" step="0.001" class="form-control" name="set_high[]" required>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<div class="form-check form-switch mt-4">
|
||||
<input class="form-check-input" type="checkbox" name="passed[]" checked>
|
||||
<label class="form-check-label">Passed</label>
|
||||
@@ -179,9 +286,59 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
$('#channelRows').append(newRow);
|
||||
$('.select2').select2();
|
||||
});
|
||||
}
|
||||
|
||||
$('#addChannel').click(addChannelRow);
|
||||
|
||||
// Initialize with any existing selections
|
||||
if ($('#probe_model').val() && $('#parameter').val()) {
|
||||
updateChannelDropdowns($('#probe_model').val(), $('#parameter').val());
|
||||
}
|
||||
|
||||
// Form validation before submission
|
||||
function validateForm() {
|
||||
// Validate channel selections
|
||||
const serialPattern = /^[0-9A-F]{16}$/;
|
||||
const serials = $('select[name="channel_serial[]"]');
|
||||
let valid = true;
|
||||
|
||||
serials.each(function() {
|
||||
const serial = $(this).val();
|
||||
if (!serial || !serialPattern.test(serial)) {
|
||||
alert('Invalid channel serial number: ' + serial);
|
||||
$(this).focus();
|
||||
valid = false;
|
||||
return false; // break loop
|
||||
}
|
||||
});
|
||||
|
||||
if (!valid) return false;
|
||||
|
||||
// Validate numeric fields
|
||||
const numericFields = [
|
||||
'scale[]', 'offset[]', 'deviation_low[]',
|
||||
'deviation_mid[]', 'deviation_high[]',
|
||||
'set_low[]', 'set_mid[]', 'set_high[]'
|
||||
];
|
||||
|
||||
for (const field of numericFields) {
|
||||
$(`input[name="${field}"]`).each(function() {
|
||||
const val = parseFloat($(this).val());
|
||||
if (isNaN(val)) {
|
||||
alert('Please enter valid numbers for all fields');
|
||||
$(this).focus();
|
||||
valid = false;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (!valid) break;
|
||||
}
|
||||
|
||||
return valid;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,24 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SmartScan Probe Track</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="#">SmartScan Probe Track</a>
|
||||
<div class="navbar-nav ms-auto">
|
||||
<span class="navbar-text me-3">
|
||||
Logged in as: {{ user.user_name }}
|
||||
</span>
|
||||
<a href="/auth/logout" class="btn btn-outline-light">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
@@ -31,6 +13,7 @@
|
||||
<a href="/calibrations" class="list-group-item list-group-item-action">Calibrations</a>
|
||||
<a href="/work_orders" class="list-group-item list-group-item-action">Work Orders</a>
|
||||
<a href="/locations" class="list-group-item list-group-item-action">Locations</a>
|
||||
<a href="{{ url_for('standards.list_standards') }}" class="list-group-item list-group-item-action">Standards</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,7 +23,7 @@
|
||||
Dashboard
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Welcome to SmartScan Probe Track</h5>
|
||||
<h5 class="card-title">Welcome to CIMTechniques Probe Tracker</h5>
|
||||
<p class="card-text">You are logged in as {{ user.user_name }}.</p>
|
||||
{% if user.can_calibrate %}
|
||||
<span class="badge bg-success">Calibrator</span>
|
||||
@@ -53,7 +36,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
{% endblock %}
|
||||
|
||||
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 %}
|
||||
<div class="container mt-4">
|
||||
<h2>Location History for Probe {{ probe_id }}</h2>
|
||||
{% if probe_id %}
|
||||
<h2>Location History for Probe {{ probe_id }}</h2>
|
||||
{% else %}
|
||||
<h2>All Locations</h2>
|
||||
<div class="mb-3">
|
||||
<a href="/locations/new" class="btn btn-primary">Add New Location</a>
|
||||
</div>
|
||||
<p>Select a probe to view its location history</p>
|
||||
{% endif %}
|
||||
|
||||
{% if locations and not probe_id %}
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h5>Locations List</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Address</th>
|
||||
<th>Contact</th>
|
||||
<th>Phone</th>
|
||||
<th>Email</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for location in locations %}
|
||||
<tr>
|
||||
<td>{{ location.name }}</td>
|
||||
<td>{{ location.address }}</td>
|
||||
<td>{{ location.contact_name }}</td>
|
||||
<td>{{ location.contact_phone }}</td>
|
||||
<td>{{ location.contact_email }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h5>Assignment Timeline</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="timelineChart" height="100"></canvas>
|
||||
{% if chart_data_json %}
|
||||
<canvas id="timelineChart" height="100"></canvas>
|
||||
{% else %}
|
||||
<p>No location data available</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if location_data %}
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h5>Location Details</h5>
|
||||
@@ -40,6 +85,7 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% block scripts %}
|
||||
|
||||
@@ -8,8 +8,14 @@
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">All Probes</h5>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="activeFilter"
|
||||
{% if active_only %}checked{% endif %}
|
||||
onchange="window.location.search = '?active_only=' + this.checked">
|
||||
<label class="form-check-label" for="activeFilter">Only show active probes</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if probes %}
|
||||
@@ -17,6 +23,7 @@
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Model</th>
|
||||
<th>Serial Number</th>
|
||||
<th>Description</th>
|
||||
<th>Created</th>
|
||||
@@ -25,23 +32,33 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for probe in probes %}
|
||||
<tr>
|
||||
<td>{{ probe.serial_number }}</td>
|
||||
<td>{{ probe.description }}</td>
|
||||
<td>{{ probe.created_at.strftime('%Y-%m-%d') }}</td>
|
||||
<td>
|
||||
{% if probe.retired_at %}
|
||||
<span class="badge bg-secondary">Retired</span>
|
||||
{% else %}
|
||||
<span class="badge bg-success">Active</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('probes.view_probe', probe_id=probe.id) }}"
|
||||
class="btn btn-sm btn-outline-primary">View</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% set current_model = none %}
|
||||
{% for probe in probes|sort(attribute='model_name') %}
|
||||
{% if probe.model_name != current_model %}
|
||||
<tr class="table-info">
|
||||
<td colspan="6">
|
||||
<strong>{{ probe.model_name }}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
{% set current_model = probe.model_name %}
|
||||
{% endif %}
|
||||
<tr class="probe-row" data-active="{{ 'true' if not probe.retired_at else 'false' }}">
|
||||
<td></td> <!-- Empty cell under model header -->
|
||||
<td>{{ probe.serial_number }}</td>
|
||||
<td>{{ probe.description }}</td>
|
||||
<td>{{ probe.created_at.strftime('%Y-%m-%d') }}</td>
|
||||
<td>
|
||||
{% if probe.retired_at %}
|
||||
<span class="badge bg-secondary">Retired</span>
|
||||
{% else %}
|
||||
<span class="badge bg-success">Active</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('probes.view_probe', probe_id=probe.id) }}"
|
||||
class="btn btn-sm btn-outline-primary">View</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -52,4 +69,21 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const activeFilter = document.getElementById('activeFilter');
|
||||
|
||||
function filterProbes() {
|
||||
const showActiveOnly = activeFilter.checked;
|
||||
document.querySelectorAll('.probe-row').forEach(row => {
|
||||
const isActive = row.dataset.active === 'true';
|
||||
row.style.display = (showActiveOnly && !isActive) ? 'none' : '';
|
||||
});
|
||||
}
|
||||
|
||||
// Initial filter on page load
|
||||
filterProbes();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
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