From ae806911bec64f2cd75837b71ddb524d585e4bcb Mon Sep 17 00:00:00 2001 From: mahdahar <89adham@gmail.com> Date: Tue, 24 Feb 2026 16:53:04 +0700 Subject: [PATCH] feat(equipment,organization): add equipment API client and complete organization module structure - Add equipment.js API client with full CRUD operations - Add organization sub-routes: account, department, discipline, instrument, site, workstation - Create EquipmentModal and DeleteConfirmModal components - Update master-data navigation and sidebar - Update tests, containers, counters, geography, locations, occupations, specialties, testmap, and valuesets pages - Add COMPONENT_ORGANIZATION.md documentation --- AGENTS.md | 141 ++-- COMPONENT_ORGANIZATION.md | 155 ++++ docs/api-docs.bundled.yaml | 688 +++++++++++++++- src/lib/api/equipment.js | 86 ++ src/lib/components/Sidebar.svelte | 49 +- src/routes/(app)/master-data/+page.svelte | 12 +- .../(app)/master-data/contacts/+page.svelte | 34 +- .../(app)/master-data/containers/+page.svelte | 8 +- .../(app)/master-data/counters/+page.svelte | 10 +- .../(app)/master-data/geography/+page.svelte | 24 +- .../(app)/master-data/locations/+page.svelte | 8 +- .../master-data/occupations/+page.svelte | 8 +- .../master-data/organization/+page.svelte | 735 ++---------------- .../organization/account/+page.svelte | 278 +++++++ .../organization/department/+page.svelte | 320 ++++++++ .../organization/discipline/+page.svelte | 307 ++++++++ .../organization/instrument/+page.svelte | 281 +++++++ .../instrument/DeleteConfirmModal.svelte | 43 + .../instrument/EquipmentModal.svelte | 155 ++++ .../organization/site/+page.svelte | 265 +++++++ .../organization/workstation/+page.svelte | 265 +++++++ .../master-data/specialties/+page.svelte | 36 +- .../(app)/master-data/testmap/+page.svelte | 111 ++- .../master-data/testmap/TestMapModal.svelte | 7 +- .../(app)/master-data/tests/+page.svelte | 34 +- .../(app)/master-data/valuesets/+page.svelte | 8 +- 26 files changed, 3154 insertions(+), 914 deletions(-) create mode 100644 COMPONENT_ORGANIZATION.md create mode 100644 src/lib/api/equipment.js create mode 100644 src/routes/(app)/master-data/organization/account/+page.svelte create mode 100644 src/routes/(app)/master-data/organization/department/+page.svelte create mode 100644 src/routes/(app)/master-data/organization/discipline/+page.svelte create mode 100644 src/routes/(app)/master-data/organization/instrument/+page.svelte create mode 100644 src/routes/(app)/master-data/organization/instrument/DeleteConfirmModal.svelte create mode 100644 src/routes/(app)/master-data/organization/instrument/EquipmentModal.svelte create mode 100644 src/routes/(app)/master-data/organization/site/+page.svelte create mode 100644 src/routes/(app)/master-data/organization/workstation/+page.svelte diff --git a/AGENTS.md b/AGENTS.md index 3cb17f6..abd99a2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,13 +5,22 @@ SvelteKit frontend for Clinical Laboratory Quality Management System. Uses Svelt ## Commands ```bash -pnpm run dev # Development server -pnpm run build # Production build +# Development +pnpm run dev # Start development server +pnpm run build # Production build (outputs to build/) pnpm run preview # Preview production build -pnpm run prepare # Sync SvelteKit +pnpm run prepare # Sync SvelteKit (runs on install) + +# Package management +pnpm install # Install dependencies (use pnpm, not npm/yarn) + +# Testing (not configured yet - add when needed) +# vitest run src/path/to/test.js # Run single test +# vitest # Run tests in watch mode +# npx playwright test # E2E tests ``` -**No test framework yet.** When adding: use Vitest (`vitest run src/path/to/test.js`), Playwright for E2E. +**No ESLint/Prettier configured yet.** When adding: configure in `vite.config.js` or separate config files. ## Code Style @@ -20,26 +29,26 @@ pnpm run prepare # Sync SvelteKit - **Quotes**: Single quotes - **Indentation**: 2 spaces - **Trailing commas**: In multi-line objects/arrays -- **JSDoc**: Document all exported functions +- **JSDoc**: Document all exported functions with `@param` and `@returns` -## Naming Conventions - -- **Components**: PascalCase (`LoginForm.svelte`) -- **Files/Routes**: lowercase with hyphens (`+page.svelte`) -- **Variables**: camelCase (`isLoading`, `userName`) -- **Constants**: UPPER_SNAKE_CASE (`API_URL`) -- **Stores**: camelCase (`auth`, `config`) -- **Handlers**: prefix with `handle` (`handleSubmit`) -- **Form state**: `formLoading`, `formError` - -## Imports Order +### Imports Order 1. Svelte (`svelte`, `$app/*`) 2. `$lib/*` (stores, api, components, utils) 3. External libraries (`lucide-svelte`) 4. Relative imports (minimize, prefer `$lib`) -## Svelte 5 Components +## Naming Conventions + +- **Components**: PascalCase (`LoginForm.svelte`, `PatientFormModal.svelte`) +- **Files/Routes**: lowercase with hyphens (`+page.svelte`, `user-profile/`) +- **Variables**: camelCase (`isLoading`, `userName`) +- **Constants**: UPPER_SNAKE_CASE (`API_URL`, `STORAGE_KEY`) +- **Stores**: camelCase, descriptive (`auth`, `config`, `userStore`) +- **Event handlers**: prefix with `handle` (`handleSubmit`, `handleClick`) +- **Form state**: `formLoading`, `formError`, `deleteConfirmOpen` + +## Svelte 5 Component Structure ```svelte + + + {#snippet children()} +
+ + + +
+ + {#if activeTab === 'basic'} + + {:else if activeTab === 'technical'} + + {:else if activeTab === 'calculation'} + + {/if} + {/snippet} +
+``` + +```svelte + + + +
+
+ + +
+
+ + +
+
+``` + +## Data Flow + +### Parent to Child +- Pass data via props (`bind:formData`) +- Use `$bindable()` for two-way binding +- Keep state in parent when shared across tabs + +### Child to Parent +- Use callbacks for actions (`onSave`, `onClose`) +- Modify bound data directly (with `$bindable`) +- Emit events for complex interactions + +## Props Interface Pattern + +```javascript +// Define props with JSDoc +/** @type {{ formData: Object, onValidate: Function, readonly: boolean }} */ +let { + formData = $bindable({}), + onValidate = () => true, + readonly = false +} = $props(); +``` + +## Naming Conventions + +- **Main modal**: `{Feature}Modal.svelte` (e.g., `TestFormModal.svelte`) +- **Tab components**: `{TabName}Tab.svelte` (e.g., `BasicInfoTab.svelte`) +- **Nested modals**: `{Action}Modal.svelte` (e.g., `ConfirmDeleteModal.svelte`) +- **Folder names**: kebab-case matching the modal name (e.g., `test-modal/`) + +## Shared State Management + +For complex modals with shared state across tabs: + +```javascript +// In main modal +let sharedState = $state({ + dirty: false, + errors: {}, + selectedItems: [] +}); + +// Pass to tabs + +``` + +## Import Order in Sub-components + +Same as main components: +1. Svelte imports +2. `$lib/*` imports +3. External libraries +4. Relative imports (other tabs/modals) + +## Testing Split Components + +```bash +# Test individual tab component +vitest run src/routes/feature/modal-tabs/BasicInfoTab.test.js + +# Test main modal integration +vitest run src/routes/feature/FeatureModal.test.js +``` + +## Benefits + +- **Maintainability**: Each file has single responsibility +- **Collaboration**: Multiple developers can work on different tabs +- **Testing**: Test individual sections in isolation +- **Performance**: Only render visible tab content +- **Reusability**: Tabs can be used in different modals diff --git a/docs/api-docs.bundled.yaml b/docs/api-docs.bundled.yaml index 69f3bfe..098ec39 100644 --- a/docs/api-docs.bundled.yaml +++ b/docs/api-docs.bundled.yaml @@ -47,6 +47,8 @@ tags: description: Value set definitions and items - name: Demo description: Demo/test endpoints (no authentication) + - name: EquipmentList + description: Laboratory equipment and instrument management paths: /api/auth/login: post: @@ -594,6 +596,225 @@ paths: responses: '200': description: Status logged + /api/equipmentlist: + get: + tags: + - EquipmentList + summary: List equipment + description: Get list of equipment with optional filters + security: + - bearerAuth: [] + parameters: + - name: IEID + in: query + schema: + type: string + description: Filter by IEID + - name: InstrumentName + in: query + schema: + type: string + description: Filter by instrument name + - name: DepartmentID + in: query + schema: + type: integer + description: Filter by department ID + - name: WorkstationID + in: query + schema: + type: integer + description: Filter by workstation ID + - name: Enable + in: query + schema: + type: integer + enum: + - 0 + - 1 + description: Filter by enable status + responses: + '200': + description: List of equipment + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + type: array + items: + $ref: '#/components/schemas/EquipmentList' + post: + tags: + - EquipmentList + summary: Create equipment + description: Create a new equipment entry + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - IEID + - DepartmentID + - Enable + - EquipmentRole + properties: + IEID: + type: string + maxLength: 50 + DepartmentID: + type: integer + InstrumentID: + type: string + maxLength: 150 + InstrumentName: + type: string + maxLength: 150 + WorkstationID: + type: integer + Enable: + type: integer + enum: + - 0 + - 1 + EquipmentRole: + type: string + maxLength: 1 + responses: + '201': + description: Equipment created + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + type: integer + patch: + tags: + - EquipmentList + summary: Update equipment + description: Update an existing equipment entry + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - EID + properties: + EID: + type: integer + IEID: + type: string + maxLength: 50 + DepartmentID: + type: integer + InstrumentID: + type: string + maxLength: 150 + InstrumentName: + type: string + maxLength: 150 + WorkstationID: + type: integer + Enable: + type: integer + enum: + - 0 + - 1 + EquipmentRole: + type: string + maxLength: 1 + responses: + '200': + description: Equipment updated + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + type: integer + delete: + tags: + - EquipmentList + summary: Delete equipment + description: Soft delete an equipment entry + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - EID + properties: + EID: + type: integer + responses: + '200': + description: Equipment deleted + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + /api/equipmentlist/{id}: + get: + tags: + - EquipmentList + summary: Get equipment by ID + description: Get a single equipment entry by its EID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Equipment ID + responses: + '200': + description: Equipment details + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '#/components/schemas/EquipmentList' /api/location: get: tags: @@ -2556,6 +2777,326 @@ paths: responses: '200': description: Collection method details + /api/test/testmap: + get: + tags: + - Tests + summary: List all test mappings + security: + - bearerAuth: [] + parameters: + - name: page + in: query + schema: + type: integer + default: 1 + description: Page number for pagination + - name: perPage + in: query + schema: + type: integer + default: 20 + description: Number of items per page + responses: + '200': + description: List of test mappings + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + data: + type: array + items: + $ref: '#/components/schemas/TestMap' + post: + tags: + - Tests + summary: Create test mapping + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + TestSiteID: + type: integer + description: Test Site ID (required) + HostType: + type: string + description: Host type code + HostID: + type: string + description: Host identifier + HostTestCode: + type: string + description: Test code in host system + HostTestName: + type: string + description: Test name in host system + ClientType: + type: string + description: Client type code + ClientID: + type: string + description: Client identifier + ConDefID: + type: integer + description: Connection definition ID + ClientTestCode: + type: string + description: Test code in client system + ClientTestName: + type: string + description: Test name in client system + required: + - TestSiteID + responses: + '201': + description: Test mapping created + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + data: + type: integer + description: Created TestMapID + patch: + tags: + - Tests + summary: Update test mapping + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + TestMapID: + type: integer + description: Test Map ID (required) + TestSiteID: + type: integer + HostType: + type: string + HostID: + type: string + HostTestCode: + type: string + HostTestName: + type: string + ClientType: + type: string + ClientID: + type: string + ConDefID: + type: integer + ClientTestCode: + type: string + ClientTestName: + type: string + required: + - TestMapID + responses: + '200': + description: Test mapping updated + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + data: + type: integer + description: Updated TestMapID + delete: + tags: + - Tests + summary: Soft delete test mapping + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + TestMapID: + type: integer + description: Test Map ID to delete (required) + required: + - TestMapID + responses: + '200': + description: Test mapping deleted successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + data: + type: integer + description: Deleted TestMapID + '404': + description: Test mapping not found or already deleted + /api/test/testmap/{id}: + get: + tags: + - Tests + summary: Get test mapping by ID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Test Map ID + responses: + '200': + description: Test mapping details + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '#/components/schemas/TestMap' + '404': + description: Test mapping not found + /api/test/testmap/by-testsite/{testSiteID}: + get: + tags: + - Tests + summary: Get test mappings by test site + security: + - bearerAuth: [] + parameters: + - name: testSiteID + in: path + required: true + schema: + type: integer + description: Test Site ID + responses: + '200': + description: List of test mappings for the test site + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + type: array + items: + $ref: '#/components/schemas/TestMap' + /api/test/testmap/by-host/{hostType}/{hostID}: + get: + tags: + - Tests + summary: Get test mappings by host + security: + - bearerAuth: [] + parameters: + - name: hostType + in: path + required: true + schema: + type: string + description: Host type code + - name: hostID + in: path + required: true + schema: + type: string + description: Host identifier + responses: + '200': + description: List of test mappings for the host + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + type: array + items: + $ref: '#/components/schemas/TestMap' + /api/test/testmap/by-client/{clientType}/{clientID}: + get: + tags: + - Tests + summary: Get test mappings by client + security: + - bearerAuth: [] + parameters: + - name: clientType + in: path + required: true + schema: + type: string + description: Client type code + - name: clientID + in: path + required: true + schema: + type: string + description: Client identifier + responses: + '200': + description: List of test mappings for the client + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + type: array + items: + $ref: '#/components/schemas/TestMap' /api/tests: get: tags: @@ -2674,14 +3215,18 @@ paths: type: string enum: - NMRIC + - RANGE + - TEXT - VSET + - NORES RefType: type: string enum: - - NMRC - - TEXT + - RANGE - THOLD - VSET + - TEXT + - NOREF VSet: type: integer ReqQty: @@ -2794,14 +3339,18 @@ paths: type: string enum: - NMRIC + - RANGE + - TEXT - VSET + - NORES RefType: type: string enum: - - NMRC - - TEXT + - RANGE - THOLD - VSET + - TEXT + - NOREF VSet: type: integer ReqQty: @@ -2955,7 +3504,7 @@ paths: tags: - ValueSets summary: List lib value sets - description: List all library/system value sets from JSON files with item counts. Returns an object where keys are value set names and values are item counts. + description: List all library/system value sets from JSON files with item counts. Returns an array of objects with value, label, and count properties. security: - bearerAuth: [] parameters: @@ -2963,7 +3512,7 @@ paths: in: query schema: type: string - description: Optional search term to filter value set names + description: Optional search term to filter value set names or labels responses: '200': description: List of lib value sets with item counts @@ -2976,14 +3525,19 @@ paths: type: string example: success data: - type: object - additionalProperties: - type: integer - description: Number of items in each value set + type: array + items: + $ref: '#/components/schemas/ValueSetListItem' example: - sex: 3 - marital_status: 6 - order_status: 6 + - value: sex + label: Sex + count: 3 + - value: marital_status + label: Marital Status + count: 6 + - value: order_status + label: Order Status + count: 6 /api/valueset/{key}: get: tags: @@ -4139,22 +4693,46 @@ components: type: string enum: - NMRIC + - RANGE + - TEXT - VSET + - NORES description: | - NMRIC: Numeric result - VSET: Value set result + Result type determines the format of test results: + - NMRIC: Single numeric value + - RANGE: Numeric range (min-max) + - TEXT: Free text result + - VSET: Value set/enum result + - NORES: No result (for GROUP and TITLE types) + + TestType to ResultType mapping: + - TEST: NMRIC | RANGE | TEXT | VSET + - PARAM: NMRIC | RANGE | TEXT | VSET + - CALC: NMRIC (calculated result is always numeric) + - GROUP: NORES (no result, container only) + - TITLE: NORES (no result, header only) RefType: type: string enum: - - NMRC - - TEXT + - RANGE - THOLD - VSET + - TEXT + - NOREF description: | - NMRC: Numeric reference range - TEXT: Text reference - THOLD: Threshold reference - VSET: Value set reference + Reference type determines which reference range table to use: + - RANGE: Numeric reference range + - THOLD: Threshold/panic range + - VSET: Value set reference + - TEXT: Free text reference + - NOREF: No reference (for NORES result type) + + ResultType to RefType mapping: + - NMRIC: RANGE | THOLD → refnum table + - RANGE: RANGE | THOLD → refnum table + - VSET: VSET → reftxt table + - TEXT: TEXT → reftxt table + - NORES: NOREF → (no reference table) VSet: type: integer description: Value set ID for VSET result type @@ -4530,9 +5108,6 @@ components: HostID: type: string description: Host identifier - HostDataSource: - type: string - description: Host data source HostTestCode: type: string description: Test code in host system @@ -4545,9 +5120,6 @@ components: ClientID: type: string description: Client identifier - ClientDataSource: - type: string - description: Client data source ConDefID: type: integer description: Connection definition ID @@ -4812,6 +5384,55 @@ components: type: string format: date-time nullable: true + EquipmentList: + type: object + properties: + EID: + type: integer + description: Equipment ID (auto-increment) + IEID: + type: string + maxLength: 50 + description: Internal Equipment ID + DepartmentID: + type: integer + description: Reference to department + InstrumentID: + type: string + maxLength: 150 + description: Instrument identifier + InstrumentName: + type: string + maxLength: 150 + description: Instrument display name + WorkstationID: + type: integer + description: Reference to workstation + Enable: + type: integer + enum: + - 0 + - 1 + description: Equipment status (0=disabled, 1=enabled) + EquipmentRole: + type: string + maxLength: 1 + description: Equipment role code + CreateDate: + type: string + format: date-time + description: Creation timestamp + EndDate: + type: string + format: date-time + nullable: true + description: Deletion timestamp (soft delete) + DepartmentName: + type: string + description: Joined department name + WorkstationName: + type: string + description: Joined workstation name Contact: type: object properties: @@ -4864,3 +5485,16 @@ components: type: string format: date-time description: Occupation display text + ValueSetListItem: + type: object + description: Library/system value set summary (from JSON files) + properties: + value: + type: string + description: The value set key/name + label: + type: string + description: The display name/label + count: + type: integer + description: Number of items in this value set diff --git a/src/lib/api/equipment.js b/src/lib/api/equipment.js new file mode 100644 index 0000000..f5ac9b8 --- /dev/null +++ b/src/lib/api/equipment.js @@ -0,0 +1,86 @@ +import { get, post, patch, del } from './client.js'; + +/** + * Fetch list of equipment with optional filters + * @param {Object} params - Filter parameters + * @param {string} [params.IEID] - Filter by IEID + * @param {string} [params.InstrumentName] - Filter by instrument name + * @param {number} [params.DepartmentID] - Filter by department ID + * @param {number} [params.WorkstationID] - Filter by workstation ID + * @param {number} [params.Enable] - Filter by enable status (0 or 1) + * @returns {Promise} List of equipment + */ +export async function fetchEquipmentList(params = {}) { + const query = new URLSearchParams(params).toString(); + return get(query ? `/api/equipmentlist?${query}` : '/api/equipmentlist'); +} + +/** + * Fetch single equipment by ID + * @param {number} id - Equipment ID (EID) + * @returns {Promise} Equipment details + */ +export async function fetchEquipment(id) { + return get(`/api/equipmentlist/${id}`); +} + +/** + * Create new equipment + * @param {Object} data - Equipment data + * @param {string} data.IEID - Internal Equipment ID (required) + * @param {number} data.DepartmentID - Department ID (required) + * @param {number} data.Enable - Enable status 0 or 1 (required) + * @param {string} data.EquipmentRole - Equipment role code (required) + * @param {string} [data.InstrumentID] - Instrument identifier + * @param {string} [data.InstrumentName] - Instrument display name + * @param {number} [data.WorkstationID] - Workstation ID + * @returns {Promise} Created equipment ID + */ +export async function createEquipment(data) { + const payload = { + IEID: data.IEID, + DepartmentID: data.DepartmentID, + Enable: data.Enable, + EquipmentRole: data.EquipmentRole, + InstrumentID: data.InstrumentID || null, + InstrumentName: data.InstrumentName || null, + WorkstationID: data.WorkstationID || null, + }; + return post('/api/equipmentlist', payload); +} + +/** + * Update existing equipment + * @param {Object} data - Equipment data + * @param {number} data.EID - Equipment ID (required) + * @param {string} [data.IEID] - Internal Equipment ID + * @param {number} [data.DepartmentID] - Department ID + * @param {number} [data.Enable] - Enable status 0 or 1 + * @param {string} [data.EquipmentRole] - Equipment role code + * @param {string} [data.InstrumentID] - Instrument identifier + * @param {string} [data.InstrumentName] - Instrument display name + * @param {number} [data.WorkstationID] - Workstation ID + * @returns {Promise} Updated equipment ID + */ +export async function updateEquipment(data) { + const payload = { + EID: data.EID, + IEID: data.IEID, + DepartmentID: data.DepartmentID, + Enable: data.Enable, + EquipmentRole: data.EquipmentRole, + InstrumentID: data.InstrumentID || null, + InstrumentName: data.InstrumentName || null, + WorkstationID: data.WorkstationID || null, + }; + return patch('/api/equipmentlist', payload); +} + +/** + * Delete equipment (soft delete) + * @param {number} eid - Equipment ID + * @returns {Promise} Delete result + */ +export async function deleteEquipment(eid) { + return del('/api/equipmentlist', { EID: eid }); +} diff --git a/src/lib/components/Sidebar.svelte b/src/lib/components/Sidebar.svelte index 702cb15..b9f4e4c 100644 --- a/src/lib/components/Sidebar.svelte +++ b/src/lib/components/Sidebar.svelte @@ -20,7 +20,11 @@ import { TestTube, FileText, X, - Link + Link, + LandPlot, + Monitor, + Activity, + User } from 'lucide-svelte'; import { auth } from '$lib/stores/auth.js'; import { goto } from '$app/navigation'; @@ -31,6 +35,7 @@ import { // Collapsible section states - default collapsed let laboratoryExpanded = $state(false); let masterDataExpanded = $state(false); + let organizationExpanded = $state(false); // Load states from localStorage on mount $effect(() => { @@ -41,6 +46,7 @@ import { const parsed = JSON.parse(savedStates); laboratoryExpanded = parsed.laboratory ?? false; masterDataExpanded = parsed.masterData ?? false; + organizationExpanded = parsed.organization ?? false; } catch (e) { // Keep defaults if parsing fails } @@ -53,7 +59,8 @@ import { if (browser) { localStorage.setItem('sidebar_section_states', JSON.stringify({ laboratory: laboratoryExpanded, - masterData: masterDataExpanded + masterData: masterDataExpanded, + organization: organizationExpanded })); } }); @@ -63,6 +70,7 @@ import { if (!isOpen) { laboratoryExpanded = false; masterDataExpanded = false; + organizationExpanded = false; } }); @@ -94,6 +102,13 @@ function toggleLaboratory() { } masterDataExpanded = !masterDataExpanded; } + + function toggleOrganization() { + if (!isOpen) { + expandSidebar(); + } + organizationExpanded = !organizationExpanded; + } @@ -213,7 +228,26 @@ function toggleLaboratory() { {#if isOpen && masterDataExpanded}