@morscherlab/mint-sdk 1.0.0-rc.4 → 1.0.0-rc.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/components/AppTopBar.navigation.test.d.ts +1 -0
- package/dist/__tests__/components/DoseCalculatorVolumeField.test.d.ts +1 -0
- package/dist/__tests__/components/PlateMapEditorToolbarInternal.test.d.ts +1 -0
- package/dist/__tests__/components/PluginWorkspaceView.controls.test.d.ts +1 -0
- package/dist/__tests__/components/PluginWorkspaceView.navigation.test.d.ts +1 -0
- package/dist/__tests__/components/PluginWorkspaceView.shell.test.d.ts +1 -0
- package/dist/__tests__/components/ProtocolStep.presentation.test.d.ts +1 -0
- package/dist/__tests__/components/ProtocolStepEditor.state.test.d.ts +1 -0
- package/dist/__tests__/components/ProtocolStepParameterField.test.d.ts +1 -0
- package/dist/__tests__/components/ReagentList.presentation.test.d.ts +1 -0
- package/dist/__tests__/components/SampleSelector.colors.test.d.ts +1 -0
- package/dist/__tests__/components/SampleSelector.drag.test.d.ts +1 -0
- package/dist/__tests__/components/SampleSelector.groups.test.d.ts +1 -0
- package/dist/__tests__/components/SampleSelector.selection.test.d.ts +1 -0
- package/dist/__tests__/components/SampleSelectorSampleRow.test.d.ts +1 -0
- package/dist/__tests__/components/ScheduleCalendar.test.d.ts +1 -0
- package/dist/__tests__/components/SettingsModal.schema.test.d.ts +1 -0
- package/dist/__tests__/components/WellPlate.colors.test.d.ts +1 -0
- package/dist/__tests__/components/WellPlate.conditions.test.d.ts +1 -0
- package/dist/__tests__/components/WellPlate.geometry.test.d.ts +1 -0
- package/dist/__tests__/components/WellPlate.interaction.test.d.ts +1 -0
- package/dist/__tests__/components/WellPlate.legend.test.d.ts +1 -0
- package/dist/__tests__/components/WellPlate.rendering.test.d.ts +1 -0
- package/dist/__tests__/components/WellPlate.sampleDrop.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/classify.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/columns.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/compose.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/cooccurrence.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/fingerprint.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/integration.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/template.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/tokenize.test.d.ts +1 -0
- package/dist/__tests__/composables/useAutoGroupInputSources.test.d.ts +1 -0
- package/dist/__tests__/composables/useScheduleCalendarLayout.test.d.ts +1 -0
- package/dist/__tests__/docs/extractDocsComponents.test.d.ts +1 -0
- package/dist/__tests__/docs/extractDocsExports.test.d.ts +1 -0
- package/dist/__tests__/docs/extractDocsParsing.test.d.ts +1 -0
- package/dist/__tests__/docs/extractDocsTemplates.test.d.ts +1 -0
- package/dist/__tests__/docs/extractDocsTheme.test.d.ts +1 -0
- package/dist/components/AppSidebar.vue.d.ts +3 -3
- package/dist/components/AppTopBar.navigation.d.ts +11 -0
- package/dist/components/BaseButton.vue.d.ts +1 -1
- package/dist/components/BaseCheckbox.vue.d.ts +1 -1
- package/dist/components/BaseInput.vue.d.ts +2 -2
- package/dist/components/BasePill.vue.d.ts +1 -1
- package/dist/components/BaseRadioGroup.vue.d.ts +1 -1
- package/dist/components/BaseSelect.vue.d.ts +1 -1
- package/dist/components/BaseSlider.vue.d.ts +2 -2
- package/dist/components/BaseTextarea.vue.d.ts +2 -2
- package/dist/components/BaseToggle.vue.d.ts +1 -1
- package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +2 -2
- package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +1 -1
- package/dist/components/ColorSlider.vue.d.ts +2 -2
- package/dist/components/ConcentrationInput.vue.d.ts +2 -2
- package/dist/components/ControlWorkspaceView.vue.d.ts +3 -3
- package/dist/components/DatePicker.vue.d.ts +1 -1
- package/dist/components/DateTimePicker.vue.d.ts +2 -2
- package/dist/components/DoseCalculatorVolumeField.vue.d.ts +15 -0
- package/dist/components/DoseDesignWorkspaceView.vue.d.ts +1 -1
- package/dist/components/DropdownButton.vue.d.ts +1 -1
- package/dist/components/FileUploader.vue.d.ts +2 -2
- package/dist/components/FormulaInput.vue.d.ts +2 -2
- package/dist/components/IconButton.vue.d.ts +1 -1
- package/dist/components/LoadingSpinner.vue.d.ts +1 -1
- package/dist/components/MoleculeInput.vue.d.ts +2 -2
- package/dist/components/MultiSelect.vue.d.ts +1 -1
- package/dist/components/NumberInput.vue.d.ts +1 -1
- package/dist/components/PlateMapEditor.vue.d.ts +6 -6
- package/dist/components/PluginWorkspaceView.controls.d.ts +28 -0
- package/dist/components/PluginWorkspaceView.navigation.d.ts +29 -0
- package/dist/components/PluginWorkspaceView.props.d.ts +151 -0
- package/dist/components/PluginWorkspaceView.shell.d.ts +19 -0
- package/dist/components/PluginWorkspaceView.vue.d.ts +46 -195
- package/dist/components/ProgressBar.vue.d.ts +1 -1
- package/dist/components/ProtocolStep.presentation.d.ts +4 -0
- package/dist/components/ProtocolStepEditor.state.d.ts +18 -0
- package/dist/components/ProtocolStepParameterField.vue.d.ts +12 -0
- package/dist/components/ReagentList.presentation.d.ts +16 -0
- package/dist/components/ResourceCard.vue.d.ts +1 -1
- package/dist/components/SampleSelector.colors.d.ts +13 -0
- package/dist/components/SampleSelector.drag.d.ts +24 -0
- package/dist/components/SampleSelector.groups.d.ts +15 -0
- package/dist/components/SampleSelector.selection.d.ts +26 -0
- package/dist/components/SampleSelector.vue.d.ts +4 -1
- package/dist/components/SampleSelectorSampleRow.vue.d.ts +21 -0
- package/dist/components/SegmentedControl.vue.d.ts +1 -1
- package/dist/components/SequenceInput.vue.d.ts +2 -2
- package/dist/components/SequenceProgressBar.vue.d.ts +1 -1
- package/dist/components/SettingsModal.schema.d.ts +9 -0
- package/dist/components/StatusIndicator.vue.d.ts +1 -1
- package/dist/components/TagsInput.vue.d.ts +2 -2
- package/dist/components/TimePicker.vue.d.ts +2 -2
- package/dist/components/TimeRangeInput.vue.d.ts +1 -1
- package/dist/components/UnitInput.vue.d.ts +2 -2
- package/dist/components/WellPlate.colors.d.ts +9 -0
- package/dist/components/WellPlate.conditions.d.ts +26 -0
- package/dist/components/WellPlate.geometry.d.ts +23 -0
- package/dist/components/WellPlate.interaction.d.ts +71 -0
- package/dist/components/WellPlate.legend.d.ts +2 -0
- package/dist/components/WellPlate.rendering.d.ts +24 -0
- package/dist/components/WellPlate.sampleDrop.d.ts +8 -0
- package/dist/components/WellPlate.vue.d.ts +1 -1
- package/dist/components/index.js +2 -2
- package/dist/components/internal/ActionItemInternal.vue.d.ts +1 -1
- package/dist/components/internal/PlateMapEditorToolbarInternal.vue.d.ts +28 -0
- package/dist/{components-DafPc4rM.js → components-DtHA2bgp.js} +3754 -2991
- package/dist/components-DtHA2bgp.js.map +1 -0
- package/dist/composables/autoGroup/classKey.d.ts +4 -0
- package/dist/composables/autoGroup/classify.d.ts +28 -0
- package/dist/composables/autoGroup/colors.d.ts +2 -0
- package/dist/composables/autoGroup/columns.d.ts +10 -0
- package/dist/composables/autoGroup/compose.d.ts +8 -0
- package/dist/composables/autoGroup/cooccurrence.d.ts +2 -0
- package/dist/composables/autoGroup/csv-shim.d.ts +2 -0
- package/dist/composables/autoGroup/fingerprint.d.ts +3 -0
- package/dist/composables/autoGroup/index.d.ts +16 -0
- package/dist/composables/autoGroup/replicatePreGroup.d.ts +38 -0
- package/dist/composables/autoGroup/template.d.ts +15 -0
- package/dist/composables/autoGroup/tokenize.d.ts +8 -0
- package/dist/composables/autoGroupConstants.d.ts +1 -0
- package/dist/composables/autoGroupGrouping.d.ts +3 -0
- package/dist/composables/controlComponentBindings.d.ts +1 -1
- package/dist/composables/controlSchemaAdapters.d.ts +1 -1
- package/dist/composables/controlSchemaDoseDesign.d.ts +11 -0
- package/dist/composables/controlSchemaLayout.d.ts +1 -1
- package/dist/composables/controlSchemaModel.d.ts +5 -0
- package/dist/composables/controlSchemaNormalize.d.ts +1 -1
- package/dist/composables/controlSchemaTypes.d.ts +305 -0
- package/dist/composables/controlWorkspaceOptions.d.ts +1 -1
- package/dist/composables/formBuilderSchema.d.ts +18 -0
- package/dist/composables/index.js +3 -3
- package/dist/composables/pluginEndpointBuilder.d.ts +13 -0
- package/dist/composables/protocolTemplateCatalog.d.ts +26 -0
- package/dist/composables/useAutoGroup.d.ts +61 -74
- package/dist/composables/useAutoGroupInputSources.d.ts +32 -0
- package/dist/composables/useBioTemplateControls.d.ts +1 -1
- package/dist/composables/useBioTemplatePresetWorkspace.d.ts +1 -1
- package/dist/composables/useBioTemplateWorkspace.d.ts +1 -1
- package/dist/composables/useControlSchema.d.ts +4 -316
- package/dist/composables/useControlWorkspace.d.ts +1 -1
- package/dist/composables/useForm.d.ts +2 -33
- package/dist/composables/useFormBuilder.d.ts +2 -9
- package/dist/composables/useFormValidation.d.ts +34 -0
- package/dist/composables/usePluginClient.d.ts +1 -4
- package/dist/composables/useProtocolTemplates.d.ts +2 -24
- package/dist/composables/useScheduleCalendarLayout.d.ts +49 -0
- package/dist/{composables-BMkPQhVK.js → composables-Dlg8jenH.js} +33 -31
- package/dist/composables-Dlg8jenH.js.map +1 -0
- package/dist/index.js +4 -4
- package/dist/install.js +2 -2
- package/dist/styles.css +547 -516
- package/dist/templates/controlSchemaTypes.d.ts +1 -1
- package/dist/templates/index.js +1 -1
- package/dist/templates/templateAdapterTypes.d.ts +48 -0
- package/dist/templates/templateCreateOptions.d.ts +165 -0
- package/dist/templates/templateQpcrTypes.d.ts +42 -0
- package/dist/templates/types.d.ts +5 -250
- package/dist/{templates-bUAWMn5L.js → templates-DtdUvJ4c.js} +144 -136
- package/dist/templates-DtdUvJ4c.js.map +1 -0
- package/dist/types/auto-group.d.ts +79 -9
- package/dist/types/componentLabTypes.d.ts +161 -0
- package/dist/types/componentWorkflowTypes.d.ts +150 -0
- package/dist/types/components.d.ts +2 -311
- package/dist/{useProtocolTemplates-QZtHFFH2.js → useProtocolTemplates-Bm5vyH4_.js} +1220 -454
- package/dist/useProtocolTemplates-Bm5vyH4_.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/components/AppTopBar.navigation.test.ts +70 -0
- package/src/__tests__/components/DoseCalculatorVolumeField.test.ts +53 -0
- package/src/__tests__/components/PlateMapEditorToolbarInternal.test.ts +54 -0
- package/src/__tests__/components/PluginWorkspaceView.controls.test.ts +156 -0
- package/src/__tests__/components/PluginWorkspaceView.navigation.test.ts +102 -0
- package/src/__tests__/components/PluginWorkspaceView.shell.test.ts +41 -0
- package/src/__tests__/components/ProtocolStep.presentation.test.ts +31 -0
- package/src/__tests__/components/ProtocolStepEditor.state.test.ts +165 -0
- package/src/__tests__/components/ProtocolStepParameterField.test.ts +44 -0
- package/src/__tests__/components/ReagentList.presentation.test.ts +68 -0
- package/src/__tests__/components/SampleSelector.colors.test.ts +49 -0
- package/src/__tests__/components/SampleSelector.drag.test.ts +100 -0
- package/src/__tests__/components/SampleSelector.groups.test.ts +81 -0
- package/src/__tests__/components/SampleSelector.selection.test.ts +70 -0
- package/src/__tests__/components/SampleSelector.test.ts +32 -0
- package/src/__tests__/components/SampleSelectorSampleRow.test.ts +37 -0
- package/src/__tests__/components/ScheduleCalendar.test.ts +44 -0
- package/src/__tests__/components/SettingsModal.schema.test.ts +97 -0
- package/src/__tests__/components/WellPlate.colors.test.ts +28 -0
- package/src/__tests__/components/WellPlate.conditions.test.ts +68 -0
- package/src/__tests__/components/WellPlate.geometry.test.ts +54 -0
- package/src/__tests__/components/WellPlate.interaction.test.ts +171 -0
- package/src/__tests__/components/WellPlate.legend.test.ts +13 -0
- package/src/__tests__/components/WellPlate.rendering.test.ts +122 -0
- package/src/__tests__/components/WellPlate.sampleDrop.test.ts +70 -0
- package/src/__tests__/composables/autoGroup/classify.test.ts +107 -0
- package/src/__tests__/composables/autoGroup/columns.test.ts +135 -0
- package/src/__tests__/composables/autoGroup/compose.test.ts +227 -0
- package/src/__tests__/composables/autoGroup/cooccurrence.test.ts +91 -0
- package/src/__tests__/composables/autoGroup/fingerprint.test.ts +50 -0
- package/src/__tests__/composables/autoGroup/integration.test.ts +79 -0
- package/src/__tests__/composables/autoGroup/template.test.ts +70 -0
- package/src/__tests__/composables/autoGroup/tokenize.test.ts +33 -0
- package/src/__tests__/composables/useAutoGroup.test.ts +129 -625
- package/src/__tests__/composables/useAutoGroupInputSources.test.ts +107 -0
- package/src/__tests__/composables/useControlSchema.test.ts +23 -0
- package/src/__tests__/composables/useScheduleCalendarLayout.test.ts +89 -0
- package/src/__tests__/docs/extractDocsComponents.test.ts +142 -0
- package/src/__tests__/docs/extractDocsExports.test.ts +77 -0
- package/src/__tests__/docs/extractDocsParsing.test.ts +69 -0
- package/src/__tests__/docs/extractDocsTemplates.test.ts +54 -0
- package/src/__tests__/docs/extractDocsTheme.test.ts +89 -0
- package/src/__tests__/docs/frontendDocsCatalog.test.ts +1 -1
- package/src/__tests__/fixtures/auto-group/mixed-lc-ms-batch.txt +187 -0
- package/src/components/AppSidebar.vue +2 -6
- package/src/components/AppTopBar.navigation.ts +62 -0
- package/src/components/AppTopBar.vue +17 -44
- package/src/components/AutoGroupModal.story.vue +50 -0
- package/src/components/AutoGroupModal.vue +441 -158
- package/src/components/ControlWorkspaceView.vue +2 -6
- package/src/components/DoseCalculator.vue +13 -73
- package/src/components/DoseCalculatorVolumeField.vue +61 -0
- package/src/components/ExperimentTimeline.vue +6 -31
- package/src/components/FormBuilder.vue +2 -7
- package/src/components/PlateMapEditor.vue +32 -106
- package/src/components/PluginWorkspaceView.controls.ts +182 -0
- package/src/components/PluginWorkspaceView.navigation.ts +106 -0
- package/src/components/PluginWorkspaceView.props.ts +174 -0
- package/src/components/PluginWorkspaceView.shell.ts +66 -0
- package/src/components/PluginWorkspaceView.vue +85 -404
- package/src/components/ProtocolStep.presentation.ts +31 -0
- package/src/components/ProtocolStepEditor.state.ts +104 -0
- package/src/components/ProtocolStepEditor.vue +48 -179
- package/src/components/ProtocolStepParameterField.vue +134 -0
- package/src/components/ReagentList.presentation.ts +105 -0
- package/src/components/ReagentList.vue +16 -79
- package/src/components/SampleSelector.colors.ts +43 -0
- package/src/components/SampleSelector.drag.ts +164 -0
- package/src/components/SampleSelector.groups.ts +109 -0
- package/src/components/SampleSelector.selection.ts +103 -0
- package/src/components/SampleSelector.vue +82 -349
- package/src/components/SampleSelectorSampleRow.vue +64 -0
- package/src/components/ScheduleCalendar.vue +44 -199
- package/src/components/SettingsModal.schema.ts +71 -0
- package/src/components/SettingsModal.vue +16 -46
- package/src/components/WellPlate.colors.ts +56 -0
- package/src/components/WellPlate.conditions.ts +100 -0
- package/src/components/WellPlate.geometry.ts +91 -0
- package/src/components/WellPlate.interaction.ts +272 -0
- package/src/components/WellPlate.legend.ts +8 -0
- package/src/components/WellPlate.rendering.ts +105 -0
- package/src/components/WellPlate.sampleDrop.ts +73 -0
- package/src/components/WellPlate.vue +102 -550
- package/src/components/internal/PlateMapEditorToolbarInternal.vue +128 -0
- package/src/composables/autoGroup/classKey.ts +5 -0
- package/src/composables/autoGroup/classify.ts +205 -0
- package/src/composables/autoGroup/colors.ts +6 -0
- package/src/composables/autoGroup/columns.ts +226 -0
- package/src/composables/autoGroup/compose.ts +156 -0
- package/src/composables/autoGroup/cooccurrence.ts +46 -0
- package/src/composables/autoGroup/csv-shim.ts +44 -0
- package/src/composables/autoGroup/fingerprint.ts +49 -0
- package/src/composables/autoGroup/index.ts +20 -0
- package/src/composables/autoGroup/replicatePreGroup.ts +90 -0
- package/src/composables/autoGroup/template.ts +126 -0
- package/src/composables/autoGroup/tokenize.ts +41 -0
- package/src/composables/autoGroup/vocab.json +67 -0
- package/src/composables/autoGroupConstants.ts +4 -0
- package/src/composables/autoGroupGrouping.ts +148 -0
- package/src/composables/controlComponentBindings.ts +1 -1
- package/src/composables/controlSchemaAdapters.ts +1 -1
- package/src/composables/controlSchemaDoseDesign.ts +215 -0
- package/src/composables/controlSchemaFormFields.ts +1 -1
- package/src/composables/controlSchemaLayout.ts +1 -1
- package/src/composables/controlSchemaModel.ts +163 -0
- package/src/composables/controlSchemaNormalize.ts +1 -1
- package/src/composables/controlSchemaTypes.ts +364 -0
- package/src/composables/controlWorkspaceOptions.ts +1 -1
- package/src/composables/formBuilderSchema.ts +153 -0
- package/src/composables/pluginEndpointBuilder.ts +203 -0
- package/src/composables/protocolTemplateCatalog.ts +325 -0
- package/src/composables/useAutoGroup.ts +395 -549
- package/src/composables/useAutoGroupInputSources.ts +147 -0
- package/src/composables/useBioTemplateControls.ts +1 -1
- package/src/composables/useBioTemplatePresetWorkspace.ts +1 -1
- package/src/composables/useBioTemplateWorkspace.ts +1 -1
- package/src/composables/useControlSchema.ts +21 -692
- package/src/composables/useControlWorkspace.ts +7 -13
- package/src/composables/useForm.ts +5 -187
- package/src/composables/useFormBuilder.ts +11 -153
- package/src/composables/useFormValidation.ts +154 -0
- package/src/composables/usePluginClient.ts +10 -193
- package/src/composables/useProtocolTemplates.ts +10 -328
- package/src/composables/useScheduleCalendarLayout.ts +287 -0
- package/src/styles/components/auto-group-modal.css +248 -310
- package/src/templates/controlSchemaTypes.ts +1 -1
- package/src/templates/templateAdapterTypes.ts +58 -0
- package/src/templates/templateCreateOptions.ts +208 -0
- package/src/templates/templateQpcrTypes.ts +48 -0
- package/src/templates/types.ts +79 -275
- package/src/types/auto-group.ts +107 -9
- package/src/types/componentLabTypes.ts +235 -0
- package/src/types/componentWorkflowTypes.ts +190 -0
- package/src/types/components.ts +74 -424
- package/dist/components-DafPc4rM.js.map +0 -1
- package/dist/composables-BMkPQhVK.js.map +0 -1
- package/dist/templates-bUAWMn5L.js.map +0 -1
- package/dist/useProtocolTemplates-QZtHFFH2.js.map +0 -1
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/** Form for creating or editing a single protocol step (incubation, wash, addition, centrifuge, etc.) with template picker, typed parameters, and duration. */
|
|
3
3
|
import { ref, computed, watch, onMounted } from 'vue'
|
|
4
|
-
import type { ProtocolStep
|
|
4
|
+
import type { ProtocolStep } from '../types'
|
|
5
5
|
import { useDropdownState } from '../composables/useDropdownState'
|
|
6
6
|
import {
|
|
7
7
|
useProtocolTemplates,
|
|
8
8
|
type StepTemplate,
|
|
9
9
|
} from '../composables/useProtocolTemplates'
|
|
10
|
-
import
|
|
11
|
-
|
|
10
|
+
import {
|
|
11
|
+
PROTOCOL_STEP_TYPE_ICONS as stepTypeIcons,
|
|
12
|
+
formatProtocolDuration as formatDuration,
|
|
13
|
+
} from './ProtocolStep.presentation'
|
|
14
|
+
import ProtocolStepParameterField from './ProtocolStepParameterField.vue'
|
|
15
|
+
import {
|
|
16
|
+
buildCustomProtocolTemplate,
|
|
17
|
+
buildProtocolPreviewParams,
|
|
18
|
+
buildProtocolStepEditorState,
|
|
19
|
+
buildProtocolStepPreview,
|
|
20
|
+
buildProtocolTemplateDefaults,
|
|
21
|
+
type ProtocolStepEditorFields,
|
|
22
|
+
} from './ProtocolStepEditor.state'
|
|
12
23
|
|
|
13
24
|
interface Props {
|
|
14
25
|
modelValue?: ProtocolStep
|
|
@@ -69,51 +80,41 @@ const selectedTemplate = computed(() => {
|
|
|
69
80
|
return availableTemplates.value.find((t) => t.id === selectedTemplateId.value) || null
|
|
70
81
|
})
|
|
71
82
|
|
|
72
|
-
// Step type icons (same as ExperimentTimeline)
|
|
73
|
-
const stepTypeIcons: Record<ProtocolStepType, string> = {
|
|
74
|
-
incubation: 'M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z',
|
|
75
|
-
wash: 'M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z',
|
|
76
|
-
addition: 'M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z',
|
|
77
|
-
measurement: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z',
|
|
78
|
-
transfer: 'M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4',
|
|
79
|
-
centrifuge: 'M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15',
|
|
80
|
-
mix: 'M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z',
|
|
81
|
-
custom: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z',
|
|
82
|
-
}
|
|
83
|
-
|
|
84
83
|
// Preview computed
|
|
85
84
|
const previewStep = computed<ProtocolStep | null>(() => {
|
|
86
85
|
if (!selectedTemplate.value) return null
|
|
87
|
-
return
|
|
88
|
-
id: props.modelValue?.id || 'preview',
|
|
89
|
-
type: selectedTemplate.value.type,
|
|
90
|
-
name: stepName.value || selectedTemplate.value.name,
|
|
91
|
-
description: stepDescription.value,
|
|
92
|
-
duration: stepDuration.value ?? selectedTemplate.value.defaultDuration,
|
|
93
|
-
status: (props.modelValue?.status || 'pending') as ProtocolStepStatus,
|
|
94
|
-
parameters: { ...parameters.value },
|
|
95
|
-
order: props.modelValue?.order || 0,
|
|
96
|
-
}
|
|
86
|
+
return buildProtocolStepPreview(selectedTemplate.value, editorFields(), props.modelValue)
|
|
97
87
|
})
|
|
98
88
|
|
|
99
89
|
const previewParams = computed(() => {
|
|
100
90
|
if (!selectedTemplate.value || !previewStep.value) return ''
|
|
101
|
-
return
|
|
102
|
-
.
|
|
103
|
-
|
|
104
|
-
|
|
91
|
+
return buildProtocolPreviewParams(
|
|
92
|
+
selectedTemplate.value,
|
|
93
|
+
parameters.value,
|
|
94
|
+
formatParameterValue
|
|
95
|
+
)
|
|
105
96
|
})
|
|
106
97
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
98
|
+
function editorFields(): ProtocolStepEditorFields {
|
|
99
|
+
return {
|
|
100
|
+
name: stepName.value,
|
|
101
|
+
description: stepDescription.value,
|
|
102
|
+
duration: stepDuration.value,
|
|
103
|
+
parameters: parameters.value,
|
|
112
104
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function applyEditorFields(fields: ProtocolStepEditorFields) {
|
|
108
|
+
stepName.value = fields.name
|
|
109
|
+
stepDescription.value = fields.description
|
|
110
|
+
stepDuration.value = fields.duration
|
|
111
|
+
parameters.value = { ...fields.parameters }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function initFromStep(step: ProtocolStep) {
|
|
115
|
+
const state = buildProtocolStepEditorState(step, getTemplateByType)
|
|
116
|
+
selectedTemplateId.value = state.selectedTemplateId
|
|
117
|
+
applyEditorFields(state.fields)
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
// Handle template selection
|
|
@@ -123,16 +124,7 @@ function selectTemplate(template: StepTemplate) {
|
|
|
123
124
|
|
|
124
125
|
// Reset to template defaults if creating new
|
|
125
126
|
if (props.mode === 'create') {
|
|
126
|
-
|
|
127
|
-
stepDescription.value = template.description || ''
|
|
128
|
-
stepDuration.value = template.defaultDuration
|
|
129
|
-
|
|
130
|
-
parameters.value = {}
|
|
131
|
-
for (const param of template.parameters) {
|
|
132
|
-
if (param.default !== undefined) {
|
|
133
|
-
parameters.value[param.key] = param.default
|
|
134
|
-
}
|
|
135
|
-
}
|
|
127
|
+
applyEditorFields(buildProtocolTemplateDefaults(template))
|
|
136
128
|
}
|
|
137
129
|
}
|
|
138
130
|
|
|
@@ -145,14 +137,6 @@ function handleParamChange(key: string, value: unknown) {
|
|
|
145
137
|
}
|
|
146
138
|
}
|
|
147
139
|
|
|
148
|
-
// Handle concentration change
|
|
149
|
-
function handleConcentrationChange(key: string, value: ConcentrationValue | undefined) {
|
|
150
|
-
parameters.value[key] = value
|
|
151
|
-
if (validationErrors.value[key]) {
|
|
152
|
-
delete validationErrors.value[key]
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
140
|
// Validate and save
|
|
157
141
|
function handleSave() {
|
|
158
142
|
if (!selectedTemplate.value || !previewStep.value) return
|
|
@@ -169,21 +153,7 @@ function handleSave() {
|
|
|
169
153
|
// Save as template
|
|
170
154
|
function handleSaveTemplate() {
|
|
171
155
|
if (!selectedTemplate.value) return
|
|
172
|
-
|
|
173
|
-
const newTemplate: StepTemplate = {
|
|
174
|
-
id: `custom-${Date.now()}`,
|
|
175
|
-
type: selectedTemplate.value.type,
|
|
176
|
-
name: stepName.value || selectedTemplate.value.name,
|
|
177
|
-
description: stepDescription.value,
|
|
178
|
-
defaultDuration: stepDuration.value,
|
|
179
|
-
parameters: selectedTemplate.value.parameters.map((p) => ({
|
|
180
|
-
...p,
|
|
181
|
-
default: parameters.value[p.key],
|
|
182
|
-
})),
|
|
183
|
-
isBuiltIn: false,
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
emit('save-template', newTemplate)
|
|
156
|
+
emit('save-template', buildCustomProtocolTemplate(selectedTemplate.value, editorFields()))
|
|
187
157
|
}
|
|
188
158
|
|
|
189
159
|
// Handle cancel
|
|
@@ -191,15 +161,6 @@ function handleCancel() {
|
|
|
191
161
|
emit('cancel')
|
|
192
162
|
}
|
|
193
163
|
|
|
194
|
-
// Format duration
|
|
195
|
-
function formatDuration(minutes: number | undefined): string {
|
|
196
|
-
if (minutes === undefined) return ''
|
|
197
|
-
if (minutes < 60) return `${minutes}m`
|
|
198
|
-
const hours = Math.floor(minutes / 60)
|
|
199
|
-
const mins = minutes % 60
|
|
200
|
-
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`
|
|
201
|
-
}
|
|
202
|
-
|
|
203
164
|
onMounted(() => {
|
|
204
165
|
if (props.modelValue) {
|
|
205
166
|
initFromStep(props.modelValue)
|
|
@@ -335,106 +296,14 @@ watch(
|
|
|
335
296
|
|
|
336
297
|
<!-- Dynamic parameters -->
|
|
337
298
|
<template v-if="selectedTemplate">
|
|
338
|
-
<
|
|
339
|
-
v-for="
|
|
299
|
+
<ProtocolStepParameterField
|
|
300
|
+
v-for="param in selectedTemplate.parameters"
|
|
340
301
|
:key="param.key"
|
|
341
|
-
:
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
]"
|
|
347
|
-
>
|
|
348
|
-
<label
|
|
349
|
-
:class="[
|
|
350
|
-
'mint-protocol-editor__field-label',
|
|
351
|
-
param.required ? 'mint-protocol-editor__field-label--required' : '',
|
|
352
|
-
]"
|
|
353
|
-
>
|
|
354
|
-
{{ param.label }}
|
|
355
|
-
</label>
|
|
356
|
-
|
|
357
|
-
<!-- Number input with unit -->
|
|
358
|
-
<div
|
|
359
|
-
v-if="param.type === 'number' || param.type === 'temperature' || param.type === 'duration'"
|
|
360
|
-
class="mint-protocol-editor__input-unit"
|
|
361
|
-
>
|
|
362
|
-
<input
|
|
363
|
-
type="number"
|
|
364
|
-
:value="parameters[param.key]"
|
|
365
|
-
:min="param.min"
|
|
366
|
-
:max="param.max"
|
|
367
|
-
:placeholder="param.placeholder || `Enter ${param.label.toLowerCase()}`"
|
|
368
|
-
:class="[
|
|
369
|
-
'mint-protocol-editor__input',
|
|
370
|
-
validationErrors[param.key] ? 'mint-protocol-editor__input--error' : '',
|
|
371
|
-
]"
|
|
372
|
-
@input="handleParamChange(param.key, ($event.target as HTMLInputElement).valueAsNumber)"
|
|
373
|
-
/>
|
|
374
|
-
<span v-if="param.unit" class="mint-protocol-editor__unit-suffix">
|
|
375
|
-
{{ param.unit }}
|
|
376
|
-
</span>
|
|
377
|
-
</div>
|
|
378
|
-
|
|
379
|
-
<!-- Text input -->
|
|
380
|
-
<input
|
|
381
|
-
v-else-if="param.type === 'text'"
|
|
382
|
-
type="text"
|
|
383
|
-
:value="parameters[param.key]"
|
|
384
|
-
:placeholder="param.placeholder || `Enter ${param.label.toLowerCase()}`"
|
|
385
|
-
:class="[
|
|
386
|
-
'mint-protocol-editor__input',
|
|
387
|
-
validationErrors[param.key] ? 'mint-protocol-editor__input--error' : '',
|
|
388
|
-
]"
|
|
389
|
-
@input="handleParamChange(param.key, ($event.target as HTMLInputElement).value)"
|
|
390
|
-
/>
|
|
391
|
-
|
|
392
|
-
<!-- Select -->
|
|
393
|
-
<select
|
|
394
|
-
v-else-if="param.type === 'select'"
|
|
395
|
-
:value="parameters[param.key]"
|
|
396
|
-
:class="[
|
|
397
|
-
'mint-protocol-editor__select',
|
|
398
|
-
validationErrors[param.key] ? 'mint-protocol-editor__input--error' : '',
|
|
399
|
-
]"
|
|
400
|
-
@change="handleParamChange(param.key, ($event.target as HTMLSelectElement).value)"
|
|
401
|
-
>
|
|
402
|
-
<option value="" disabled>Select {{ param.label.toLowerCase() }}</option>
|
|
403
|
-
<option
|
|
404
|
-
v-for="option in param.options"
|
|
405
|
-
:key="option.value"
|
|
406
|
-
:value="option.value"
|
|
407
|
-
>
|
|
408
|
-
{{ option.label }}
|
|
409
|
-
</option>
|
|
410
|
-
</select>
|
|
411
|
-
|
|
412
|
-
<!-- Concentration input -->
|
|
413
|
-
<ConcentrationInput
|
|
414
|
-
v-else-if="param.type === 'concentration'"
|
|
415
|
-
:model-value="parameters[param.key] as ConcentrationValue | undefined"
|
|
416
|
-
:error="!!validationErrors[param.key]"
|
|
417
|
-
size="md"
|
|
418
|
-
@update:model-value="handleConcentrationChange(param.key, $event)"
|
|
419
|
-
/>
|
|
420
|
-
|
|
421
|
-
<!-- Reagent input (text for now) -->
|
|
422
|
-
<input
|
|
423
|
-
v-else-if="param.type === 'reagent'"
|
|
424
|
-
type="text"
|
|
425
|
-
:value="parameters[param.key]"
|
|
426
|
-
:placeholder="param.placeholder || 'Enter reagent name'"
|
|
427
|
-
:class="[
|
|
428
|
-
'mint-protocol-editor__input',
|
|
429
|
-
validationErrors[param.key] ? 'mint-protocol-editor__input--error' : '',
|
|
430
|
-
]"
|
|
431
|
-
@input="handleParamChange(param.key, ($event.target as HTMLInputElement).value)"
|
|
432
|
-
/>
|
|
433
|
-
|
|
434
|
-
<span v-if="validationErrors[param.key]" class="mint-protocol-editor__field-error">
|
|
435
|
-
{{ validationErrors[param.key] }}
|
|
436
|
-
</span>
|
|
437
|
-
</div>
|
|
302
|
+
:parameter="param"
|
|
303
|
+
:model-value="parameters[param.key]"
|
|
304
|
+
:error="validationErrors[param.key]"
|
|
305
|
+
@update:model-value="handleParamChange(param.key, $event)"
|
|
306
|
+
/>
|
|
438
307
|
</template>
|
|
439
308
|
|
|
440
309
|
<!-- Description -->
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/** Renders one typed parameter control for ProtocolStepEditor. */
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
import type { ParameterDefinition } from '../composables/useProtocolTemplates'
|
|
5
|
+
import type { ConcentrationValue } from '../composables/useConcentrationUnits'
|
|
6
|
+
import ConcentrationInput from './ConcentrationInput.vue'
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
parameter: ParameterDefinition
|
|
10
|
+
modelValue?: unknown
|
|
11
|
+
error?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const props = defineProps<Props>()
|
|
15
|
+
|
|
16
|
+
const emit = defineEmits<{
|
|
17
|
+
'update:modelValue': [value: unknown]
|
|
18
|
+
}>()
|
|
19
|
+
|
|
20
|
+
const scalarValue = computed(() => (
|
|
21
|
+
props.modelValue as string | number | undefined
|
|
22
|
+
))
|
|
23
|
+
|
|
24
|
+
const concentrationValue = computed(() => (
|
|
25
|
+
props.modelValue as ConcentrationValue | undefined
|
|
26
|
+
))
|
|
27
|
+
|
|
28
|
+
const hasError = computed(() => !!props.error)
|
|
29
|
+
|
|
30
|
+
function handleNumberInput(event: Event) {
|
|
31
|
+
emit('update:modelValue', (event.target as HTMLInputElement).valueAsNumber)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function handleTextInput(event: Event) {
|
|
35
|
+
emit('update:modelValue', (event.target as HTMLInputElement).value)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function handleSelectChange(event: Event) {
|
|
39
|
+
emit('update:modelValue', (event.target as HTMLSelectElement).value)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function handleConcentrationChange(value: ConcentrationValue | undefined) {
|
|
43
|
+
emit('update:modelValue', value)
|
|
44
|
+
}
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<template>
|
|
48
|
+
<div class="mint-protocol-editor__field">
|
|
49
|
+
<label
|
|
50
|
+
:class="[
|
|
51
|
+
'mint-protocol-editor__field-label',
|
|
52
|
+
parameter.required ? 'mint-protocol-editor__field-label--required' : '',
|
|
53
|
+
]"
|
|
54
|
+
>
|
|
55
|
+
{{ parameter.label }}
|
|
56
|
+
</label>
|
|
57
|
+
|
|
58
|
+
<div
|
|
59
|
+
v-if="parameter.type === 'number' || parameter.type === 'temperature' || parameter.type === 'duration'"
|
|
60
|
+
class="mint-protocol-editor__input-unit"
|
|
61
|
+
>
|
|
62
|
+
<input
|
|
63
|
+
type="number"
|
|
64
|
+
:value="scalarValue"
|
|
65
|
+
:min="parameter.min"
|
|
66
|
+
:max="parameter.max"
|
|
67
|
+
:placeholder="parameter.placeholder || `Enter ${parameter.label.toLowerCase()}`"
|
|
68
|
+
:class="[
|
|
69
|
+
'mint-protocol-editor__input',
|
|
70
|
+
hasError ? 'mint-protocol-editor__input--error' : '',
|
|
71
|
+
]"
|
|
72
|
+
@input="handleNumberInput"
|
|
73
|
+
/>
|
|
74
|
+
<span v-if="parameter.unit" class="mint-protocol-editor__unit-suffix">
|
|
75
|
+
{{ parameter.unit }}
|
|
76
|
+
</span>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<input
|
|
80
|
+
v-else-if="parameter.type === 'text'"
|
|
81
|
+
type="text"
|
|
82
|
+
:value="scalarValue"
|
|
83
|
+
:placeholder="parameter.placeholder || `Enter ${parameter.label.toLowerCase()}`"
|
|
84
|
+
:class="[
|
|
85
|
+
'mint-protocol-editor__input',
|
|
86
|
+
hasError ? 'mint-protocol-editor__input--error' : '',
|
|
87
|
+
]"
|
|
88
|
+
@input="handleTextInput"
|
|
89
|
+
/>
|
|
90
|
+
|
|
91
|
+
<select
|
|
92
|
+
v-else-if="parameter.type === 'select'"
|
|
93
|
+
:value="scalarValue"
|
|
94
|
+
:class="[
|
|
95
|
+
'mint-protocol-editor__select',
|
|
96
|
+
hasError ? 'mint-protocol-editor__input--error' : '',
|
|
97
|
+
]"
|
|
98
|
+
@change="handleSelectChange"
|
|
99
|
+
>
|
|
100
|
+
<option value="" disabled>Select {{ parameter.label.toLowerCase() }}</option>
|
|
101
|
+
<option
|
|
102
|
+
v-for="option in parameter.options"
|
|
103
|
+
:key="option.value"
|
|
104
|
+
:value="option.value"
|
|
105
|
+
>
|
|
106
|
+
{{ option.label }}
|
|
107
|
+
</option>
|
|
108
|
+
</select>
|
|
109
|
+
|
|
110
|
+
<ConcentrationInput
|
|
111
|
+
v-else-if="parameter.type === 'concentration'"
|
|
112
|
+
:model-value="concentrationValue"
|
|
113
|
+
:error="hasError"
|
|
114
|
+
size="md"
|
|
115
|
+
@update:model-value="handleConcentrationChange"
|
|
116
|
+
/>
|
|
117
|
+
|
|
118
|
+
<input
|
|
119
|
+
v-else-if="parameter.type === 'reagent'"
|
|
120
|
+
type="text"
|
|
121
|
+
:value="scalarValue"
|
|
122
|
+
:placeholder="parameter.placeholder || 'Enter reagent name'"
|
|
123
|
+
:class="[
|
|
124
|
+
'mint-protocol-editor__input',
|
|
125
|
+
hasError ? 'mint-protocol-editor__input--error' : '',
|
|
126
|
+
]"
|
|
127
|
+
@input="handleTextInput"
|
|
128
|
+
/>
|
|
129
|
+
|
|
130
|
+
<span v-if="error" class="mint-protocol-editor__field-error">
|
|
131
|
+
{{ error }}
|
|
132
|
+
</span>
|
|
133
|
+
</div>
|
|
134
|
+
</template>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { Reagent, ReagentColumn } from '../types'
|
|
2
|
+
|
|
3
|
+
export const REAGENT_COLUMN_LABELS: Record<ReagentColumn, string> = {
|
|
4
|
+
name: 'Name',
|
|
5
|
+
catalog: 'Catalog #',
|
|
6
|
+
lot: 'Lot #',
|
|
7
|
+
expiry: 'Expiry',
|
|
8
|
+
storage: 'Storage',
|
|
9
|
+
location: 'Location',
|
|
10
|
+
stock: 'Stock',
|
|
11
|
+
supplier: 'Supplier',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ReagentRowClassOptions {
|
|
15
|
+
lowStockThreshold: number
|
|
16
|
+
draggedId?: string | null
|
|
17
|
+
dragOverId?: string | null
|
|
18
|
+
now?: Date
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getReagentColumnValue(reagent: Reagent, column: ReagentColumn): unknown {
|
|
22
|
+
switch (column) {
|
|
23
|
+
case 'name':
|
|
24
|
+
return reagent.name
|
|
25
|
+
case 'catalog':
|
|
26
|
+
return reagent.catalogNumber
|
|
27
|
+
case 'lot':
|
|
28
|
+
return reagent.lotNumber
|
|
29
|
+
case 'expiry':
|
|
30
|
+
return reagent.expiryDate ? new Date(reagent.expiryDate).getTime() : null
|
|
31
|
+
case 'storage':
|
|
32
|
+
return reagent.storageCondition
|
|
33
|
+
case 'location':
|
|
34
|
+
return reagent.location
|
|
35
|
+
case 'stock':
|
|
36
|
+
return reagent.stockLevel
|
|
37
|
+
case 'supplier':
|
|
38
|
+
return reagent.supplier
|
|
39
|
+
default:
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isReagentExpired(reagent: Reagent, now = new Date()): boolean {
|
|
45
|
+
if (!reagent.expiryDate) return false
|
|
46
|
+
const expiry = new Date(reagent.expiryDate)
|
|
47
|
+
return expiry < now
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isReagentExpiringSoon(
|
|
51
|
+
reagent: Reagent,
|
|
52
|
+
daysThreshold = 30,
|
|
53
|
+
now = new Date()
|
|
54
|
+
): boolean {
|
|
55
|
+
if (!reagent.expiryDate) return false
|
|
56
|
+
const expiry = new Date(reagent.expiryDate)
|
|
57
|
+
const daysUntilExpiry = (expiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
|
|
58
|
+
return daysUntilExpiry > 0 && daysUntilExpiry <= daysThreshold
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function isReagentLowStock(reagent: Reagent, lowStockThreshold: number): boolean {
|
|
62
|
+
if (reagent.stockLevel === undefined) return false
|
|
63
|
+
return reagent.stockLevel <= lowStockThreshold
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function formatReagentExpiryDate(date: Date | string | undefined): string {
|
|
67
|
+
if (!date) return '-'
|
|
68
|
+
const parsed = new Date(date)
|
|
69
|
+
return parsed.toLocaleDateString('en-US', { year: 'numeric', month: 'short' })
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getReagentStockLevel(reagent: Reagent): number {
|
|
73
|
+
return reagent.stockLevel ?? 0
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getReagentStockFillClass(level: number): string {
|
|
77
|
+
if (level >= 50) return 'mint-reagent-list__stock-fill--good'
|
|
78
|
+
if (level >= 20) return 'mint-reagent-list__stock-fill--warning'
|
|
79
|
+
return 'mint-reagent-list__stock-fill--low'
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function getReagentRowClasses(
|
|
83
|
+
reagent: Reagent,
|
|
84
|
+
options: ReagentRowClassOptions
|
|
85
|
+
): string[] {
|
|
86
|
+
const classes: string[] = ['mint-reagent-list__row']
|
|
87
|
+
|
|
88
|
+
if (isReagentExpired(reagent, options.now)) {
|
|
89
|
+
classes.push('mint-reagent-list__row--expired')
|
|
90
|
+
} else if (isReagentExpiringSoon(reagent, 30, options.now)) {
|
|
91
|
+
classes.push('mint-reagent-list__row--expiring-soon')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (isReagentLowStock(reagent, options.lowStockThreshold)) {
|
|
95
|
+
classes.push('mint-reagent-list__row--low-stock')
|
|
96
|
+
}
|
|
97
|
+
if (options.draggedId === reagent.id) {
|
|
98
|
+
classes.push('mint-reagent-list__row--dragging')
|
|
99
|
+
}
|
|
100
|
+
if (options.dragOverId === reagent.id) {
|
|
101
|
+
classes.push('mint-reagent-list__row--drag-over')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return classes
|
|
105
|
+
}
|
|
@@ -4,6 +4,16 @@ import { ref, computed } from 'vue'
|
|
|
4
4
|
import type { ReagentColumn, Reagent } from '../types'
|
|
5
5
|
import { useSortedItems } from '../composables/useSortedItems'
|
|
6
6
|
import { useTextSearch } from '../composables/useTextSearch'
|
|
7
|
+
import {
|
|
8
|
+
REAGENT_COLUMN_LABELS as columnLabels,
|
|
9
|
+
formatReagentExpiryDate as formatExpiryDate,
|
|
10
|
+
getReagentColumnValue,
|
|
11
|
+
getReagentRowClasses,
|
|
12
|
+
getReagentStockFillClass as getStockFillClass,
|
|
13
|
+
getReagentStockLevel as getStockLevel,
|
|
14
|
+
isReagentExpired as isExpired,
|
|
15
|
+
isReagentExpiringSoon as isExpiringSoon,
|
|
16
|
+
} from './ReagentList.presentation'
|
|
7
17
|
|
|
8
18
|
interface Props {
|
|
9
19
|
modelValue?: Reagent[]
|
|
@@ -40,18 +50,6 @@ const sortDirection = ref<'asc' | 'desc'>('asc')
|
|
|
40
50
|
const draggedId = ref<string | null>(null)
|
|
41
51
|
const dragOverId = ref<string | null>(null)
|
|
42
52
|
|
|
43
|
-
// Column configuration
|
|
44
|
-
const columnLabels: Record<ReagentColumn, string> = {
|
|
45
|
-
name: 'Name',
|
|
46
|
-
catalog: 'Catalog #',
|
|
47
|
-
lot: 'Lot #',
|
|
48
|
-
expiry: 'Expiry',
|
|
49
|
-
storage: 'Storage',
|
|
50
|
-
location: 'Location',
|
|
51
|
-
stock: 'Stock',
|
|
52
|
-
supplier: 'Supplier',
|
|
53
|
-
}
|
|
54
|
-
|
|
55
53
|
const reagentSearch = useTextSearch({
|
|
56
54
|
items: () => props.modelValue || [],
|
|
57
55
|
query: searchQuery,
|
|
@@ -76,77 +74,16 @@ const sortedReagents = useSortedItems<Reagent, ReagentColumn>({
|
|
|
76
74
|
items: reagentSearch.filteredItems,
|
|
77
75
|
sort: reagentSort,
|
|
78
76
|
caseSensitive: false,
|
|
79
|
-
getValue: (reagent, column) =>
|
|
77
|
+
getValue: (reagent, column) => getReagentColumnValue(reagent, column),
|
|
80
78
|
})
|
|
81
79
|
const filteredReagents = sortedReagents.sortedItems
|
|
82
80
|
|
|
83
|
-
function getColumnValue(reagent: Reagent, column: ReagentColumn): unknown {
|
|
84
|
-
switch (column) {
|
|
85
|
-
case 'name':
|
|
86
|
-
return reagent.name
|
|
87
|
-
case 'catalog':
|
|
88
|
-
return reagent.catalogNumber
|
|
89
|
-
case 'lot':
|
|
90
|
-
return reagent.lotNumber
|
|
91
|
-
case 'expiry':
|
|
92
|
-
return reagent.expiryDate ? new Date(reagent.expiryDate).getTime() : null
|
|
93
|
-
case 'storage':
|
|
94
|
-
return reagent.storageCondition
|
|
95
|
-
case 'location':
|
|
96
|
-
return reagent.location
|
|
97
|
-
case 'stock':
|
|
98
|
-
return reagent.stockLevel
|
|
99
|
-
case 'supplier':
|
|
100
|
-
return reagent.supplier
|
|
101
|
-
default:
|
|
102
|
-
return null
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Helpers
|
|
107
|
-
function isExpired(reagent: Reagent): boolean {
|
|
108
|
-
if (!reagent.expiryDate) return false
|
|
109
|
-
const expiry = new Date(reagent.expiryDate)
|
|
110
|
-
return expiry < new Date()
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function isExpiringSoon(reagent: Reagent, daysThreshold = 30): boolean {
|
|
114
|
-
if (!reagent.expiryDate) return false
|
|
115
|
-
const expiry = new Date(reagent.expiryDate)
|
|
116
|
-
const now = new Date()
|
|
117
|
-
const daysUntilExpiry = (expiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
|
|
118
|
-
return daysUntilExpiry > 0 && daysUntilExpiry <= daysThreshold
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function isLowStock(reagent: Reagent): boolean {
|
|
122
|
-
if (reagent.stockLevel === undefined) return false
|
|
123
|
-
return reagent.stockLevel <= props.lowStockThreshold
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function formatExpiryDate(date: Date | string | undefined): string {
|
|
127
|
-
if (!date) return '-'
|
|
128
|
-
const d = new Date(date)
|
|
129
|
-
return d.toLocaleDateString('en-US', { year: 'numeric', month: 'short' })
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function getStockLevel(reagent: Reagent): number {
|
|
133
|
-
return reagent.stockLevel ?? 0
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function getStockFillClass(level: number): string {
|
|
137
|
-
if (level >= 50) return 'mint-reagent-list__stock-fill--good'
|
|
138
|
-
if (level >= 20) return 'mint-reagent-list__stock-fill--warning'
|
|
139
|
-
return 'mint-reagent-list__stock-fill--low'
|
|
140
|
-
}
|
|
141
|
-
|
|
142
81
|
function getRowClass(reagent: Reagent): string[] {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (dragOverId.value === reagent.id) classes.push('mint-reagent-list__row--drag-over')
|
|
149
|
-
return classes
|
|
82
|
+
return getReagentRowClasses(reagent, {
|
|
83
|
+
lowStockThreshold: props.lowStockThreshold,
|
|
84
|
+
draggedId: draggedId.value,
|
|
85
|
+
dragOverId: dragOverId.value,
|
|
86
|
+
})
|
|
150
87
|
}
|
|
151
88
|
|
|
152
89
|
// Actions
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { DEFAULT_COLORS } from '../composables/useAutoGroup'
|
|
2
|
+
import type { SampleGroup } from '../types'
|
|
3
|
+
|
|
4
|
+
export type ColorEdit =
|
|
5
|
+
| { kind: 'single'; name: string }
|
|
6
|
+
| { kind: 'family'; names: string[] }
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_COLOR_PICKER_SEED = '#3B82F6'
|
|
9
|
+
|
|
10
|
+
export function applySampleGroupColorEdit(
|
|
11
|
+
groups: SampleGroup[],
|
|
12
|
+
edit: ColorEdit,
|
|
13
|
+
color: string,
|
|
14
|
+
): SampleGroup[] {
|
|
15
|
+
const targets = edit.kind === 'family' ? new Set(edit.names) : new Set([edit.name])
|
|
16
|
+
return groups.map(group =>
|
|
17
|
+
targets.has(group.name) ? { ...group, color } : group,
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getSampleGroupColorEditSeed(
|
|
22
|
+
edit: ColorEdit | null,
|
|
23
|
+
getGroupColor: (groupName: string) => string,
|
|
24
|
+
): string {
|
|
25
|
+
if (!edit) return DEFAULT_COLOR_PICKER_SEED
|
|
26
|
+
return getGroupColor(edit.kind === 'family' ? edit.names[0] : edit.name)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function pickUnusedSampleGroupColor(groups: SampleGroup[]): string {
|
|
30
|
+
const usedColors = new Set(groups.map(group => group.color))
|
|
31
|
+
return DEFAULT_COLORS.find(color => !usedColors.has(color)) || DEFAULT_COLORS[0]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createSampleGroup(name: string, existingGroups: SampleGroup[]): SampleGroup | null {
|
|
35
|
+
const trimmedName = name.trim()
|
|
36
|
+
if (!trimmedName) return null
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
name: trimmedName,
|
|
40
|
+
color: pickUnusedSampleGroupColor(existingGroups),
|
|
41
|
+
samples: [],
|
|
42
|
+
}
|
|
43
|
+
}
|