import { get, post, patch } from './client.js'; /** * @typedef {import('$lib/types/test.types.js').TestSummary} TestSummary * @typedef {import('$lib/types/test.types.js').TestDetail} TestDetail * @typedef {import('$lib/types/test.types.js').CreateTestPayload} CreateTestPayload * @typedef {import('$lib/types/test.types.js').UpdateTestPayload} UpdateTestPayload * @typedef {import('$lib/types/test.types.js').TestListResponse} TestListResponse * @typedef {import('$lib/types/test.types.js').TestDetailResponse} TestDetailResponse * @typedef {import('$lib/types/test.types.js').CreateTestResponse} CreateTestResponse * @typedef {import('$lib/types/test.types.js').UpdateTestResponse} UpdateTestResponse * @typedef {import('$lib/types/test.types.js').DeleteTestResponse} DeleteTestResponse * @typedef {import('$lib/types/test.types.js').TestFilterOptions} TestFilterOptions */ /** * Fetch tests list with optional filters, pagination, and search * @param {TestFilterOptions & { page?: number, perPage?: number }} [params] - Query parameters * @returns {Promise} API response with test data and pagination */ export async function fetchTests(params = {}) { const query = new URLSearchParams(params).toString(); return get(query ? `/api/test?${query}` : '/api/test'); } /** * Fetch a single test by ID * @param {number} id - Test Site ID * @returns {Promise} API response with test detail */ export async function fetchTest(id) { return get(`/api/test/${id}`); } /** * Build payload from form state for API submission * Follows the specification: nested details object for technical fields * @param {any} formData - The form state * @param {boolean} isUpdate - Whether this is an update operation * @returns {CreateTestPayload | UpdateTestPayload} */ function buildPayload(formData, isUpdate = false) { /** @type {CreateTestPayload} */ const payload = { SiteID: formData.SiteID || 1, TestSiteCode: formData.TestSiteCode, TestSiteName: formData.TestSiteName, TestType: formData.TestType, Description: formData.Description, SeqScr: parseInt(formData.SeqScr) || 0, SeqRpt: parseInt(formData.SeqRpt) || 0, VisibleScr: formData.VisibleScr ? 1 : 0, VisibleRpt: formData.VisibleRpt ? 1 : 0, CountStat: formData.CountStat ? 1 : 0, Requestable: formData.Requestable ? 1 : 0, // StartDate is auto-set by backend (created_at) details: {}, refnum: [], reftxt: [], testmap: [] }; // Add TestSiteID for updates if (isUpdate && formData.TestSiteID) { payload.TestSiteID = formData.TestSiteID; } // Build details object based on TestType const type = formData.TestType; // Common fields for TEST, PARAM, CALC if (['TEST', 'PARAM', 'CALC'].includes(type)) { payload.details = { DisciplineID: formData.details?.DisciplineID || null, DepartmentID: formData.details?.DepartmentID || null, ResultType: formData.details?.ResultType || '', RefType: formData.details?.RefType || '', VSet: formData.details?.VSet || '', Unit1: formData.details?.Unit1 || '', Factor: formData.details?.Factor || null, Unit2: formData.details?.Unit2 || '', Decimal: formData.details?.Decimal || 2, ReqQty: formData.details?.ReqQty || null, ReqQtyUnit: formData.details?.ReqQtyUnit || '', CollReq: formData.details?.CollReq || '', Method: formData.details?.Method || '', ExpectedTAT: formData.details?.ExpectedTAT || null }; // Add formula fields for CALC type if (type === 'CALC') { payload.details.FormulaInput = formData.details?.FormulaInput || ''; payload.details.FormulaCode = formData.details?.FormulaCode || ''; } } // Add members for GROUP type if (type === 'GROUP' && formData.details?.members) { payload.details = { members: formData.details.members.map(m => ({ TestSiteID: m.TestSiteID, Member: m.Member })) }; } // Add reference ranges (TEST and PARAM only) if (['TEST', 'PARAM'].includes(type)) { if (formData.refnum && Array.isArray(formData.refnum)) { payload.refnum = formData.refnum.map(r => ({ NumRefType: r.NumRefType, RangeType: r.RangeType, Sex: r.Sex, AgeStart: parseInt(r.AgeStart) || 0, AgeEnd: parseInt(r.AgeEnd) || 54750, // 150 years in days LowSign: r.LowSign || null, Low: r.Low !== null && r.Low !== undefined ? parseFloat(r.Low) : null, HighSign: r.HighSign || null, High: r.High !== null && r.High !== undefined ? parseFloat(r.High) : null, Flag: r.Flag || null, Interpretation: r.Interpretation || null })); } if (formData.reftxt && Array.isArray(formData.reftxt)) { payload.reftxt = formData.reftxt.map(r => ({ TxtRefType: r.TxtRefType, Sex: r.Sex, AgeStart: parseInt(r.AgeStart) || 0, AgeEnd: parseInt(r.AgeEnd) || 54750, // 150 years in days RefTxt: r.RefTxt || '', Flag: r.Flag || null })); } } // Add mappings (all types) if (formData.testmap && Array.isArray(formData.testmap)) { payload.testmap = formData.testmap.map(m => ({ HostType: m.HostType || null, HostID: m.HostID || null, HostDataSource: m.HostDataSource || null, HostTestCode: m.HostTestCode || null, HostTestName: m.HostTestName || null, ClientType: m.ClientType || null, ClientID: m.ClientID || null, ClientDataSource: m.ClientDataSource || null, ConDefID: m.ConDefID || null, ClientTestCode: m.ClientTestCode || null, ClientTestName: m.ClientTestName || null })); } return payload; } /** * Create a new test * @param {any} formData - The form state * @returns {Promise} API response */ export async function createTest(formData) { const payload = buildPayload(formData, false); return post('/api/test', payload); } /** * Update an existing test * @param {any} formData - The form state * @returns {Promise} API response */ export async function updateTest(formData) { const payload = buildPayload(formData, true); return patch('/api/test', payload); } /** * Soft delete a test (set IsActive to '0') * @param {number} id - Test Site ID * @returns {Promise} API response */ export async function deleteTest(id) { return patch('/api/test', { TestSiteID: id, IsActive: '0', }); } /** * Validate test type combination * According to business rules: TestType + ResultType + RefType must be valid * @param {string} testType - The test type * @param {string} resultType - The result type * @param {string} refType - The reference type * @returns {{ valid: boolean, error?: string }} */ export function validateTypeCombination(testType, resultType, refType) { // Valid combinations based on specification const validCombinations = { TEST: { NMRIC: ['RANGE', 'THOLD'], RANGE: ['RANGE', 'THOLD'], TEXT: ['TEXT'], VSET: ['VSET'] }, PARAM: { NMRIC: ['RANGE', 'THOLD'], RANGE: ['RANGE', 'THOLD'], TEXT: ['TEXT'], VSET: ['VSET'] }, CALC: { NMRIC: ['RANGE', 'THOLD'] }, GROUP: { NORES: ['NOREF'] }, TITLE: { NORES: ['NOREF'] } }; const validResultTypes = validCombinations[testType]; if (!validResultTypes) { return { valid: false, error: 'Invalid test type' }; } if (!resultType) { return { valid: true }; // ResultType might be optional } const validRefTypes = validResultTypes[resultType]; if (!validRefTypes) { return { valid: false, error: `Result type '${resultType}' is not valid for '${testType}'` }; } if (refType && !validRefTypes.includes(refType)) { return { valid: false, error: `Reference type '${refType}' is not valid for '${testType}' + '${resultType}'` }; } return { valid: true }; } /** * Get valid result types for a test type * @param {string} testType - The test type * @returns {Array<{value: string, label: string}>} */ export function getResultTypeOptions(testType) { switch (testType) { case 'CALC': return [{ value: 'NMRIC', label: 'Numeric' }]; case 'GROUP': case 'TITLE': return [{ value: 'NORES', label: 'No Result' }]; default: return [ { value: 'NMRIC', label: 'Numeric' }, { value: 'RANGE', label: 'Range' }, { value: 'TEXT', label: 'Text' }, { value: 'VSET', label: 'Value Set' } ]; } } /** * Get valid reference types for a result type * @param {string} resultType - The result type * @returns {Array<{value: string, label: string}>} */ export function getRefTypeOptions(resultType) { switch (resultType) { case 'NMRIC': case 'RANGE': return [ { value: 'RANGE', label: 'Range' }, { value: 'THOLD', label: 'Threshold' } ]; case 'VSET': return [{ value: 'VSET', label: 'Value Set' }]; case 'TEXT': return [{ value: 'TEXT', label: 'Text' }]; case 'NORES': return [{ value: 'NOREF', label: 'No Reference' }]; default: return []; } } /** * Validate test code according to specification * - Required * - 3-6 characters * - Uppercase alphanumeric only * - Regex: ^[A-Z0-9]{3,6}$ * @param {string} code - The test code * @returns {{ valid: boolean, error?: string }} */ export function validateTestCode(code) { if (!code || code.trim() === '') { return { valid: false, error: 'Test code is required' }; } const trimmed = code.trim(); const regex = /^[A-Z0-9]+$/; if (!regex.test(trimmed)) { return { valid: false, error: 'Test code must be uppercase alphanumeric' }; } return { valid: true }; } /** * Validate test name according to specification * - Required * - 3-255 characters * - Alphanumeric with hyphen, space, and parentheses allowed * - Regex: ^[a-zA-Z0-9\s\-\(\)]{3,255}$ * @param {string} name - The test name * @returns {{ valid: boolean, error?: string }} */ export function validateTestName(name) { if (!name || name.trim() === '') { return { valid: false, error: 'Test name is required' }; } const trimmed = name.trim(); if (trimmed.length < 3) { return { valid: false, error: 'Test name must be at least 3 characters' }; } if (trimmed.length > 255) { return { valid: false, error: 'Test name must be at most 255 characters' }; } const regex = /^[a-zA-Z0-9\s\-\(\)]{3,255}$/; if (!regex.test(trimmed)) { return { valid: false, error: 'Test name can only contain letters, numbers, spaces, hyphens, and parentheses' }; } return { valid: true }; }