From 22ee1ebfd197400d93c2222a6233ea0772961e6f Mon Sep 17 00:00:00 2001 From: mahdahar <89adham@gmail.com> Date: Tue, 10 Mar 2026 16:40:44 +0700 Subject: [PATCH] feat: update organization pages and test modal - Update organization discipline and site pages with new features - Enhance TestFormModal component - Refactor GroupMembersTab implementation - Update API documentation - Add organization API methods --- .serena/project.yml | 5 + docs/api-docs.bundled.yaml | 435 +++++++++++++++++- src/lib/api/organization.js | 4 + .../organization/discipline/+page.svelte | 50 +- .../organization/site/+page.svelte | 52 ++- .../tests/test-modal/TestFormModal.svelte | 10 +- .../test-modal/tabs/GroupMembersTab.svelte | 308 +++++++------ 7 files changed, 701 insertions(+), 163 deletions(-) diff --git a/.serena/project.yml b/.serena/project.yml index e002ca0..e04b5a7 100644 --- a/.serena/project.yml +++ b/.serena/project.yml @@ -125,3 +125,8 @@ language_backend: # list of regex patterns which, when matched, mark a memory entry as read‑only. # Extends the list from the global configuration, merging the two lists. read_only_memory_patterns: [] + +# line ending convention to use when writing source files. +# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default) +# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings. +line_ending: diff --git a/docs/api-docs.bundled.yaml b/docs/api-docs.bundled.yaml index 70a345a..9d35699 100644 --- a/docs/api-docs.bundled.yaml +++ b/docs/api-docs.bundled.yaml @@ -51,6 +51,8 @@ tags: description: Demo/test endpoints (no authentication) - name: EquipmentList description: Laboratory equipment and instrument management + - name: Users + description: User management and administration paths: /api/auth/login: post: @@ -3104,6 +3106,68 @@ paths: responses: '200': description: Specimen details + delete: + tags: + - Specimen + summary: Delete specimen (soft delete) + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Specimen ID (SID) + responses: + '200': + description: Specimen deleted successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + example: Specimen deleted successfully + data: + type: object + properties: + SID: + type: integer + '404': + description: Specimen not found + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: failed + message: + type: string + example: Specimen not found + data: + type: null + '500': + description: Server error + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: failed + message: + type: string + example: Failed to delete specimen + data: + type: null /api/specimen/container: get: tags: @@ -3845,7 +3909,7 @@ paths: responses: '200': description: Batch delete results - /api/tests: + /api/test: get: tags: - Tests @@ -4170,7 +4234,7 @@ paths: properties: TestSiteId: type: integer - /api/tests/{id}: + /api/test/{id}: get: tags: - Tests @@ -4247,6 +4311,219 @@ paths: description: Test not found '422': description: Test already disabled + /api/users: + get: + tags: + - Users + summary: List users with pagination and search + security: + - bearerAuth: [] + parameters: + - name: page + in: query + schema: + type: integer + default: 1 + description: Page number + - name: per_page + in: query + schema: + type: integer + default: 20 + description: Items per page + - name: search + in: query + schema: + type: string + description: Search term for username, email, or name + responses: + '200': + description: List of users with pagination + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + example: Users retrieved successfully + data: + type: object + properties: + users: + type: array + items: + $ref: '#/components/schemas/User' + pagination: + type: object + properties: + current_page: + type: integer + per_page: + type: integer + total: + type: integer + total_pages: + type: integer + '500': + description: Server error + post: + tags: + - Users + summary: Create new user + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UserCreate' + responses: + '201': + description: User created successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + example: User created successfully + data: + type: object + properties: + UserID: + type: integer + Username: + type: string + Email: + type: string + '400': + description: Validation failed + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: failed + message: + type: string + example: Validation failed + data: + type: object + '500': + description: Server error + patch: + tags: + - Users + summary: Update existing user + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UserUpdate' + responses: + '200': + description: User updated successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + example: User updated successfully + data: + type: object + properties: + UserID: + type: integer + updated_fields: + type: array + items: + type: string + '400': + description: UserID is required + '404': + description: User not found + '500': + description: Server error + /api/users/{id}: + get: + tags: + - Users + summary: Get user by ID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: User ID + responses: + '200': + description: User details + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '404': + description: User not found + '500': + description: Server error + delete: + tags: + - Users + summary: Delete user (soft delete) + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: User ID + responses: + '200': + description: User deleted successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + example: User deleted successfully + data: + type: object + properties: + UserID: + type: integer + '404': + description: User not found + '500': + description: Server error /api/valueset: get: tags: @@ -5242,6 +5519,8 @@ components: type: string SiteCode: type: string + maxLength: 2 + pattern: ^[A-Z0-9]{2}$ AccountID: type: integer Discipline: @@ -5627,6 +5906,34 @@ components: description: Group members (only for GROUP type) items: type: object + properties: + TestGrpID: + type: integer + description: Group membership record ID + TestSiteID: + type: integer + description: Parent group TestSiteID + Member: + type: integer + description: Member TestSiteID (foreign key to testdefsite) + MemberTestSiteID: + type: integer + description: Member's actual TestSiteID (same as Member, for clarity) + TestSiteCode: + type: string + description: Member test code + TestSiteName: + type: string + description: Member test name + TestType: + type: string + description: Member test type + CreateDate: + type: string + format: date-time + EndDate: + type: string + format: date-time testmap: type: array description: Test mappings @@ -5886,13 +6193,19 @@ components: CountStat: 1 testdefgrp: - TestGrpID: 1 + TestSiteID: 6 Member: 100 + MemberTestSiteID: 100 TestSiteCode: CHOL TestSiteName: Total Cholesterol + TestType: TEST - TestGrpID: 2 + TestSiteID: 6 Member: 101 + MemberTestSiteID: 101 TestSiteCode: TG TestSiteName: Triglycerides + TestType: TEST TITLE: summary: Section header value: @@ -6268,6 +6581,124 @@ components: WorkstationName: type: string description: Joined workstation name + User: + type: object + properties: + UserID: + type: integer + description: Unique user identifier + Username: + type: string + description: Unique login username + Email: + type: string + format: email + description: User email address + Name: + type: string + description: Full name of the user + Role: + type: string + description: User role (admin, technician, doctor, etc.) + Department: + type: string + description: Department name + IsActive: + type: boolean + description: Whether the user account is active + CreatedAt: + type: string + format: date-time + description: Creation timestamp + UpdatedAt: + type: string + format: date-time + description: Last update timestamp + DelDate: + type: string + format: date-time + nullable: true + description: Soft delete timestamp (null if active) + UserCreate: + type: object + required: + - Username + - Email + properties: + Username: + type: string + minLength: 3 + maxLength: 50 + description: Unique login username + Email: + type: string + format: email + maxLength: 100 + description: User email address + Name: + type: string + description: Full name of the user + Role: + type: string + description: User role + Department: + type: string + description: Department name + IsActive: + type: boolean + default: true + description: Whether the user account is active + UserUpdate: + type: object + required: + - UserID + properties: + UserID: + type: integer + description: User ID to update + Email: + type: string + format: email + description: User email address + Name: + type: string + description: Full name of the user + Role: + type: string + description: User role + Department: + type: string + description: Department name + IsActive: + type: boolean + description: Whether the user account is active + UserListResponse: + type: object + properties: + status: + type: string + example: success + message: + type: string + example: Users retrieved successfully + data: + type: object + properties: + users: + type: array + items: + $ref: '#/components/schemas/User' + pagination: + type: object + properties: + current_page: + type: integer + per_page: + type: integer + total: + type: integer + total_pages: + type: integer Contact: type: object properties: diff --git a/src/lib/api/organization.js b/src/lib/api/organization.js index 78fefa9..84fad9f 100644 --- a/src/lib/api/organization.js +++ b/src/lib/api/organization.js @@ -15,6 +15,8 @@ export async function createDiscipline(data) { DisciplineCode: data.DisciplineCode, DisciplineName: data.DisciplineName, Parent: data.Parent || null, + SeqScr: data.SeqScr, + SeqRpt: data.SeqRpt, }; return post('/api/organization/discipline', payload); } @@ -25,6 +27,8 @@ export async function updateDiscipline(data) { DisciplineCode: data.DisciplineCode, DisciplineName: data.DisciplineName, Parent: data.Parent || null, + SeqScr: data.SeqScr, + SeqRpt: data.SeqRpt, }; return patch('/api/organization/discipline', payload); } diff --git a/src/routes/(app)/master-data/organization/discipline/+page.svelte b/src/routes/(app)/master-data/organization/discipline/+page.svelte index 28724cc..c18e83a 100644 --- a/src/routes/(app)/master-data/organization/discipline/+page.svelte +++ b/src/routes/(app)/master-data/organization/discipline/+page.svelte @@ -21,6 +21,8 @@ DisciplineCode: '', DisciplineName: '', Parent: null, + SeqScr: 0, + SeqRpt: 0, }); let deleteConfirmOpen = $state(false); let deleteItem = $state(null); @@ -30,6 +32,8 @@ const columns = [ { key: 'DisciplineCode', label: 'Code', class: 'font-medium' }, { key: 'DisciplineName', label: 'Name' }, + { key: 'SeqScr', label: 'Screen Seq', class: 'w-24 text-center' }, + { key: 'SeqRpt', label: 'Report Seq', class: 'w-24 text-center' }, { key: 'ParentName', label: 'Parent Discipline', class: 'w-48' }, { key: 'actions', label: 'Actions', class: 'w-32 text-center' }, ]; @@ -73,7 +77,7 @@ function openCreateModal() { modalMode = 'create'; - formData = { DisciplineID: null, DisciplineCode: '', DisciplineName: '', Parent: null }; + formData = { DisciplineID: null, DisciplineCode: '', DisciplineName: '', Parent: null, SeqScr: 0, SeqRpt: 0 }; modalOpen = true; } @@ -84,6 +88,8 @@ DisciplineCode: row.DisciplineCode, DisciplineName: row.DisciplineName, Parent: row.Parent || null, + SeqScr: row.SeqScr ?? 0, + SeqRpt: row.SeqRpt ?? 0, }; modalOpen = true; } @@ -97,6 +103,14 @@ toastError('Discipline name is required'); return; } + if (formData.SeqScr === null || formData.SeqScr === undefined || formData.SeqScr === '') { + toastError('Screen sequence is required'); + return; + } + if (formData.SeqRpt === null || formData.SeqRpt === undefined || formData.SeqRpt === '') { + toastError('Report sequence is required'); + return; + } saving = true; try { @@ -271,6 +285,40 @@ Optional parent for hierarchical structure +
+
+ + + Order for screen display +
+
+ + + Order for report display +
+
{#snippet footer()} diff --git a/src/routes/(app)/master-data/organization/site/+page.svelte b/src/routes/(app)/master-data/organization/site/+page.svelte index fdbe696..5fc602a 100644 --- a/src/routes/(app)/master-data/organization/site/+page.svelte +++ b/src/routes/(app)/master-data/organization/site/+page.svelte @@ -10,7 +10,7 @@ import { success as toastSuccess, error as toastError } from '$lib/utils/toast.js'; import DataTable from '$lib/components/DataTable.svelte'; import Modal from '$lib/components/Modal.svelte'; - import { Plus, Edit2, Trash2, ArrowLeft, Search, LandPlot } from 'lucide-svelte'; + import { Plus, Edit2, Trash2, ArrowLeft, Search, LandPlot, Check } from 'lucide-svelte'; let loading = $state(false); let items = $state([]); @@ -29,6 +29,17 @@ let deleting = $state(false); let searchQuery = $state(''); + // Site code validation (must be exactly 2 characters) + let siteCodeLength = $derived(formData.SiteCode?.length || 0); + let isSiteCodeValid = $derived(siteCodeLength === 2); + let siteCodeBadgeClass = $derived( + siteCodeLength === 0 + ? 'badge-ghost' + : isSiteCodeValid + ? 'badge-success' + : 'badge-error' + ); + const columns = [ { key: 'SiteCode', label: 'Code', class: 'font-medium' }, { key: 'SiteName', label: 'Name' }, @@ -103,6 +114,10 @@ toastError('Site code is required'); return; } + if (formData.SiteCode.trim().length !== 2) { + toastError('Site code must be exactly 2 characters'); + return; + } if (!formData.SiteName.trim()) { toastError('Site name is required'); return; @@ -239,15 +254,30 @@ Site Code * - - Unique identifier for this site +
+ 0 && !isSiteCodeValid} + bind:value={formData.SiteCode} + placeholder="e.g., AB" + maxlength="10" + required + /> +
+ + {#if isSiteCodeValid} + + {/if} + {siteCodeLength}/2 + +
+
+ + Must be exactly 2 characters (e.g., AB, NY, 01) +