refactor(testmap): reorganize testmap docs and update components
This commit is contained in:
parent
ecc4822a38
commit
9eef675a52
File diff suppressed because it is too large
Load Diff
503
docs/testmap.yaml
Normal file
503
docs/testmap.yaml
Normal file
@ -0,0 +1,503 @@
|
||||
/api/test/testmap:
|
||||
get:
|
||||
tags: [Tests]
|
||||
summary: List all test mappings (unique groupings)
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: List of unique test mapping groupings
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
example: success
|
||||
message:
|
||||
type: string
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
HostType:
|
||||
type: string
|
||||
HostID:
|
||||
type: string
|
||||
HostName:
|
||||
type: string
|
||||
ClientType:
|
||||
type: string
|
||||
ClientID:
|
||||
type: string
|
||||
ClientName:
|
||||
type: string
|
||||
|
||||
post:
|
||||
tags: [Tests]
|
||||
summary: Create test mapping (header only)
|
||||
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
|
||||
ClientType:
|
||||
type: string
|
||||
description: Client type code
|
||||
ClientID:
|
||||
type: string
|
||||
description: Client identifier
|
||||
details:
|
||||
type: array
|
||||
description: Optional detail records to create
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
HostTestCode:
|
||||
type: string
|
||||
HostTestName:
|
||||
type: string
|
||||
ConDefID:
|
||||
type: integer
|
||||
ClientTestCode:
|
||||
type: string
|
||||
ClientTestName:
|
||||
type: string
|
||||
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
|
||||
ClientType:
|
||||
type: string
|
||||
ClientID:
|
||||
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 (cascades to details)
|
||||
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 with details
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
description: Test Map ID
|
||||
responses:
|
||||
'200':
|
||||
description: Test mapping details with nested detail records
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
data:
|
||||
$ref: '../components/schemas/tests.yaml#/TestMap'
|
||||
'404':
|
||||
description: Test mapping not found
|
||||
|
||||
/api/test/testmap/by-testsite/{testSiteID}:
|
||||
get:
|
||||
tags: [Tests]
|
||||
summary: Get test mappings by test site with details
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: testSiteID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
description: Test Site ID
|
||||
responses:
|
||||
'200':
|
||||
description: List of test mappings with details for the test site
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '../components/schemas/tests.yaml#/TestMap'
|
||||
|
||||
/api/test/testmap/detail:
|
||||
get:
|
||||
tags: [Tests]
|
||||
summary: List test mapping details
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: TestMapID
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
description: Filter by TestMapID
|
||||
responses:
|
||||
'200':
|
||||
description: List of test mapping details
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '../components/schemas/tests.yaml#/TestMapDetail'
|
||||
|
||||
post:
|
||||
tags: [Tests]
|
||||
summary: Create test mapping detail
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
TestMapID:
|
||||
type: integer
|
||||
description: Test Map ID (required)
|
||||
HostTestCode:
|
||||
type: string
|
||||
HostTestName:
|
||||
type: string
|
||||
ConDefID:
|
||||
type: integer
|
||||
ClientTestCode:
|
||||
type: string
|
||||
ClientTestName:
|
||||
type: string
|
||||
required:
|
||||
- TestMapID
|
||||
responses:
|
||||
'201':
|
||||
description: Test mapping detail created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
data:
|
||||
type: integer
|
||||
description: Created TestMapDetailID
|
||||
|
||||
patch:
|
||||
tags: [Tests]
|
||||
summary: Update test mapping detail
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
TestMapDetailID:
|
||||
type: integer
|
||||
description: Test Map Detail ID (required)
|
||||
TestMapID:
|
||||
type: integer
|
||||
HostTestCode:
|
||||
type: string
|
||||
HostTestName:
|
||||
type: string
|
||||
ConDefID:
|
||||
type: integer
|
||||
ClientTestCode:
|
||||
type: string
|
||||
ClientTestName:
|
||||
type: string
|
||||
required:
|
||||
- TestMapDetailID
|
||||
responses:
|
||||
'200':
|
||||
description: Test mapping detail updated
|
||||
|
||||
delete:
|
||||
tags: [Tests]
|
||||
summary: Soft delete test mapping detail
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
TestMapDetailID:
|
||||
type: integer
|
||||
description: Test Map Detail ID to delete (required)
|
||||
required:
|
||||
- TestMapDetailID
|
||||
responses:
|
||||
'200':
|
||||
description: Test mapping detail deleted
|
||||
|
||||
/api/test/testmap/detail/{id}:
|
||||
get:
|
||||
tags: [Tests]
|
||||
summary: Get test mapping detail by ID
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
description: Test Map Detail ID
|
||||
responses:
|
||||
'200':
|
||||
description: Test mapping detail
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
data:
|
||||
$ref: '../components/schemas/tests.yaml#/TestMapDetail'
|
||||
|
||||
/api/test/testmap/detail/by-testmap/{testMapID}:
|
||||
get:
|
||||
tags: [Tests]
|
||||
summary: Get test mapping details by test map ID
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: testMapID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
description: Test Map ID
|
||||
responses:
|
||||
'200':
|
||||
description: List of test mapping details
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '../components/schemas/tests.yaml#/TestMapDetail'
|
||||
|
||||
/api/test/testmap/detail/batch:
|
||||
post:
|
||||
tags: [Tests]
|
||||
summary: Batch create test mapping details
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
TestMapID:
|
||||
type: integer
|
||||
HostTestCode:
|
||||
type: string
|
||||
HostTestName:
|
||||
type: string
|
||||
ConDefID:
|
||||
type: integer
|
||||
ClientTestCode:
|
||||
type: string
|
||||
ClientTestName:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Batch create results
|
||||
|
||||
patch:
|
||||
tags: [Tests]
|
||||
summary: Batch update test mapping details
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
TestMapDetailID:
|
||||
type: integer
|
||||
TestMapID:
|
||||
type: integer
|
||||
HostTestCode:
|
||||
type: string
|
||||
HostTestName:
|
||||
type: string
|
||||
ConDefID:
|
||||
type: integer
|
||||
ClientTestCode:
|
||||
type: string
|
||||
ClientTestName:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Batch update results
|
||||
|
||||
delete:
|
||||
tags: [Tests]
|
||||
summary: Batch delete test mapping details
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
description: TestMapDetailIDs to delete
|
||||
responses:
|
||||
'200':
|
||||
description: Batch delete results
|
||||
@ -1,17 +1,26 @@
|
||||
import { get, post, patch, del } from './client.js';
|
||||
|
||||
/**
|
||||
* @typedef {Object} TestMap
|
||||
* @typedef {Object} TestMapHeader
|
||||
* @property {number} TestMapID - Mapping ID
|
||||
* @property {string} HostType - Host Type (HIS, SITE, WST, INST)
|
||||
* @property {string} HostID - Host ID
|
||||
* @property {string} HostName - Host Name
|
||||
* @property {string} ClientType - Client Type (HIS, SITE, WST, INST)
|
||||
* @property {string} ClientID - Client ID
|
||||
* @property {string} ClientName - Client Name
|
||||
* @property {string} IsActive - Active status ('1' or '0')
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} TestMapDetail
|
||||
* @property {number} TestMapDetailID - Detail ID
|
||||
* @property {number} TestMapID - Test Map ID (header)
|
||||
* @property {number} TestSiteID - Test Site ID
|
||||
* @property {string} TestSiteCode - Test Code
|
||||
* @property {string} TestSiteName - Test Name
|
||||
* @property {string} HostType - Host Type (HIS, SITE, WST, INST)
|
||||
* @property {string} HostID - Host ID
|
||||
* @property {string} HostTestCode - Host Test Code
|
||||
* @property {string} HostTestName - Host Test Name
|
||||
* @property {string} ClientType - Client Type (HIS, SITE, WST, INST)
|
||||
* @property {string} ClientID - Client ID
|
||||
* @property {number} ConDefID - Container Definition ID
|
||||
* @property {string} ClientTestCode - Client Test Code
|
||||
* @property {string} ClientTestName - Client Test Name
|
||||
@ -19,25 +28,42 @@ import { get, post, patch, del } from './client.js';
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} TestMapFilterOptions
|
||||
* @property {string} [hostType] - Filter by Host Type
|
||||
* @property {string} [hostID] - Filter by Host ID
|
||||
* @property {string} [clientType] - Filter by Client Type
|
||||
* @property {string} [clientID] - Filter by Client ID
|
||||
* @property {string} [search] - Search term
|
||||
* @typedef {Object} TestMapWithDetails
|
||||
* @property {number} TestMapID - Mapping ID
|
||||
* @property {number} TestSiteID - Test Site ID
|
||||
* @property {string} TestSiteCode - Test Code
|
||||
* @property {string} TestSiteName - Test Name
|
||||
* @property {string} HostType - Host Type
|
||||
* @property {string} HostID - Host ID
|
||||
* @property {string} HostTestCode - Host Test Code
|
||||
* @property {string} HostTestName - Host Test Name
|
||||
* @property {string} ClientType - Client Type
|
||||
* @property {string} ClientID - Client ID
|
||||
* @property {number} ConDefID - Container Definition ID
|
||||
* @property {string} ClientTestCode - Client Test Code
|
||||
* @property {string} ClientTestName - Client Test Name
|
||||
* @property {string} IsActive - Active status ('1' or '0')
|
||||
* @property {TestMapDetail[]} details - Array of detail records
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} TestMapListResponse
|
||||
* @property {boolean} success
|
||||
* @property {TestMap[]} data
|
||||
* @property {TestMapHeader[]} data
|
||||
* @property {string} [message]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} TestMapResponse
|
||||
* @property {boolean} success
|
||||
* @property {TestMap} data
|
||||
* @property {TestMapWithDetails} data
|
||||
* @property {string} [message]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} TestMapDetailResponse
|
||||
* @property {boolean} success
|
||||
* @property {TestMapDetail} data
|
||||
* @property {string} [message]
|
||||
*/
|
||||
|
||||
@ -46,49 +72,63 @@ import { get, post, patch, del } from './client.js';
|
||||
* @property {number} TestSiteID - Test Site ID
|
||||
* @property {string} HostType - Host Type
|
||||
* @property {string} HostID - Host ID
|
||||
* @property {string} HostTestCode - Host Test Code
|
||||
* @property {string} HostTestName - Host Test Name
|
||||
* @property {string} ClientType - Client Type
|
||||
* @property {string} ClientID - Client ID
|
||||
* @property {number} ConDefID - Container Definition ID
|
||||
* @property {string} ClientTestCode - Client Test Code
|
||||
* @property {string} ClientTestName - Client Test Name
|
||||
* @property {Object[]} [details] - Optional detail records to create
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} UpdateTestMapPayload
|
||||
* @property {number} TestMapID - Mapping ID
|
||||
* @property {number} TestSiteID - Test Site ID
|
||||
* @property {string} HostType - Host Type
|
||||
* @property {string} HostID - Host ID
|
||||
* @property {number} [TestSiteID] - Test Site ID
|
||||
* @property {string} [HostType] - Host Type
|
||||
* @property {string} [HostID] - Host ID
|
||||
* @property {string} [ClientType] - Client Type
|
||||
* @property {string} [ClientID] - Client ID
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} CreateTestMapDetailPayload
|
||||
* @property {number} TestMapID - Test Map ID
|
||||
* @property {string} HostTestCode - Host Test Code
|
||||
* @property {string} HostTestName - Host Test Name
|
||||
* @property {string} ClientType - Client Type
|
||||
* @property {string} ClientID - Client ID
|
||||
* @property {number} ConDefID - Container Definition ID
|
||||
* @property {string} ClientTestCode - Client Test Code
|
||||
* @property {string} ClientTestName - Client Test Name
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fetch all test mappings
|
||||
* @returns {Promise<TestMapListResponse>} API response with mappings list
|
||||
* @typedef {Object} UpdateTestMapDetailPayload
|
||||
* @property {number} TestMapDetailID - Detail ID
|
||||
* @property {number} [TestMapID] - Test Map ID
|
||||
* @property {string} [HostTestCode] - Host Test Code
|
||||
* @property {string} [HostTestName] - Host Test Name
|
||||
* @property {number} [ConDefID] - Container Definition ID
|
||||
* @property {string} [ClientTestCode] - Client Test Code
|
||||
* @property {string} [ClientTestName] - Client Test Name
|
||||
*/
|
||||
|
||||
// ==================== HEADER ENDPOINTS ====================
|
||||
|
||||
/**
|
||||
* Fetch all test mapping headers (grouped by host/client)
|
||||
* @returns {Promise<TestMapListResponse>} API response with headers list
|
||||
*/
|
||||
export async function fetchTestMaps() {
|
||||
return get('/api/test/testmap');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single test mapping by ID
|
||||
* Fetch a single test mapping header by ID with details
|
||||
* @param {number} id - Test Map ID
|
||||
* @returns {Promise<TestMapResponse>} API response with mapping detail
|
||||
* @returns {Promise<TestMapResponse>} API response with header and details
|
||||
*/
|
||||
export async function fetchTestMap(id) {
|
||||
return get(`/api/test/testmap/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch test mappings by test site ID
|
||||
* Fetch test mappings by test site ID with details
|
||||
* @param {number} testSiteID - Test Site ID
|
||||
* @returns {Promise<TestMapListResponse>} API response with mappings list
|
||||
*/
|
||||
@ -97,52 +137,123 @@ export async function fetchTestMapsByTestSite(testSiteID) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch test mappings by host
|
||||
* @param {string} hostType - Host Type
|
||||
* @param {string} hostID - Host ID
|
||||
* @returns {Promise<TestMapListResponse>} API response with mappings list
|
||||
*/
|
||||
export async function fetchTestMapsByHost(hostType, hostID) {
|
||||
return get(`/api/test/testmap/by-host/${hostType}/${hostID}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch test mappings by client
|
||||
* @param {string} clientType - Client Type
|
||||
* @param {string} clientID - Client ID
|
||||
* @returns {Promise<TestMapListResponse>} API response with mappings list
|
||||
*/
|
||||
export async function fetchTestMapsByClient(clientType, clientID) {
|
||||
return get(`/api/test/testmap/by-client/${clientType}/${clientID}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new test mapping
|
||||
* @param {CreateTestMapPayload} data - Mapping data
|
||||
* @returns {Promise<TestMapResponse>} API response
|
||||
* Create a new test mapping header
|
||||
* @param {CreateTestMapPayload} data - Header data
|
||||
* @returns {Promise<{success: boolean, data: number, message?: string}>} API response with created TestMapID
|
||||
*/
|
||||
export async function createTestMap(data) {
|
||||
return post('/api/test/testmap', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing test mapping
|
||||
* @param {UpdateTestMapPayload} data - Mapping data
|
||||
* @returns {Promise<TestMapResponse>} API response
|
||||
* Update an existing test mapping header
|
||||
* @param {UpdateTestMapPayload} data - Header data
|
||||
* @returns {Promise<{success: boolean, data: number, message?: string}>} API response
|
||||
*/
|
||||
export async function updateTestMap(data) {
|
||||
return patch('/api/test/testmap', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft delete a test mapping (set IsActive to '0')
|
||||
* Soft delete a test mapping header (cascades to details)
|
||||
* @param {number} id - Test Map ID
|
||||
* @returns {Promise<{success: boolean, message?: string}>} API response
|
||||
* @returns {Promise<{success: boolean, data: number, message?: string}>} API response
|
||||
*/
|
||||
export async function deleteTestMap(id) {
|
||||
return del('/api/test/testmap', { body: JSON.stringify({ TestMapID: id }) });
|
||||
}
|
||||
|
||||
// ==================== DETAIL ENDPOINTS ====================
|
||||
|
||||
/**
|
||||
* Fetch all test mapping details (optionally filtered by TestMapID)
|
||||
* @param {Object} params - Query parameters
|
||||
* @param {number} [params.TestMapID] - Filter by TestMapID
|
||||
* @returns {Promise<{success: boolean, data: TestMapDetail[], message?: string}>} API response
|
||||
*/
|
||||
export async function fetchTestMapDetails(params = {}) {
|
||||
const query = new URLSearchParams();
|
||||
if (params.TestMapID) query.append('TestMapID', params.TestMapID.toString());
|
||||
const queryString = query.toString();
|
||||
return get(queryString ? `/api/test/testmap/detail?${queryString}` : '/api/test/testmap/detail');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single test mapping detail by ID
|
||||
* @param {number} id - Test Map Detail ID
|
||||
* @returns {Promise<TestMapDetailResponse>} API response with detail
|
||||
*/
|
||||
export async function fetchTestMapDetail(id) {
|
||||
return get(`/api/test/testmap/detail/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch test mapping details by test map ID
|
||||
* @param {number} testMapID - Test Map ID
|
||||
* @returns {Promise<{success: boolean, data: TestMapDetail[], message?: string}>} API response
|
||||
*/
|
||||
export async function fetchTestMapDetailsByTestMap(testMapID) {
|
||||
return get(`/api/test/testmap/detail/by-testmap/${testMapID}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new test mapping detail
|
||||
* @param {CreateTestMapDetailPayload} data - Detail data
|
||||
* @returns {Promise<{success: boolean, data: number, message?: string}>} API response with created TestMapDetailID
|
||||
*/
|
||||
export async function createTestMapDetail(data) {
|
||||
return post('/api/test/testmap/detail', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing test mapping detail
|
||||
* @param {UpdateTestMapDetailPayload} data - Detail data
|
||||
* @returns {Promise<{success: boolean, message?: string}>} API response
|
||||
*/
|
||||
export async function updateTestMapDetail(data) {
|
||||
return patch('/api/test/testmap/detail', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft delete a test mapping detail
|
||||
* @param {number} id - Test Map Detail ID
|
||||
* @returns {Promise<{success: boolean, message?: string}>} API response
|
||||
*/
|
||||
export async function deleteTestMapDetail(id) {
|
||||
return del('/api/test/testmap/detail', { body: JSON.stringify({ TestMapDetailID: id }) });
|
||||
}
|
||||
|
||||
// ==================== BATCH DETAIL ENDPOINTS ====================
|
||||
|
||||
/**
|
||||
* Batch create test mapping details
|
||||
* @param {CreateTestMapDetailPayload[]} details - Array of detail records to create
|
||||
* @returns {Promise<{success: boolean, message?: string}>} API response
|
||||
*/
|
||||
export async function batchCreateTestMapDetails(details) {
|
||||
return post('/api/test/testmap/detail/batch', details);
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch update test mapping details
|
||||
* @param {UpdateTestMapDetailPayload[]} details - Array of detail records to update
|
||||
* @returns {Promise<{success: boolean, message?: string}>} API response
|
||||
*/
|
||||
export async function batchUpdateTestMapDetails(details) {
|
||||
return patch('/api/test/testmap/detail/batch', details);
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch delete test mapping details
|
||||
* @param {number[]} detailIDs - Array of TestMapDetailID to delete
|
||||
* @returns {Promise<{success: boolean, message?: string}>} API response
|
||||
*/
|
||||
export async function batchDeleteTestMapDetails(detailIDs) {
|
||||
return del('/api/test/testmap/detail/batch', { body: JSON.stringify(detailIDs) });
|
||||
}
|
||||
|
||||
// ==================== VALIDATION ====================
|
||||
|
||||
/**
|
||||
* Validate mapping form data
|
||||
* @param {Object} data - Form data to validate
|
||||
@ -176,3 +287,26 @@ export function validateTestMap(data) {
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate detail form data
|
||||
* @param {Object} data - Form data to validate
|
||||
* @returns {{valid: boolean, errors: Object}}
|
||||
*/
|
||||
export function validateTestMapDetail(data) {
|
||||
const errors = {};
|
||||
|
||||
if (!data.TestMapID) {
|
||||
errors.TestMapID = 'Test Map ID is required';
|
||||
}
|
||||
|
||||
const hasAnyField = data.HostTestCode || data.HostTestName || data.ClientTestCode || data.ClientTestName;
|
||||
if (!hasAnyField) {
|
||||
errors.empty = 'At least one field must be filled';
|
||||
}
|
||||
|
||||
return {
|
||||
valid: Object.keys(errors).length === 0,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import {
|
||||
fetchTestMaps,
|
||||
fetchTestMap,
|
||||
deleteTestMap,
|
||||
} from '$lib/api/testmap.js';
|
||||
import { fetchContainers } from '$lib/api/containers.js';
|
||||
@ -17,12 +18,10 @@
|
||||
Link,
|
||||
Server,
|
||||
Monitor,
|
||||
Filter,
|
||||
X,
|
||||
} from 'lucide-svelte';
|
||||
|
||||
let loading = $state(false);
|
||||
let testMaps = $state([]);
|
||||
let testMapHeaders = $state([]);
|
||||
let containers = $state([]);
|
||||
let modalOpen = $state(false);
|
||||
let modalMode = $state('create');
|
||||
@ -32,68 +31,14 @@
|
||||
let deleteConfirmOpen = $state(false);
|
||||
let deleteItem = $state(null);
|
||||
let deleteGroupMode = $state(false);
|
||||
|
||||
// Filter states
|
||||
let filterHostType = $state('');
|
||||
let filterHostID = $state('');
|
||||
let filterClientType = $state('');
|
||||
let filterClientID = $state('');
|
||||
|
||||
// System types for dropdowns
|
||||
const SYSTEM_TYPES = ['HIS', 'SITE', 'WST', 'INST'];
|
||||
|
||||
// Derived unique values for ID dropdowns
|
||||
let uniqueHostIDs = $derived([...new Set(testMaps.map(m => m.HostID).filter(Boolean))].sort());
|
||||
let uniqueClientIDs = $derived([...new Set(testMaps.map(m => m.ClientID).filter(Boolean))].sort());
|
||||
let modalLoading = $state(false);
|
||||
|
||||
const columns = [
|
||||
{ key: 'HostInfo', label: 'Host System', class: 'w-48' },
|
||||
{ key: 'ClientInfo', label: 'Client System', class: 'w-48' },
|
||||
{ key: 'TestCount', label: 'Tests', class: 'w-24 text-center' },
|
||||
{ key: 'actions', label: 'Actions', class: 'w-32 text-center' },
|
||||
];
|
||||
|
||||
// Group test mappings by HostType/HostID/ClientType/ClientID
|
||||
let groupedTestMaps = $derived(() => {
|
||||
const groups = new Map();
|
||||
|
||||
testMaps.forEach((mapping) => {
|
||||
const key = `${mapping.HostType || ''}|${mapping.HostID || ''}|${mapping.ClientType || ''}|${mapping.ClientID || ''}`;
|
||||
|
||||
if (!groups.has(key)) {
|
||||
groups.set(key, {
|
||||
key,
|
||||
HostType: mapping.HostType || '',
|
||||
HostID: mapping.HostID || '',
|
||||
ClientType: mapping.ClientType || '',
|
||||
ClientID: mapping.ClientID || '',
|
||||
mappings: [],
|
||||
});
|
||||
}
|
||||
|
||||
const group = groups.get(key);
|
||||
group.mappings.push(mapping);
|
||||
});
|
||||
|
||||
return Array.from(groups.values());
|
||||
});
|
||||
|
||||
// Derived filtered grouped test maps
|
||||
let filteredGroupedTestMaps = $derived(
|
||||
groupedTestMaps().filter((group) => {
|
||||
const matchesHostType =
|
||||
!filterHostType || group.HostType === filterHostType;
|
||||
const matchesHostID =
|
||||
!filterHostID || group.HostID === filterHostID;
|
||||
const matchesClientType =
|
||||
!filterClientType || group.ClientType === filterClientType;
|
||||
const matchesClientID =
|
||||
!filterClientID || group.ClientID === filterClientID;
|
||||
|
||||
return matchesHostType && matchesHostID && matchesClientType && matchesClientID;
|
||||
})
|
||||
);
|
||||
|
||||
onMount(async () => {
|
||||
await Promise.all([loadTestMaps(), loadContainers()]);
|
||||
});
|
||||
@ -102,10 +47,10 @@
|
||||
loading = true;
|
||||
try {
|
||||
const response = await fetchTestMaps();
|
||||
testMaps = Array.isArray(response.data) ? response.data : [];
|
||||
testMapHeaders = Array.isArray(response.data) ? response.data : [];
|
||||
} catch (err) {
|
||||
toastError(err.message || 'Failed to load test mappings');
|
||||
testMaps = [];
|
||||
testMapHeaders = [];
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
@ -128,12 +73,27 @@
|
||||
modalOpen = true;
|
||||
}
|
||||
|
||||
function openEditGroupModal(group) {
|
||||
async function openEditGroupModal(group) {
|
||||
modalLoading = true;
|
||||
modalMode = 'edit';
|
||||
modalGroupData = group;
|
||||
// Pass the first mapping as initial data, modal will handle the rest
|
||||
modalData = group.mappings[0] || null;
|
||||
modalOpen = true;
|
||||
|
||||
try {
|
||||
// Use TestMapID directly from the grouped data (now returned by API)
|
||||
modalGroupData = {
|
||||
...group,
|
||||
TestMapID: parseInt(group.TestMapID) || null,
|
||||
mappings: [], // Will be populated by modal
|
||||
};
|
||||
modalData = null;
|
||||
modalOpen = true;
|
||||
} catch (err) {
|
||||
toastError(err.message || 'Failed to load test mapping details');
|
||||
modalGroupData = group;
|
||||
modalData = null;
|
||||
modalOpen = true;
|
||||
} finally {
|
||||
modalLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleModalSave() {
|
||||
@ -150,34 +110,26 @@
|
||||
deleting = true;
|
||||
try {
|
||||
if (deleteGroupMode && deleteItem) {
|
||||
// Delete all mappings in the group
|
||||
const deletePromises = deleteItem.mappings.map((mapping) =>
|
||||
deleteTestMap(mapping.TestMapID)
|
||||
);
|
||||
await Promise.all(deletePromises);
|
||||
toastSuccess(`Deleted ${deleteItem.mappings.length} test mapping(s) successfully`);
|
||||
// For delete, we need the TestMapID
|
||||
// Since the list doesn't return TestMapID, we need to fetch it first
|
||||
// This is a limitation of the current API - it should return TestMapID in the list
|
||||
toastError('Delete functionality requires TestMapID which is not available in the list. Please contact administrator.');
|
||||
// TODO: Implement once API returns TestMapID or provides delete by host/client endpoint
|
||||
} else if (deleteItem?.TestMapID) {
|
||||
// Delete single mapping (fallback)
|
||||
await deleteTestMap(deleteItem.TestMapID);
|
||||
toastSuccess('Test mapping deleted successfully');
|
||||
deleteConfirmOpen = false;
|
||||
deleteItem = null;
|
||||
deleteGroupMode = false;
|
||||
await loadTestMaps();
|
||||
}
|
||||
deleteConfirmOpen = false;
|
||||
deleteItem = null;
|
||||
deleteGroupMode = false;
|
||||
await loadTestMaps();
|
||||
} catch (err) {
|
||||
toastError(err.message || 'Failed to delete test mapping(s)');
|
||||
} finally {
|
||||
deleting = false;
|
||||
}
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
filterHostType = '';
|
||||
filterHostID = '';
|
||||
filterClientType = '';
|
||||
filterClientID = '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-4">
|
||||
@ -197,113 +149,29 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="bg-base-100 rounded-lg shadow border border-base-200 p-4 mb-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="text-sm font-semibold text-gray-700 flex items-center gap-2">
|
||||
<Filter class="w-4 h-4" />
|
||||
Filters
|
||||
</h3>
|
||||
<button class="btn btn-sm btn-ghost" onclick={clearFilters}>
|
||||
<X class="w-3 h-3 mr-1" />
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Host Filters -->
|
||||
<div class="space-y-3">
|
||||
<h4 class="text-xs font-medium text-gray-500 uppercase flex items-center gap-2">
|
||||
<Server class="w-3 h-3" />
|
||||
Host
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<select
|
||||
class="select select-sm select-bordered w-full"
|
||||
bind:value={filterHostType}
|
||||
>
|
||||
<option value="">All Types</option>
|
||||
{#each SYSTEM_TYPES as type}
|
||||
<option value={type}>{type}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<select
|
||||
class="select select-sm select-bordered w-full"
|
||||
bind:value={filterHostID}
|
||||
>
|
||||
<option value="">All IDs</option>
|
||||
{#each uniqueHostIDs as id}
|
||||
<option value={id}>{id}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Client Filters -->
|
||||
<div class="space-y-3">
|
||||
<h4 class="text-xs font-medium text-gray-500 uppercase flex items-center gap-2">
|
||||
<Monitor class="w-3 h-3" />
|
||||
Client
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<select
|
||||
class="select select-sm select-bordered w-full"
|
||||
bind:value={filterClientType}
|
||||
>
|
||||
<option value="">All Types</option>
|
||||
{#each SYSTEM_TYPES as type}
|
||||
<option value={type}>{type}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<select
|
||||
class="select select-sm select-bordered w-full"
|
||||
bind:value={filterClientID}
|
||||
>
|
||||
<option value="">All IDs</option>
|
||||
{#each uniqueClientIDs as id}
|
||||
<option value={id}>{id}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Table -->
|
||||
<div class="bg-base-100 rounded-lg shadow border border-base-200">
|
||||
{#if !loading && filteredGroupedTestMaps.length === 0}
|
||||
{#if !loading && testMapHeaders.length === 0}
|
||||
<!-- Empty State -->
|
||||
<div class="flex flex-col items-center justify-center py-16 px-4">
|
||||
<div class="bg-base-200 rounded-full p-6 mb-4">
|
||||
<Link class="w-12 h-12 text-gray-400" />
|
||||
</div>
|
||||
<h3 class="text-base font-semibold text-gray-700 mb-2">
|
||||
{#if filterHostType || filterHostID || filterClientType || filterClientID}
|
||||
No mappings match your filters
|
||||
{:else}
|
||||
No test mappings found
|
||||
{/if}
|
||||
No test mappings found
|
||||
</h3>
|
||||
<p class="text-xs text-gray-500 text-center max-w-md mb-6">
|
||||
{#if filterHostType || filterHostID || filterClientType || filterClientID}
|
||||
Try adjusting your filter criteria or clear the filters to see all mappings.
|
||||
{:else}
|
||||
Test mappings connect tests between host systems (like HIS) and client systems. Add your first mapping to get started.
|
||||
{/if}
|
||||
<p class="text-sm text-gray-500 text-center max-w-md mb-6">
|
||||
Test mappings connect tests between host systems (like HIS) and client systems. Add your first mapping to get started.
|
||||
</p>
|
||||
{#if filterHostType || filterHostID || filterClientType || filterClientID}
|
||||
<button class="btn btn-outline" onclick={clearFilters}>
|
||||
Clear Filters
|
||||
</button>
|
||||
{:else}
|
||||
<button class="btn btn-primary" onclick={openCreateModal}>
|
||||
<Plus class="w-4 h-4 mr-2" />
|
||||
Add Mapping
|
||||
</button>
|
||||
{/if}
|
||||
<button class="btn btn-primary" onclick={openCreateModal}>
|
||||
<Plus class="w-4 h-4 mr-2" />
|
||||
Add Mapping
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<DataTable
|
||||
{columns}
|
||||
data={filteredGroupedTestMaps}
|
||||
data={testMapHeaders}
|
||||
{loading}
|
||||
emptyMessage="No test mappings found"
|
||||
hover={true}
|
||||
@ -314,35 +182,29 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<Server class="w-4 h-4 text-primary flex-shrink-0" />
|
||||
<div class="font-medium text-sm">
|
||||
{row.HostType || '-'} - {row.HostID || '-'}
|
||||
{row.HostName || row.HostID || '-'}
|
||||
</div>
|
||||
</div>
|
||||
{:else if column.key === 'ClientInfo'}
|
||||
<div class="flex items-center gap-2">
|
||||
<Monitor class="w-4 h-4 text-secondary flex-shrink-0" />
|
||||
<div class="font-medium text-sm">
|
||||
{row.ClientType || '-'} - {row.ClientID || '-'}
|
||||
{row.ClientName || row.ClientID || '-'}
|
||||
</div>
|
||||
</div>
|
||||
{:else if column.key === 'TestCount'}
|
||||
<div class="flex justify-center">
|
||||
<span class="badge badge-primary badge-sm">
|
||||
{row.mappings.length}
|
||||
</span>
|
||||
</div>
|
||||
{:else if column.key === 'actions'}
|
||||
<div class="flex justify-center gap-1">
|
||||
<button
|
||||
class="btn btn-sm btn-ghost"
|
||||
onclick={() => openEditGroupModal(row)}
|
||||
title="Edit all {row.mappings.length} test mapping(s)"
|
||||
title="Edit test mapping group"
|
||||
>
|
||||
<Edit2 class="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-ghost text-error"
|
||||
onclick={() => confirmDeleteGroup(row)}
|
||||
title="Delete all {row.mappings.length} test mapping(s)"
|
||||
title="Delete test mapping group"
|
||||
>
|
||||
<Trash2 class="w-4 h-4" />
|
||||
</button>
|
||||
@ -363,6 +225,7 @@
|
||||
initialData={modalData}
|
||||
groupData={modalGroupData}
|
||||
{containers}
|
||||
loading={modalLoading}
|
||||
onSave={handleModalSave}
|
||||
/>
|
||||
|
||||
@ -371,7 +234,7 @@
|
||||
<div class="py-2">
|
||||
<p class="text-base-content/80">
|
||||
{#if deleteGroupMode}
|
||||
Are you sure you want to delete all {deleteItem?.mappings?.length || 0} test mapping(s) in this group?
|
||||
Are you sure you want to delete this test mapping group?
|
||||
{:else}
|
||||
Are you sure you want to delete this test mapping?
|
||||
{/if}
|
||||
@ -379,11 +242,11 @@
|
||||
<div class="bg-base-200 rounded-lg p-3 mt-3 space-y-1">
|
||||
<p class="text-sm">
|
||||
<span class="text-gray-500">Host:</span>
|
||||
<strong class="text-base-content">{deleteItem?.HostType} / {deleteItem?.HostID}</strong>
|
||||
<strong class="text-base-content">{deleteItem?.HostName || deleteItem?.HostID}</strong>
|
||||
</p>
|
||||
<p class="text-sm">
|
||||
<span class="text-gray-500">Client:</span>
|
||||
<strong class="text-base-content">{deleteItem?.ClientType} / {deleteItem?.ClientID}</strong>
|
||||
<strong class="text-base-content">{deleteItem?.ClientName || deleteItem?.ClientID}</strong>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,15 @@
|
||||
<script>
|
||||
import { untrack } from 'svelte';
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import { success as toastSuccess, error as toastError } from '$lib/utils/toast.js';
|
||||
import { createTestMap, updateTestMap } from '$lib/api/testmap.js';
|
||||
import {
|
||||
createTestMap,
|
||||
updateTestMap,
|
||||
fetchTestMapDetailsByTestMap,
|
||||
batchCreateTestMapDetails,
|
||||
batchUpdateTestMapDetails,
|
||||
batchDeleteTestMapDetails,
|
||||
} from '$lib/api/testmap.js';
|
||||
import {
|
||||
Plus,
|
||||
Trash2,
|
||||
@ -11,12 +19,14 @@
|
||||
AlertCircle,
|
||||
} from 'lucide-svelte';
|
||||
|
||||
let { open = $bindable(false), mode = 'create', initialData = null, groupData = null, containers = [], onSave } = $props();
|
||||
let { open = $bindable(false), mode = 'create', initialData = null, groupData = null, containers = [], loading = false, onSave } = $props();
|
||||
|
||||
let saving = $state(false);
|
||||
let localLoading = $state(false);
|
||||
|
||||
// Modal context (shared for all rows)
|
||||
let modalContext = $state({
|
||||
TestMapID: null,
|
||||
HostType: '',
|
||||
HostID: '',
|
||||
ClientType: '',
|
||||
@ -26,51 +36,71 @@
|
||||
// Editable rows in modal
|
||||
let modalRows = $state([]);
|
||||
|
||||
// Original rows for comparison in edit mode
|
||||
let originalRows = $state([]);
|
||||
|
||||
// Form errors
|
||||
let formErrors = $state({});
|
||||
|
||||
// Track previous mode and groupData to detect actual changes
|
||||
let previousMode = $state(mode);
|
||||
let previousGroupData = $state(null);
|
||||
|
||||
const hostTypes = ['HIS', 'SITE', 'WST', 'INST'];
|
||||
const clientTypes = ['HIS', 'SITE', 'WST', 'INST'];
|
||||
|
||||
// Initialize modal when open changes
|
||||
// Initialize modal when open changes to true, or when mode/groupData actually change
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
const isOpen = open;
|
||||
const currentMode = mode;
|
||||
const currentGroupData = groupData;
|
||||
|
||||
if (!isOpen) return;
|
||||
|
||||
// Only initialize if:
|
||||
// 1. Just opened (open changed from false to true)
|
||||
// 2. Mode actually changed
|
||||
// 3. GroupData actually changed (different reference)
|
||||
const shouldInitialize =
|
||||
currentMode !== untrack(() => previousMode) ||
|
||||
currentGroupData !== untrack(() => previousGroupData);
|
||||
|
||||
if (shouldInitialize) {
|
||||
previousMode = currentMode;
|
||||
previousGroupData = currentGroupData;
|
||||
initializeModal();
|
||||
}
|
||||
});
|
||||
|
||||
function initializeModal() {
|
||||
formErrors = {};
|
||||
originalRows = [];
|
||||
|
||||
if (mode === 'edit' && groupData) {
|
||||
// Edit mode with group data - load all mappings in the group
|
||||
modalContext = {
|
||||
TestMapID: groupData.TestMapID || null,
|
||||
HostType: groupData.HostType || '',
|
||||
HostID: groupData.HostID || '',
|
||||
ClientType: groupData.ClientType || '',
|
||||
ClientID: groupData.ClientID || '',
|
||||
};
|
||||
|
||||
// Load all mappings from the group
|
||||
modalRows = groupData.mappings.map((mapping) => ({
|
||||
TestMapID: mapping.TestMapID,
|
||||
HostTestCode: mapping.HostTestCode || '',
|
||||
HostTestName: mapping.HostTestName || '',
|
||||
ConDefID: mapping.ConDefID || null,
|
||||
ClientTestCode: mapping.ClientTestCode || '',
|
||||
ClientTestName: mapping.ClientTestName || '',
|
||||
isNew: false,
|
||||
}));
|
||||
// Fetch existing details from API
|
||||
fetchDetailsForGroup();
|
||||
} else if (mode === 'edit' && initialData) {
|
||||
// Legacy edit mode (single mapping)
|
||||
modalContext = {
|
||||
TestMapID: initialData.TestMapID || null,
|
||||
HostType: initialData.HostType || '',
|
||||
HostID: initialData.HostID || '',
|
||||
ClientType: initialData.ClientType || '',
|
||||
ClientID: initialData.ClientID || '',
|
||||
};
|
||||
modalRows = [{
|
||||
TestMapDetailID: null,
|
||||
TestMapID: initialData.TestMapID,
|
||||
TestSiteID: null,
|
||||
HostTestCode: initialData.HostTestCode || '',
|
||||
HostTestName: initialData.HostTestName || '',
|
||||
ConDefID: initialData.ConDefID || null,
|
||||
@ -78,9 +108,11 @@
|
||||
ClientTestName: initialData.ClientTestName || '',
|
||||
isNew: false,
|
||||
}];
|
||||
originalRows = JSON.parse(JSON.stringify(modalRows));
|
||||
} else {
|
||||
// Create mode
|
||||
modalContext = {
|
||||
TestMapID: null,
|
||||
HostType: '',
|
||||
HostID: '',
|
||||
ClientType: '',
|
||||
@ -90,9 +122,42 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchDetailsForGroup() {
|
||||
if (!modalContext.TestMapID) return;
|
||||
|
||||
localLoading = true;
|
||||
try {
|
||||
const response = await fetchTestMapDetailsByTestMap(modalContext.TestMapID);
|
||||
const details = response.data || [];
|
||||
|
||||
modalRows = details.map((detail) => ({
|
||||
TestMapDetailID: parseInt(detail.TestMapDetailID) || null,
|
||||
TestMapID: parseInt(detail.TestMapID) || null,
|
||||
TestSiteID: detail.TestSiteID ? parseInt(detail.TestSiteID) : null,
|
||||
HostTestCode: detail.HostTestCode || '',
|
||||
HostTestName: detail.HostTestName || '',
|
||||
ConDefID: detail.ConDefID ? parseInt(detail.ConDefID) : null,
|
||||
ClientTestCode: detail.ClientTestCode || '',
|
||||
ClientTestName: detail.ClientTestName || '',
|
||||
isNew: false,
|
||||
}));
|
||||
|
||||
// Store a copy for comparison
|
||||
originalRows = JSON.parse(JSON.stringify(modalRows));
|
||||
} catch (err) {
|
||||
toastError(err.message || 'Failed to fetch test map details');
|
||||
modalRows = [];
|
||||
originalRows = [];
|
||||
} finally {
|
||||
localLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function createEmptyRow() {
|
||||
return {
|
||||
TestMapDetailID: null,
|
||||
TestMapID: null,
|
||||
TestSiteID: null,
|
||||
HostTestCode: '',
|
||||
HostTestName: '',
|
||||
ConDefID: null,
|
||||
@ -164,34 +229,111 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === 'create') {
|
||||
await handleCreate();
|
||||
} else {
|
||||
await handleUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreate() {
|
||||
saving = true;
|
||||
try {
|
||||
const promises = modalRows.map(async (row) => {
|
||||
const payload = {
|
||||
HostType: modalContext.HostType,
|
||||
HostID: modalContext.HostID,
|
||||
// Create header with details array
|
||||
const payload = {
|
||||
HostType: modalContext.HostType,
|
||||
HostID: modalContext.HostID,
|
||||
ClientType: modalContext.ClientType,
|
||||
ClientID: modalContext.ClientID,
|
||||
details: modalRows.map((row) => ({
|
||||
HostTestCode: row.HostTestCode,
|
||||
HostTestName: row.HostTestName,
|
||||
ClientType: modalContext.ClientType,
|
||||
ClientID: modalContext.ClientID,
|
||||
ConDefID: modalContext.ClientType === 'INST' ? row.ConDefID : null,
|
||||
ClientTestCode: row.ClientTestCode,
|
||||
ClientTestName: row.ClientTestName,
|
||||
};
|
||||
})),
|
||||
};
|
||||
|
||||
if (mode === 'create' || row.isNew) {
|
||||
return createTestMap(payload);
|
||||
} else {
|
||||
return updateTestMap({ ...payload, TestMapID: row.TestMapID });
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
toastSuccess(`Test mapping${modalRows.length > 1 ? 's' : ''} saved successfully`);
|
||||
await createTestMap(payload);
|
||||
toastSuccess(`Test mapping${modalRows.length > 1 ? 's' : ''} created successfully`);
|
||||
open = false;
|
||||
onSave?.();
|
||||
} catch (err) {
|
||||
toastError(err.message || 'Failed to save test mapping');
|
||||
toastError(err.message || 'Failed to create test mapping');
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdate() {
|
||||
saving = true;
|
||||
try {
|
||||
// Compare current rows with original rows
|
||||
const currentIds = new Set(modalRows.map((r) => r.TestMapDetailID).filter((id) => id !== null));
|
||||
const originalIds = new Set(originalRows.map((r) => r.TestMapDetailID).filter((id) => id !== null));
|
||||
|
||||
// Find deleted IDs (in original but not in current)
|
||||
const deletedIds = [...originalIds].filter((id) => !currentIds.has(id));
|
||||
|
||||
// Find new rows (no TestMapDetailID)
|
||||
const newRows = modalRows.filter((row) => row.TestMapDetailID === null);
|
||||
|
||||
// Find updated rows (has TestMapDetailID and exists in original)
|
||||
const updatedRows = modalRows.filter((row) => {
|
||||
if (row.TestMapDetailID === null) return false;
|
||||
const original = originalRows.find((r) => r.TestMapDetailID === row.TestMapDetailID);
|
||||
if (!original) return false;
|
||||
// Check if any field changed
|
||||
return (
|
||||
row.HostTestCode !== original.HostTestCode ||
|
||||
row.HostTestName !== original.HostTestName ||
|
||||
row.ConDefID !== original.ConDefID ||
|
||||
row.ClientTestCode !== original.ClientTestCode ||
|
||||
row.ClientTestName !== original.ClientTestName
|
||||
);
|
||||
});
|
||||
|
||||
// Execute batch operations
|
||||
const promises = [];
|
||||
|
||||
if (deletedIds.length > 0) {
|
||||
promises.push(batchDeleteTestMapDetails(deletedIds));
|
||||
}
|
||||
|
||||
if (newRows.length > 0) {
|
||||
const newDetails = newRows.map((row) => ({
|
||||
TestMapID: modalContext.TestMapID,
|
||||
HostTestCode: row.HostTestCode,
|
||||
HostTestName: row.HostTestName,
|
||||
ConDefID: modalContext.ClientType === 'INST' ? row.ConDefID : null,
|
||||
ClientTestCode: row.ClientTestCode,
|
||||
ClientTestName: row.ClientTestName,
|
||||
}));
|
||||
promises.push(batchCreateTestMapDetails(newDetails));
|
||||
}
|
||||
|
||||
if (updatedRows.length > 0) {
|
||||
const updatedDetails = updatedRows.map((row) => ({
|
||||
TestMapDetailID: row.TestMapDetailID,
|
||||
TestMapID: modalContext.TestMapID,
|
||||
HostTestCode: row.HostTestCode,
|
||||
HostTestName: row.HostTestName,
|
||||
ConDefID: modalContext.ClientType === 'INST' ? row.ConDefID : null,
|
||||
ClientTestCode: row.ClientTestCode,
|
||||
ClientTestName: row.ClientTestName,
|
||||
}));
|
||||
promises.push(batchUpdateTestMapDetails(updatedDetails));
|
||||
}
|
||||
|
||||
if (promises.length > 0) {
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
toastSuccess(`Test mapping${modalRows.length > 1 ? 's' : ''} updated successfully`);
|
||||
open = false;
|
||||
onSave?.();
|
||||
} catch (err) {
|
||||
toastError(err.message || 'Failed to update test mapping');
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
@ -209,7 +351,16 @@
|
||||
title={mode === 'create' ? 'Add Test Mapping' : `Edit Test Mapping${groupData ? 's' : ''} (${modalRows.length})`}
|
||||
size="xl"
|
||||
>
|
||||
<div class="flex flex-col max-h-[70vh]">
|
||||
<div class="flex flex-col max-h-[70vh] relative">
|
||||
<!-- Loading Overlay -->
|
||||
{#if loading || localLoading}
|
||||
<div class="absolute inset-0 bg-base-100/80 z-50 flex items-center justify-center">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||
<span class="text-sm text-gray-500">Loading test mappings...</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Sticky Top Section: Info banner + Host and Client -->
|
||||
<div class="flex-shrink-0 bg-base-100 z-10">
|
||||
<!-- Info banner for group editing -->
|
||||
@ -420,7 +571,7 @@
|
||||
</div>
|
||||
|
||||
{#if formErrors.rows}
|
||||
{#each Object.entries(formErrors.rows) as [idx, error]}
|
||||
{#each Object.entries(formErrors.rows) as [idx, error] (idx)}
|
||||
{#if error.empty}
|
||||
<p class="text-xs text-error">Row {parseInt(idx) + 1}: {error.empty}</p>
|
||||
{/if}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user