Refactor experiment management and update data structures
- Renamed columns in the experimental run sheet CSV for clarity. - Updated the ExperimentForm component to include new fields for weight per repetition and additional parameters specific to Meyer Cracker experiments. - Enhanced the data entry logic to handle new experiment phases and machine types. - Refactored repetition scheduling logic to use scheduled_date instead of schedule_status for better clarity in status representation. - Improved the user interface for displaying experiment phases and their associated statuses. - Removed outdated seed data and updated database migration scripts to reflect the new schema changes.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import type { CreateExperimentRequest, UpdateExperimentRequest, ScheduleStatus, ResultsStatus } from '../lib/supabase'
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { CreateExperimentRequest, UpdateExperimentRequest, ScheduleStatus, ResultsStatus, ExperimentPhase, MachineType } from '../lib/supabase'
|
||||
import { experimentPhaseManagement, machineTypeManagement } from '../lib/supabase'
|
||||
|
||||
interface ExperimentFormProps {
|
||||
initialData?: Partial<CreateExperimentRequest & { schedule_status: ScheduleStatus; results_status: ResultsStatus; completion_status: boolean }>
|
||||
@@ -14,12 +15,17 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa
|
||||
const [formData, setFormData] = useState<CreateExperimentRequest & { schedule_status: ScheduleStatus; results_status: ResultsStatus; completion_status: boolean }>({
|
||||
experiment_number: initialData?.experiment_number || 0,
|
||||
reps_required: initialData?.reps_required || 1,
|
||||
weight_per_repetition_lbs: (initialData as any)?.weight_per_repetition_lbs || 1,
|
||||
soaking_duration_hr: initialData?.soaking_duration_hr || 0,
|
||||
air_drying_time_min: initialData?.air_drying_time_min || 0,
|
||||
plate_contact_frequency_hz: initialData?.plate_contact_frequency_hz || 1,
|
||||
throughput_rate_pecans_sec: initialData?.throughput_rate_pecans_sec || 1,
|
||||
crush_amount_in: initialData?.crush_amount_in || 0,
|
||||
entry_exit_height_diff_in: initialData?.entry_exit_height_diff_in || 0,
|
||||
// Meyer-specific (UI only)
|
||||
motor_speed_hz: (initialData as any)?.motor_speed_hz || 1,
|
||||
jig_displacement_inches: (initialData as any)?.jig_displacement_inches || 0,
|
||||
spring_stiffness_nm: (initialData as any)?.spring_stiffness_nm || 1,
|
||||
schedule_status: initialData?.schedule_status || 'pending schedule',
|
||||
results_status: initialData?.results_status || 'valid',
|
||||
completion_status: initialData?.completion_status || false,
|
||||
@@ -27,6 +33,31 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa
|
||||
})
|
||||
|
||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||
const [phase, setPhase] = useState<ExperimentPhase | null>(null)
|
||||
const [crackingMachine, setCrackingMachine] = useState<MachineType | null>(null)
|
||||
const [metaLoading, setMetaLoading] = useState<boolean>(false)
|
||||
|
||||
useEffect(() => {
|
||||
const loadMeta = async () => {
|
||||
if (!phaseId) return
|
||||
try {
|
||||
setMetaLoading(true)
|
||||
const p = await experimentPhaseManagement.getExperimentPhaseById(phaseId)
|
||||
setPhase(p)
|
||||
if (p?.has_cracking && p.cracking_machine_type_id) {
|
||||
const mt = await machineTypeManagement.getMachineTypeById(p.cracking_machine_type_id)
|
||||
setCrackingMachine(mt)
|
||||
} else {
|
||||
setCrackingMachine(null)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load phase/machine metadata', e)
|
||||
} finally {
|
||||
setMetaLoading(false)
|
||||
}
|
||||
}
|
||||
loadMeta()
|
||||
}, [phaseId])
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: Record<string, string> = {}
|
||||
@@ -40,8 +71,9 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa
|
||||
newErrors.reps_required = 'Repetitions required must be a positive integer'
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (!formData.weight_per_repetition_lbs || formData.weight_per_repetition_lbs <= 0) {
|
||||
newErrors.weight_per_repetition_lbs = 'Weight per repetition must be positive'
|
||||
}
|
||||
|
||||
|
||||
if (formData.soaking_duration_hr < 0) {
|
||||
@@ -52,16 +84,30 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa
|
||||
newErrors.air_drying_time_min = 'Air drying time cannot be negative'
|
||||
}
|
||||
|
||||
if (!formData.plate_contact_frequency_hz || formData.plate_contact_frequency_hz <= 0) {
|
||||
newErrors.plate_contact_frequency_hz = 'Plate contact frequency must be positive'
|
||||
}
|
||||
|
||||
if (!formData.throughput_rate_pecans_sec || formData.throughput_rate_pecans_sec <= 0) {
|
||||
newErrors.throughput_rate_pecans_sec = 'Throughput rate must be positive'
|
||||
}
|
||||
|
||||
if (formData.crush_amount_in < 0) {
|
||||
newErrors.crush_amount_in = 'Crush amount cannot be negative'
|
||||
// Validate cracking fields depending on machine
|
||||
if (phase?.has_cracking) {
|
||||
if (crackingMachine?.name === 'JC Cracker') {
|
||||
if (!formData.plate_contact_frequency_hz || formData.plate_contact_frequency_hz <= 0) {
|
||||
newErrors.plate_contact_frequency_hz = 'Plate contact frequency must be positive'
|
||||
}
|
||||
if (!formData.throughput_rate_pecans_sec || formData.throughput_rate_pecans_sec <= 0) {
|
||||
newErrors.throughput_rate_pecans_sec = 'Throughput rate must be positive'
|
||||
}
|
||||
if (formData.crush_amount_in < 0) {
|
||||
newErrors.crush_amount_in = 'Crush amount cannot be negative'
|
||||
}
|
||||
}
|
||||
if (crackingMachine?.name === 'Meyer Cracker') {
|
||||
if (!(formData as any).motor_speed_hz || (formData as any).motor_speed_hz <= 0) {
|
||||
newErrors.motor_speed_hz = 'Motor speed must be positive'
|
||||
}
|
||||
if ((formData as any).jig_displacement_inches === undefined) {
|
||||
newErrors.jig_displacement_inches = 'Jig displacement is required'
|
||||
}
|
||||
if (!(formData as any).spring_stiffness_nm || (formData as any).spring_stiffness_nm <= 0) {
|
||||
newErrors.spring_stiffness_nm = 'Spring stiffness must be positive'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
@@ -80,14 +126,10 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa
|
||||
const submitData = isEditing ? formData : {
|
||||
experiment_number: formData.experiment_number,
|
||||
reps_required: formData.reps_required,
|
||||
soaking_duration_hr: formData.soaking_duration_hr,
|
||||
air_drying_time_min: formData.air_drying_time_min,
|
||||
plate_contact_frequency_hz: formData.plate_contact_frequency_hz,
|
||||
throughput_rate_pecans_sec: formData.throughput_rate_pecans_sec,
|
||||
crush_amount_in: formData.crush_amount_in,
|
||||
entry_exit_height_diff_in: formData.entry_exit_height_diff_in,
|
||||
schedule_status: formData.schedule_status,
|
||||
results_status: formData.results_status
|
||||
weight_per_repetition_lbs: formData.weight_per_repetition_lbs,
|
||||
results_status: formData.results_status,
|
||||
completion_status: formData.completion_status,
|
||||
phase_id: formData.phase_id
|
||||
}
|
||||
|
||||
await onSubmit(submitData)
|
||||
@@ -157,139 +199,264 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="weight_per_repetition_lbs" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Weight per Repetition (lbs) *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="weight_per_repetition_lbs"
|
||||
value={formData.weight_per_repetition_lbs}
|
||||
onChange={(e) => handleInputChange('weight_per_repetition_lbs' as any, parseFloat(e.target.value) || 0)}
|
||||
className={`max-w-xs px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.weight_per_repetition_lbs ? 'border-red-300' : 'border-gray-300'}`}
|
||||
placeholder="e.g. 10.0"
|
||||
min="0.1"
|
||||
step="0.1"
|
||||
required
|
||||
/>
|
||||
{errors.weight_per_repetition_lbs && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.weight_per_repetition_lbs}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Experiment Parameters */}
|
||||
<div className="border-t pt-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Experiment Parameters</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Dynamic Sections by Phase */}
|
||||
<div className="border-t pt-6 space-y-8">
|
||||
{/* Soaking */}
|
||||
{phase?.has_soaking && (
|
||||
<div>
|
||||
<label htmlFor="soaking_duration_hr" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Soaking Duration (hours) *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="soaking_duration_hr"
|
||||
value={formData.soaking_duration_hr}
|
||||
onChange={(e) => handleInputChange('soaking_duration_hr', parseFloat(e.target.value) || 0)}
|
||||
className={`max-w-xs px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.soaking_duration_hr ? 'border-red-300' : 'border-gray-300'
|
||||
}`}
|
||||
placeholder="0.0"
|
||||
min="0"
|
||||
step="0.1"
|
||||
required
|
||||
/>
|
||||
{errors.soaking_duration_hr && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.soaking_duration_hr}</p>
|
||||
)}
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Soaking</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label htmlFor="soaking_duration_hr" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Soaking Duration (hours) *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="soaking_duration_hr"
|
||||
value={formData.soaking_duration_hr}
|
||||
onChange={(e) => handleInputChange('soaking_duration_hr', parseFloat(e.target.value) || 0)}
|
||||
className={`max-w-xs px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.soaking_duration_hr ? 'border-red-300' : 'border-gray-300'}`}
|
||||
placeholder="0.0"
|
||||
min="0"
|
||||
step="0.1"
|
||||
required
|
||||
/>
|
||||
{errors.soaking_duration_hr && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.soaking_duration_hr}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Air-Drying */}
|
||||
{phase?.has_airdrying && (
|
||||
<div>
|
||||
<label htmlFor="air_drying_time_min" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Air Drying Time (minutes) *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="air_drying_time_min"
|
||||
value={formData.air_drying_time_min}
|
||||
onChange={(e) => handleInputChange('air_drying_time_min', parseInt(e.target.value) || 0)}
|
||||
className={`max-w-xs px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.air_drying_time_min ? 'border-red-300' : 'border-gray-300'
|
||||
}`}
|
||||
placeholder="0"
|
||||
min="0"
|
||||
step="1"
|
||||
required
|
||||
/>
|
||||
{errors.air_drying_time_min && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.air_drying_time_min}</p>
|
||||
)}
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Air-Drying</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label htmlFor="air_drying_time_min" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Air Drying Time (minutes) *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="air_drying_time_min"
|
||||
value={formData.air_drying_time_min}
|
||||
onChange={(e) => handleInputChange('air_drying_time_min', parseInt(e.target.value) || 0)}
|
||||
className={`max-w-xs px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.air_drying_time_min ? 'border-red-300' : 'border-gray-300'}`}
|
||||
placeholder="0"
|
||||
min="0"
|
||||
step="1"
|
||||
required
|
||||
/>
|
||||
{errors.air_drying_time_min && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.air_drying_time_min}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Cracking - machine specific */}
|
||||
{phase?.has_cracking && (
|
||||
<div>
|
||||
<label htmlFor="plate_contact_frequency_hz" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Plate Contact Frequency (Hz) *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="plate_contact_frequency_hz"
|
||||
value={formData.plate_contact_frequency_hz}
|
||||
onChange={(e) => handleInputChange('plate_contact_frequency_hz', parseFloat(e.target.value) || 1)}
|
||||
className={`max-w-xs px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.plate_contact_frequency_hz ? 'border-red-300' : 'border-gray-300'
|
||||
}`}
|
||||
placeholder="1.0"
|
||||
min="0.1"
|
||||
step="0.1"
|
||||
required
|
||||
/>
|
||||
{errors.plate_contact_frequency_hz && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.plate_contact_frequency_hz}</p>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Cracking {crackingMachine ? `(${crackingMachine.name})` : ''}</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{crackingMachine?.name === 'JC Cracker' && (
|
||||
<>
|
||||
<div>
|
||||
<label htmlFor="plate_contact_frequency_hz" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Plate Contact Frequency (Hz) *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="plate_contact_frequency_hz"
|
||||
value={formData.plate_contact_frequency_hz}
|
||||
onChange={(e) => handleInputChange('plate_contact_frequency_hz', parseFloat(e.target.value) || 1)}
|
||||
className={`max-w-xs px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.plate_contact_frequency_hz ? 'border-red-300' : 'border-gray-300'}`}
|
||||
placeholder="1.0"
|
||||
min="0.1"
|
||||
step="0.1"
|
||||
required
|
||||
/>
|
||||
{errors.plate_contact_frequency_hz && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.plate_contact_frequency_hz}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="throughput_rate_pecans_sec" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Throughput Rate (pecans/sec) *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="throughput_rate_pecans_sec"
|
||||
value={formData.throughput_rate_pecans_sec}
|
||||
onChange={(e) => handleInputChange('throughput_rate_pecans_sec', parseFloat(e.target.value) || 1)}
|
||||
className={`max-w-xs px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.throughput_rate_pecans_sec ? 'border-red-300' : 'border-gray-300'}`}
|
||||
placeholder="1.0"
|
||||
min="0.1"
|
||||
step="0.1"
|
||||
required
|
||||
/>
|
||||
{errors.throughput_rate_pecans_sec && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.throughput_rate_pecans_sec}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="crush_amount_in" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Crush Amount (thousandths of inch) *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="crush_amount_in"
|
||||
value={formData.crush_amount_in}
|
||||
onChange={(e) => handleInputChange('crush_amount_in', parseFloat(e.target.value) || 0)}
|
||||
className={`max-w-xs px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.crush_amount_in ? 'border-red-300' : 'border-gray-300'}`}
|
||||
placeholder="0.0"
|
||||
min="0"
|
||||
step="0.001"
|
||||
required
|
||||
/>
|
||||
{errors.crush_amount_in && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.crush_amount_in}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<label htmlFor="entry_exit_height_diff_in" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Entry/Exit Height Difference (inches) *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="entry_exit_height_diff_in"
|
||||
value={formData.entry_exit_height_diff_in}
|
||||
onChange={(e) => handleInputChange('entry_exit_height_diff_in', parseFloat(e.target.value) || 0)}
|
||||
className={`max-w-sm px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.entry_exit_height_diff_in ? 'border-red-300' : 'border-gray-300'}`}
|
||||
placeholder="0.0 (can be negative)"
|
||||
step="0.1"
|
||||
required
|
||||
/>
|
||||
{errors.entry_exit_height_diff_in && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.entry_exit_height_diff_in}</p>
|
||||
)}
|
||||
<p className="mt-1 text-sm text-gray-500">Positive values indicate entry is higher than exit</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{crackingMachine?.name === 'Meyer Cracker' && (
|
||||
<>
|
||||
<div>
|
||||
<label htmlFor="motor_speed_hz" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Motor Speed (Hz) *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="motor_speed_hz"
|
||||
value={(formData as any).motor_speed_hz}
|
||||
onChange={(e) => handleInputChange('motor_speed_hz' as any, parseFloat(e.target.value) || 1)}
|
||||
className={`max-w-xs px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.motor_speed_hz ? 'border-red-300' : 'border-gray-300'}`}
|
||||
placeholder="1.0"
|
||||
min="0.1"
|
||||
step="0.1"
|
||||
required
|
||||
/>
|
||||
{errors.motor_speed_hz && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.motor_speed_hz}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="jig_displacement_inches" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Jig Displacement (inches) *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="jig_displacement_inches"
|
||||
value={(formData as any).jig_displacement_inches}
|
||||
onChange={(e) => handleInputChange('jig_displacement_inches' as any, parseFloat(e.target.value) || 0)}
|
||||
className={`max-w-xs px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.jig_displacement_inches ? 'border-red-300' : 'border-gray-300'}`}
|
||||
placeholder="0.0"
|
||||
min="0"
|
||||
step="0.01"
|
||||
required
|
||||
/>
|
||||
{errors.jig_displacement_inches && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.jig_displacement_inches}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="spring_stiffness_nm" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Spring Stiffness (N·m) *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="spring_stiffness_nm"
|
||||
value={(formData as any).spring_stiffness_nm}
|
||||
onChange={(e) => handleInputChange('spring_stiffness_nm' as any, parseFloat(e.target.value) || 1)}
|
||||
className={`max-w-xs px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.spring_stiffness_nm ? 'border-red-300' : 'border-gray-300'}`}
|
||||
placeholder="1.0"
|
||||
min="0.1"
|
||||
step="0.1"
|
||||
required
|
||||
/>
|
||||
{errors.spring_stiffness_nm && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.spring_stiffness_nm}</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Shelling */}
|
||||
{phase?.has_shelling && (
|
||||
<div>
|
||||
<label htmlFor="throughput_rate_pecans_sec" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Throughput Rate (pecans/sec) *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="throughput_rate_pecans_sec"
|
||||
value={formData.throughput_rate_pecans_sec}
|
||||
onChange={(e) => handleInputChange('throughput_rate_pecans_sec', parseFloat(e.target.value) || 1)}
|
||||
className={`max-w-xs px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.throughput_rate_pecans_sec ? 'border-red-300' : 'border-gray-300'
|
||||
}`}
|
||||
placeholder="1.0"
|
||||
min="0.1"
|
||||
step="0.1"
|
||||
required
|
||||
/>
|
||||
{errors.throughput_rate_pecans_sec && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.throughput_rate_pecans_sec}</p>
|
||||
)}
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Shelling</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Shelling Start Offset (minutes)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={(formData as any).shelling_start_offset_min || 0}
|
||||
onChange={(e) => handleInputChange('shelling_start_offset_min' as any, parseInt(e.target.value) || 0)}
|
||||
className="max-w-xs px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm"
|
||||
placeholder="0"
|
||||
min="0"
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="crush_amount_in" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Crush Amount (thousandths of inch) *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="crush_amount_in"
|
||||
value={formData.crush_amount_in}
|
||||
onChange={(e) => handleInputChange('crush_amount_in', parseFloat(e.target.value) || 0)}
|
||||
className={`max-w-xs px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.crush_amount_in ? 'border-red-300' : 'border-gray-300'
|
||||
}`}
|
||||
placeholder="0.0"
|
||||
min="0"
|
||||
step="0.001"
|
||||
required
|
||||
/>
|
||||
{errors.crush_amount_in && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.crush_amount_in}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<label htmlFor="entry_exit_height_diff_in" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Entry/Exit Height Difference (inches) *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="entry_exit_height_diff_in"
|
||||
value={formData.entry_exit_height_diff_in}
|
||||
onChange={(e) => handleInputChange('entry_exit_height_diff_in', parseFloat(e.target.value) || 0)}
|
||||
className={`max-w-sm px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.entry_exit_height_diff_in ? 'border-red-300' : 'border-gray-300'
|
||||
}`}
|
||||
placeholder="0.0 (can be negative)"
|
||||
step="0.1"
|
||||
required
|
||||
/>
|
||||
{errors.entry_exit_height_diff_in && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.entry_exit_height_diff_in}</p>
|
||||
)}
|
||||
<p className="mt-1 text-sm text-gray-500">Positive values indicate entry is higher than exit</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Status Fields (only show when editing) */}
|
||||
|
||||
Reference in New Issue
Block a user