diff --git a/backend/app/models.py b/backend/app/models.py index 20772c8..b692f3c 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -139,10 +139,14 @@ class Sample(Base): id = Column(Integer, primary_key=True, index=True, autoincrement=True) sample_name = Column(String(255), index=True) - position = Column(Integer) # Matches `position` in data creation script + proteinname = Column(String(255), index=True) + position = Column(Integer) + priority = Column(Integer) + comments = Column(String(255)) data_collection_parameters = Column(JSON, nullable=True) # Foreign keys and relationships + dewar_id = Column(Integer, ForeignKey("dewars.id")) puck_id = Column(Integer, ForeignKey("pucks.id")) puck = relationship("Puck", back_populates="samples") events = relationship("SampleEvent", back_populates="sample") diff --git a/backend/app/routers/dewar.py b/backend/app/routers/dewar.py index 25ee92f..318f122 100644 --- a/backend/app/routers/dewar.py +++ b/backend/app/routers/dewar.py @@ -91,7 +91,10 @@ async def create_dewar( sample = SampleModel( puck_id=puck.id, sample_name=sample_data.sample_name, + proteinname=sample_data.proteinname, position=sample_data.position, + priority=sample_data.priority, + comments=sample_data.comments, data_collection_parameters=sample_data.data_collection_parameters, ) db.add(sample) @@ -285,6 +288,66 @@ async def download_dewar_label(dewar_id: int, db: Session = Depends(get_db)): ) +@router.get("/dewars/{dewar_id}/samples", response_model=dict) +async def get_dewar_samples(dewar_id: int, db: Session = Depends(get_db)): + # Fetch Dewar, associated Pucks, and Samples + dewar = db.query(DewarModel).filter(DewarModel.id == dewar_id).first() + if not dewar: + raise HTTPException(status_code=404, detail="Dewar not found") + + pucks = db.query(PuckModel).filter(PuckModel.dewar_id == dewar.id).all() + + data = {"dewar": {"id": dewar.id, "dewar_name": dewar.dewar_name}, "pucks": []} + + for puck in pucks: + samples = db.query(SampleModel).filter(SampleModel.puck_id == puck.id).all() + data["pucks"].append( + { + "id": puck.id, + "name": puck.puck_name, + "type": puck.puck_type, + "samples": [ + { + "id": sample.id, + "position": sample.position, + "dewar_name": dewar.dewar_name, # Add Dewar name here + "sample_name": sample.sample_name, + "priority": sample.priority, + "comments": sample.comments, + # "directory":sample.directory, + "proteinname": sample.proteinname, + # "oscillation": datacollection.oscillation, + # "aperture": 10, + # "exposure": 11, + # "totalrange": 12, + # "transmission": 13, + # "dose": 14, + # "targetresolution": 15, + # "datacollectiontype": 16, + # "processingpipeline": 17, + # "spacegroupnumber": 18, + # "cellparameters": 19, + # "rescutkey": 20, + # "rescutvalue": 21, + # "userresolution": 22, + # "pdbid": 23, + # "autoprocfull": 24, + # "procfull": 25, + # "adpenabled": 26, + # "noano": 27, + # "ffcscampaign": 28, + # "trustedhigh": 29, + # "autoprocextraparams": 30, + # "chiphiangles": 31, + } + for sample in samples + ], + } + ) + + return data + + @router.get("/", response_model=List[DewarSchema]) async def get_dewars(db: Session = Depends(get_db)): try: diff --git a/backend/app/sample_models.py b/backend/app/sample_models.py index b29b2c6..4dc5497 100644 --- a/backend/app/sample_models.py +++ b/backend/app/sample_models.py @@ -3,6 +3,10 @@ from typing import Any, Optional, List, Dict from pydantic import BaseModel, Field, field_validator from typing_extensions import Annotated +import logging + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) class SpreadsheetModel(BaseModel): @@ -98,34 +102,45 @@ class SpreadsheetModel(BaseModel): @field_validator("directory", mode="before") @classmethod def directory_characters(cls, v): - if v: - v = str(v).strip("/").replace(" ", "_") - if re.search("\n", v): - raise ValueError(f" '{v}' is not valid. newline character detected.") + logger.debug(f"Validating 'directory' field with value: {repr(v)}") - valid_macros = [ - "{date}", - "{prefix}", - "{sgpuck}", - "{puck}", - "{beamline}", - "{sgprefix}", - "{sgpriority}", - "{sgposition}", - "{protein}", - "{method}", - ] - pattern = re.compile("|".join(re.escape(macro) for macro in valid_macros)) - v = pattern.sub("macro", v) - - allowed_chars = "[a-z0-9_.+-]" - directory_re = re.compile( - f"^(({allowed_chars}*|{allowed_chars}+)*/*)*$", re.IGNORECASE + # Assign default value if v is None or empty + if not v: + default_value = "{sgPuck}/{sgPosition}" + logger.warning( + f"'directory' field is empty or None. Assigning default value: " + f"{default_value}" + ) + return default_value + + v = str(v).strip("/").replace(" ", "_") + if "\n" in v: + raise ValueError(f"'{v}' is not valid. Newline character detected.") + + # Replace valid macros for consistency + valid_macros = [ + "{date}", + "{prefix}", + "{sgPuck}", + "{sgPosition}", + "{beamline}", + "{sgPrefix}", + "{sgPriority}", + "{protein}", + "{method}", + ] + pattern = re.compile("|".join(re.escape(macro) for macro in valid_macros)) + v = pattern.sub("macro", v) + + # Ensure only allowed characters are in the directory value + allowed_chars = "[a-z0-9_.+-]" + directory_re = re.compile( + f"^(({allowed_chars}*|{allowed_chars}+)*/*)*$", re.IGNORECASE + ) + if not directory_re.match(v): + raise ValueError( + f"'{v}' is not valid. Value must be a valid path or macro." ) - if not directory_re.match(v): - raise ValueError( - f" '{v}' is not valid. Value must be a valid path or macro." - ) return v @field_validator("positioninpuck", mode="before") @@ -251,110 +266,109 @@ class SpreadsheetModel(BaseModel): raise ValueError(f" '{v}' is not valid." f"Value must be one of {allowed}.") return v - @field_validator("spacegroupnumber", mode="before") - @classmethod - def spacegroupnumber_allowed(cls, v): - if v is not None: - try: - v = int(v) - if not (1 <= v <= 230): - raise ValueError( - f" '{v}' is not valid." - f"Value must be an integer between 1 and 230." - ) - except (ValueError, TypeError) as e: + @field_validator("spacegroupnumber", mode="before") + @classmethod + def spacegroupnumber_allowed(cls, v): + if v is not None: + try: + v = int(v) + if not (1 <= v <= 230): raise ValueError( f" '{v}' is not valid." f"Value must be an integer between 1 and 230." - ) from e - return v - - @field_validator("cellparameters", mode="before") - @classmethod - def cellparameters_format(cls, v): - if v: - values = [float(i) for i in v.split(",")] - if len(values) != 6 or any(val <= 0 for val in values): - raise ValueError( - f" '{v}' is not valid." - f"Value must be a set of six positive floats or integers." ) - return v + except (ValueError, TypeError) as e: + raise ValueError( + f" '{v}' is not valid." + f"Value must be an integer between 1 and 230." + ) from e + return v - @field_validator("rescutkey", "rescutvalue", mode="before") - @classmethod - def rescutkey_value_pair(cls, values): - rescutkey = values.get("rescutkey") - rescutvalue = values.get("rescutvalue") - if rescutkey and rescutvalue: - if rescutkey not in {"is", "cchalf"}: - raise ValueError("Rescutkey must be either 'is' or 'cchalf'") - if not isinstance(rescutvalue, float) or rescutvalue <= 0: - raise ValueError( - "Rescutvalue must be a positive float if rescutkey is provided" - ) - return values + @field_validator("cellparameters", mode="before") + @classmethod + def cellparameters_format(cls, v): + if v: + values = [float(i) for i in v.split(",")] + if len(values) != 6 or any(val <= 0 for val in values): + raise ValueError( + f" '{v}' is not valid." + f"Value must be a set of six positive floats or integers." + ) + return v - @field_validator("trustedhigh", mode="before") - @classmethod - def trustedhigh_allowed(cls, v): - if v is not None: - try: - v = float(v) - if not (0 <= v <= 2.0): - raise ValueError( - f" '{v}' is not valid." - f"Value must be a float between 0 and 2.0." - ) - except (ValueError, TypeError) as e: + # @field_validator("rescutkey", "rescutvalue", mode="before") + # @classmethod + # def rescutkey_value_pair(cls, values): + # rescutkey = values.get("rescutkey") + # rescutvalue = values.get("rescutvalue") + # if rescutkey and rescutvalue: + # if rescutkey not in {"is", "cchalf"}: + # raise ValueError("Rescutkey must be either 'is' or 'cchalf'") + # if not isinstance(rescutvalue, float) or rescutvalue <= 0: + # raise ValueError( + # "Rescutvalue must be a positive float if rescutkey is provided" + # ) + # return values + + @field_validator("trustedhigh", mode="before") + @classmethod + def trustedhigh_allowed(cls, v): + if v is not None: + try: + v = float(v) + if not (0 <= v <= 2.0): raise ValueError( f" '{v}' is not valid." f"Value must be a float between 0 and 2.0." - ) from e - return v + ) + except (ValueError, TypeError) as e: + raise ValueError( + f" '{v}' is not valid." f"Value must be a float between 0 and 2.0." + ) from e + return v - @field_validator("chiphiangles", mode="before") - @classmethod - def chiphiangles_allowed(cls, v): - if v is not None: - try: - v = float(v) - if not (0 <= v <= 30): - raise ValueError( - f" '{v}' is not valid." - f"Value must be a float between 0 and 30." - ) - except (ValueError, TypeError) as e: + @field_validator("chiphiangles", mode="before") + @classmethod + def chiphiangles_allowed(cls, v): + if v is not None: + try: + v = float(v) + if not (0 <= v <= 30): raise ValueError( - f" '{v}' is not valid. Value must be a float between 0 and 30." - ) from e - return v + f" '{v}' is not valid." + f"Value must be a float between 0 and 30." + ) + except (ValueError, TypeError) as e: + raise ValueError( + f" '{v}' is not valid. Value must be a float between 0 and 30." + ) from e + return v - @field_validator("dose", mode="before") - @classmethod - def dose_positive(cls, v): - if v is not None: - try: - v = float(v) - if v <= 0: - raise ValueError( - f" '{v}' is not valid. Value must be a positive float." - ) - except (ValueError, TypeError) as e: + @field_validator("dose", mode="before") + @classmethod + def dose_positive(cls, v): + if v is not None: + try: + v = float(v) + if v <= 0: raise ValueError( f" '{v}' is not valid. Value must be a positive float." - ) from e - return v + ) + except (ValueError, TypeError) as e: + raise ValueError( + f" '{v}' is not valid. Value must be a positive float." + ) from e + return v - class TELLModel(SpreadsheetModel): - input_order: int - samplemountcount: int = 0 - samplestatus: str = "not present" - puckaddress: str = "---" - username: str - puck_number: int - prefix: Optional[str] - folder: Optional[str] + # class TELLModel(SpreadsheetModel): + # input_order: int + # samplemountcount: int = 0 + # samplestatus: str = "not present" + # puckaddress: str = "---" + # username: str + # puck_number: int + # prefix: Optional[str] + # folder: Optional[str] class SpreadsheetResponse(BaseModel): diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 0e38c70..c4da527 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -150,14 +150,20 @@ class Sample(BaseModel): position: int # Position within the puck puck_id: int crystalname: Optional[str] = Field(None) + proteinname: Optional[str] = None positioninpuck: Optional[int] = Field(None) + priority: Optional[int] = None + comments: Optional[str] = None events: List[SampleEventCreate] = [] class SampleCreate(BaseModel): sample_name: str = Field(..., alias="crystalname") + proteinname: Optional[str] = None position: int = Field(..., alias="positioninpuck") data_collection_parameters: Optional[DataCollectionParameters] = None + priority: Optional[int] = None + comments: Optional[str] = None results: Optional[Results] = None events: Optional[List[str]] = None diff --git a/backend/app/services/shipment_processor.py b/backend/app/services/shipment_processor.py index 93c3e7c..4a26b61 100644 --- a/backend/app/services/shipment_processor.py +++ b/backend/app/services/shipment_processor.py @@ -1,5 +1,4 @@ # Adjusting the ShipmentProcessor for better error handling and alignment - from sqlalchemy.orm import Session from app.models import Shipment, Dewar, Puck, Sample, DataCollectionParameters from app.schemas import ShipmentCreate, ShipmentResponse @@ -47,14 +46,17 @@ class ShipmentProcessor: self.db.refresh(puck) for sample_data in puck_data.samples: - data_collection_params = DataCollectionParameters( + data_collection_parameters = DataCollectionParameters( **sample_data.data_collection_parameters.dict(by_alias=True) ) sample = Sample( puck_id=puck.id, sample_name=sample_data.sample_name, + proteinname=sample_data.proteinname, position=sample_data.position, - data_collection_parameters=data_collection_params, + priority=sample_data.priority, + comments=sample_data.comments, + data_collection_parameters=data_collection_parameters, ) self.db.add(sample) self.db.commit() diff --git a/backend/app/services/spreadsheet_service.py b/backend/app/services/spreadsheet_service.py index 7df4c97..3cc6936 100644 --- a/backend/app/services/spreadsheet_service.py +++ b/backend/app/services/spreadsheet_service.py @@ -48,17 +48,6 @@ class SampleSpreadsheetImporter: def import_spreadsheet(self, file): return self.import_spreadsheet_with_errors(file) - def get_expected_type(self, col_name): - type_mapping = { - "dewarname": str, - "puckname": str, - "positioninpuck": int, - "priority": int, - "oscillation": float, - # Add all other mappings based on model requirements - } - return type_mapping.get(col_name, str) # Default to `str` - def import_spreadsheet_with_errors( self, file ) -> Tuple[List[SpreadsheetModel], List[dict], List[dict], List[str]]: @@ -194,6 +183,20 @@ class SampleSpreadsheetImporter: try: validated_record = SpreadsheetModel(**record) + # Update the raw data with assigned default values + if ( + validated_record.directory == "{sgPuck}/{sgPosition}" + and row[7] is None + ): + row_list = list(row) + row_list[ + 7 + ] = validated_record.directory # Set the field to the default value + raw_data[-1]["data"] = row_list + raw_data[-1][ + "default_set" + ] = True # Mark this row as having a default value assigned + model.append(validated_record) logger.debug(f"Row {index + 4} processed and validated successfully") except ValidationError as e: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7454672..81d1558 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -21,6 +21,7 @@ "@mui/icons-material": "^6.1.5", "@mui/material": "^6.1.6", "@mui/system": "^6.1.6", + "@mui/x-data-grid": "^7.23.3", "axios": "^1.7.7", "chokidar": "^4.0.1", "dayjs": "^1.11.13", @@ -1632,6 +1633,63 @@ } } }, + "node_modules/@mui/x-data-grid": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.23.3.tgz", + "integrity": "sha512-EiM5kut6N/0o0iEYx8A7M3fJqknAa1kcPvGhlX3hH50ERLDeuJaqoKzvRYLBbYKWydHIc+0hHIFcK5oQTXLenw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@mui/utils": "^5.16.6 || ^6.0.0", + "@mui/x-internals": "7.23.0", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "reselect": "^5.1.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0", + "@mui/system": "^5.15.14 || ^6.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/x-data-grid/node_modules/@mui/x-internals": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.23.0.tgz", + "integrity": "sha512-bPclKpqUiJYIHqmTxSzMVZi6MH51cQsn5U+8jskaTlo3J4QiMeCYJn/gn7YbeR9GOZFp8hetyHjoQoVHKRXCig==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@mui/utils": "^5.16.6 || ^6.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@mui/x-date-pickers": { "version": "7.22.2", "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.22.2.tgz", @@ -5829,6 +5887,12 @@ "node": ">=0.10.0" } }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", diff --git a/frontend/package.json b/frontend/package.json index ac0fa28..2d5a8f2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -27,6 +27,7 @@ "@mui/icons-material": "^6.1.5", "@mui/material": "^6.1.6", "@mui/system": "^6.1.6", + "@mui/x-data-grid": "^7.23.3", "axios": "^1.7.7", "chokidar": "^4.0.1", "dayjs": "^1.11.13", diff --git a/frontend/src/components/DewarDetails.tsx b/frontend/src/components/DewarDetails.tsx index c02b3bb..2ed3359 100644 --- a/frontend/src/components/DewarDetails.tsx +++ b/frontend/src/components/DewarDetails.tsx @@ -13,6 +13,9 @@ import { Tooltip, Alert, } from '@mui/material'; +import SampleSpreadsheet from '../components/SampleSpreadsheetGrid'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ExpandLessIcon from '@mui/icons-material/ExpandLess'; import QRCode from 'react-qr-code'; import { @@ -101,7 +104,10 @@ const DewarDetails: React.FC = ({ const [isQRCodeGenerated, setIsQRCodeGenerated] = useState(false); const [qrCodeValue, setQrCodeValue] = useState(dewar.unique_id || ''); const qrCodeRef = useRef(null); // - + const [showSpreadsheet, setShowSpreadsheet] = useState(false); + const toggleSpreadsheet = () => { + setShowSpreadsheet((prev) => !prev); + }; useEffect(() => { const fetchDewarTypes = async () => { try { @@ -705,8 +711,25 @@ const DewarDetails: React.FC = ({ > Save Changes + {/* Toggle Button for Spreadsheet */} + + + {showSpreadsheet ? : } + + + {/* Conditionally Render the SampleSpreadsheet component */} + {showSpreadsheet && ( + + Sample Spreadsheet + {/* Ensure dewar.id is passed */} + + )} + = ({ dewarId }) => { + const [rows, setRows] = useState([]); // Rows for the grid + const [columns, setColumns] = useState([]); // Columns for the grid + + // Fetch rows and columns related to the given Dewar ID + useEffect(() => { + if (!dewarId) { + console.error("Invalid or missing Dewar ID"); + return; + } + + const fetchSamples = async () => { + try { + const response = await DewarsService.getDewarSamplesDewarsDewarsDewarIdSamplesGet(dewarId); + const dewarData = response; // Assume response is already in correct structure + + // Transform pucks and samples into rows for the grid + const allRows = []; + dewarData.pucks.forEach((puck: any) => { + puck.samples.forEach((sample: any) => { + allRows.push({ + id: sample.id, + puckId: puck.id, + puckName: puck.name, + puckType: puck.type, + dewarName: dewarData.dewar.dewar_name, + position: sample.position, + crystalName: sample.sample_name, + proteinName: sample.proteinname, + priority: sample.priority, + comments: sample.comments, + }); + }); + }); + setRows(allRows); + + // Define table columns if not already set + setColumns([ + { field: "dewarName", headerName: "Dewar Name", width: 150, editable: false }, // Display Dewar Name + { field: "puckName", headerName: "Puck Name", width: 150 }, + { field: "puckType", headerName: "Puck Type", width: 150 }, + { field: "crystalName", headerName: "Crystal Name", width: 200, editable: true }, + { field: "proteinName", headerName: "Protein Name", width: 200, editable: true }, + { field: "position", headerName: "Position", width: 100, editable: true , type: "number"}, + { field: "priority", headerName: "Priority", width: 100, editable: true, type: "number" }, + { field: "comments", headerName: "Comments", width: 300, editable: true }, + ]); + } catch (error) { + console.error("Error fetching dewar samples:", error); + } +}; + + fetchSamples(); + }, [dewarId]); + + // Handle cell editing to persist changes to the backend + const handleCellEditStop = async (params: GridCellEditStopParams) => { + const { id, field, value } = params; + try { + // Example: Replace with a proper OpenAPI call if available + await DewarsService.updateSampleData(id as number, { [field]: value }); // Assuming this exists + console.log("Updated successfully"); + } catch (error) { + console.error("Error saving data:", error); + } + }; + + return ( +
+ +
+ ); +}; + +export default SampleSpreadsheet; \ No newline at end of file diff --git a/frontend/src/components/SpreadsheetTable.tsx b/frontend/src/components/SpreadsheetTable.tsx index 017dbc9..7d57e48 100644 --- a/frontend/src/components/SpreadsheetTable.tsx +++ b/frontend/src/components/SpreadsheetTable.tsx @@ -206,7 +206,10 @@ const SpreadsheetTable = ({ const puckNameIdx = fieldToCol['puckname']; const puckTypeIdx = fieldToCol['pucktype']; const sampleNameIdx = fieldToCol['crystalname']; + const proteinNameIdx = fieldToCol['proteinname']; const samplePositionIdx = fieldToCol['positioninpuck']; + const priorityIdx = fieldToCol['priority']; + const commentsIdx = fieldToCol['comments']; for (let rowIndex = 0; rowIndex < data.length; rowIndex++) { const row = data[rowIndex]; @@ -220,7 +223,10 @@ const SpreadsheetTable = ({ const puckName = row.data[puckNameIdx] !== undefined && row.data[puckNameIdx] !== null ? String(row.data[puckNameIdx]).trim() : null; const puckType = typeof row.data[puckTypeIdx] === 'string' ? row.data[puckTypeIdx] : 'Unipuck'; const sampleName = typeof row.data[sampleNameIdx] === 'string' ? row.data[sampleNameIdx].trim() : null; + const proteinName = typeof row.data[proteinNameIdx] === 'string' ? row.data[proteinNameIdx].trim() : null; const samplePosition = row.data[samplePositionIdx] !== undefined && row.data[samplePositionIdx] !== null ? Number(row.data[samplePositionIdx]) : null; + const priority = row?.data?.[priorityIdx] ? Number(row.data[priorityIdx]) : null; + const comments = typeof row.data[commentsIdx] === 'string' ? row.data[commentsIdx].trim() : null; if (dewarName && puckName) { let dewar; @@ -263,7 +269,11 @@ const SpreadsheetTable = ({ const sample = { sample_name: sampleName, + proteinname: proteinName, position: samplePosition, + priority: priority, + comments: comments, + data_collection_parameters: null, results: null // Placeholder for results field }; @@ -425,14 +435,28 @@ const SpreadsheetTable = ({ const cellValue = (row.data && row.data[colIndex]) || ""; const editingValue = editingCell[`${rowIndex}-${colIndex}`]; const isReadonly = !isInvalid; - + + const isDefaultAssigned = colIndex === 7 && row.default_set; // Directory column (index 7) and marked as default_set + return ( - + {isInvalid ? ( setEditingCell({ ...editingCell, [`${rowIndex}-${colIndex}`]: e.target.value })} + onChange={(e) => + setEditingCell({ + ...editingCell, + [`${rowIndex}-${colIndex}`]: e.target.value, + }) + } onKeyDown={(e) => { if (e.key === "Enter") { handleCellEdit(rowIndex, colIndex); diff --git a/frontend/src/components/UploadDialog.tsx b/frontend/src/components/UploadDialog.tsx index b80628c..8e8690e 100644 --- a/frontend/src/components/UploadDialog.tsx +++ b/frontend/src/components/UploadDialog.tsx @@ -98,6 +98,11 @@ const UploadDialog: React.FC = ({ open, onClose, selectedShip } }; + // Count rows with directory defaulted + const defaultDirectoryCount = fileSummary?.raw_data + ? fileSummary.raw_data.filter((row) => row.default_set).length + : 0; + return ( <> @@ -155,6 +160,11 @@ const UploadDialog: React.FC = ({ open, onClose, selectedShip The file is validated successfully with no errors. )} + + {defaultDirectoryCount > 0 + ? `${defaultDirectoryCount} rows had the "directory" field auto-assigned to the default value "{sgPuck}/{sgPosition}". These rows are highlighted in green.` + : "No rows had default values assigned to the 'directory' field."} + Dewars: {fileSummary.dewars_count} Pucks: {fileSummary.pucks_count} Samples: {fileSummary.samples_count}