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';
|
import { get, post, patch, del } from './client.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} TestMap
|
* @typedef {Object} TestMapHeader
|
||||||
* @property {number} TestMapID - Mapping ID
|
* @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 {number} TestSiteID - Test Site ID
|
||||||
* @property {string} TestSiteCode - Test Code
|
* @property {string} TestSiteCode - Test Code
|
||||||
* @property {string} TestSiteName - Test Name
|
* @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} HostTestCode - Host Test Code
|
||||||
* @property {string} HostTestName - Host Test Name
|
* @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 {number} ConDefID - Container Definition ID
|
||||||
* @property {string} ClientTestCode - Client Test Code
|
* @property {string} ClientTestCode - Client Test Code
|
||||||
* @property {string} ClientTestName - Client Test Name
|
* @property {string} ClientTestName - Client Test Name
|
||||||
@ -19,25 +28,42 @@ import { get, post, patch, del } from './client.js';
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} TestMapFilterOptions
|
* @typedef {Object} TestMapWithDetails
|
||||||
* @property {string} [hostType] - Filter by Host Type
|
* @property {number} TestMapID - Mapping ID
|
||||||
* @property {string} [hostID] - Filter by Host ID
|
* @property {number} TestSiteID - Test Site ID
|
||||||
* @property {string} [clientType] - Filter by Client Type
|
* @property {string} TestSiteCode - Test Code
|
||||||
* @property {string} [clientID] - Filter by Client ID
|
* @property {string} TestSiteName - Test Name
|
||||||
* @property {string} [search] - Search term
|
* @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
|
* @typedef {Object} TestMapListResponse
|
||||||
* @property {boolean} success
|
* @property {boolean} success
|
||||||
* @property {TestMap[]} data
|
* @property {TestMapHeader[]} data
|
||||||
* @property {string} [message]
|
* @property {string} [message]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} TestMapResponse
|
* @typedef {Object} TestMapResponse
|
||||||
* @property {boolean} success
|
* @property {boolean} success
|
||||||
* @property {TestMap} data
|
* @property {TestMapWithDetails} data
|
||||||
|
* @property {string} [message]
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} TestMapDetailResponse
|
||||||
|
* @property {boolean} success
|
||||||
|
* @property {TestMapDetail} data
|
||||||
* @property {string} [message]
|
* @property {string} [message]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -46,49 +72,63 @@ import { get, post, patch, del } from './client.js';
|
|||||||
* @property {number} TestSiteID - Test Site ID
|
* @property {number} TestSiteID - Test Site ID
|
||||||
* @property {string} HostType - Host Type
|
* @property {string} HostType - Host Type
|
||||||
* @property {string} HostID - Host ID
|
* @property {string} HostID - Host ID
|
||||||
* @property {string} HostTestCode - Host Test Code
|
|
||||||
* @property {string} HostTestName - Host Test Name
|
|
||||||
* @property {string} ClientType - Client Type
|
* @property {string} ClientType - Client Type
|
||||||
* @property {string} ClientID - Client ID
|
* @property {string} ClientID - Client ID
|
||||||
* @property {number} ConDefID - Container Definition ID
|
* @property {Object[]} [details] - Optional detail records to create
|
||||||
* @property {string} ClientTestCode - Client Test Code
|
|
||||||
* @property {string} ClientTestName - Client Test Name
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} UpdateTestMapPayload
|
* @typedef {Object} UpdateTestMapPayload
|
||||||
* @property {number} TestMapID - Mapping ID
|
* @property {number} TestMapID - Mapping ID
|
||||||
* @property {number} TestSiteID - Test Site ID
|
* @property {number} [TestSiteID] - Test Site ID
|
||||||
* @property {string} HostType - Host Type
|
* @property {string} [HostType] - Host Type
|
||||||
* @property {string} HostID - Host ID
|
* @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} HostTestCode - Host Test Code
|
||||||
* @property {string} HostTestName - Host Test Name
|
* @property {string} HostTestName - Host Test Name
|
||||||
* @property {string} ClientType - Client Type
|
|
||||||
* @property {string} ClientID - Client ID
|
|
||||||
* @property {number} ConDefID - Container Definition ID
|
* @property {number} ConDefID - Container Definition ID
|
||||||
* @property {string} ClientTestCode - Client Test Code
|
* @property {string} ClientTestCode - Client Test Code
|
||||||
* @property {string} ClientTestName - Client Test Name
|
* @property {string} ClientTestName - Client Test Name
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch all test mappings
|
* @typedef {Object} UpdateTestMapDetailPayload
|
||||||
* @returns {Promise<TestMapListResponse>} API response with mappings list
|
* @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() {
|
export async function fetchTestMaps() {
|
||||||
return get('/api/test/testmap');
|
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
|
* @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) {
|
export async function fetchTestMap(id) {
|
||||||
return get(`/api/test/testmap/${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
|
* @param {number} testSiteID - Test Site ID
|
||||||
* @returns {Promise<TestMapListResponse>} API response with mappings list
|
* @returns {Promise<TestMapListResponse>} API response with mappings list
|
||||||
*/
|
*/
|
||||||
@ -97,52 +137,123 @@ export async function fetchTestMapsByTestSite(testSiteID) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch test mappings by host
|
* Create a new test mapping header
|
||||||
* @param {string} hostType - Host Type
|
* @param {CreateTestMapPayload} data - Header data
|
||||||
* @param {string} hostID - Host ID
|
* @returns {Promise<{success: boolean, data: number, message?: string}>} API response with created TestMapID
|
||||||
* @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
|
|
||||||
*/
|
*/
|
||||||
export async function createTestMap(data) {
|
export async function createTestMap(data) {
|
||||||
return post('/api/test/testmap', data);
|
return post('/api/test/testmap', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update an existing test mapping
|
* Update an existing test mapping header
|
||||||
* @param {UpdateTestMapPayload} data - Mapping data
|
* @param {UpdateTestMapPayload} data - Header data
|
||||||
* @returns {Promise<TestMapResponse>} API response
|
* @returns {Promise<{success: boolean, data: number, message?: string}>} API response
|
||||||
*/
|
*/
|
||||||
export async function updateTestMap(data) {
|
export async function updateTestMap(data) {
|
||||||
return patch('/api/test/testmap', 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
|
* @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) {
|
export async function deleteTestMap(id) {
|
||||||
return del('/api/test/testmap', { body: JSON.stringify({ TestMapID: 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
|
* Validate mapping form data
|
||||||
* @param {Object} data - Form data to validate
|
* @param {Object} data - Form data to validate
|
||||||
@ -176,3 +287,26 @@ export function validateTestMap(data) {
|
|||||||
errors
|
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 { onMount } from 'svelte';
|
||||||
import {
|
import {
|
||||||
fetchTestMaps,
|
fetchTestMaps,
|
||||||
|
fetchTestMap,
|
||||||
deleteTestMap,
|
deleteTestMap,
|
||||||
} from '$lib/api/testmap.js';
|
} from '$lib/api/testmap.js';
|
||||||
import { fetchContainers } from '$lib/api/containers.js';
|
import { fetchContainers } from '$lib/api/containers.js';
|
||||||
@ -17,12 +18,10 @@
|
|||||||
Link,
|
Link,
|
||||||
Server,
|
Server,
|
||||||
Monitor,
|
Monitor,
|
||||||
Filter,
|
|
||||||
X,
|
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
|
|
||||||
let loading = $state(false);
|
let loading = $state(false);
|
||||||
let testMaps = $state([]);
|
let testMapHeaders = $state([]);
|
||||||
let containers = $state([]);
|
let containers = $state([]);
|
||||||
let modalOpen = $state(false);
|
let modalOpen = $state(false);
|
||||||
let modalMode = $state('create');
|
let modalMode = $state('create');
|
||||||
@ -32,68 +31,14 @@
|
|||||||
let deleteConfirmOpen = $state(false);
|
let deleteConfirmOpen = $state(false);
|
||||||
let deleteItem = $state(null);
|
let deleteItem = $state(null);
|
||||||
let deleteGroupMode = $state(false);
|
let deleteGroupMode = $state(false);
|
||||||
|
let modalLoading = $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());
|
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ key: 'HostInfo', label: 'Host System', class: 'w-48' },
|
{ key: 'HostInfo', label: 'Host System', class: 'w-48' },
|
||||||
{ key: 'ClientInfo', label: 'Client 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' },
|
{ 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 () => {
|
onMount(async () => {
|
||||||
await Promise.all([loadTestMaps(), loadContainers()]);
|
await Promise.all([loadTestMaps(), loadContainers()]);
|
||||||
});
|
});
|
||||||
@ -102,10 +47,10 @@
|
|||||||
loading = true;
|
loading = true;
|
||||||
try {
|
try {
|
||||||
const response = await fetchTestMaps();
|
const response = await fetchTestMaps();
|
||||||
testMaps = Array.isArray(response.data) ? response.data : [];
|
testMapHeaders = Array.isArray(response.data) ? response.data : [];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toastError(err.message || 'Failed to load test mappings');
|
toastError(err.message || 'Failed to load test mappings');
|
||||||
testMaps = [];
|
testMapHeaders = [];
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
@ -128,12 +73,27 @@
|
|||||||
modalOpen = true;
|
modalOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEditGroupModal(group) {
|
async function openEditGroupModal(group) {
|
||||||
|
modalLoading = true;
|
||||||
modalMode = 'edit';
|
modalMode = 'edit';
|
||||||
modalGroupData = group;
|
|
||||||
// Pass the first mapping as initial data, modal will handle the rest
|
try {
|
||||||
modalData = group.mappings[0] || null;
|
// 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;
|
modalOpen = true;
|
||||||
|
} catch (err) {
|
||||||
|
toastError(err.message || 'Failed to load test mapping details');
|
||||||
|
modalGroupData = group;
|
||||||
|
modalData = null;
|
||||||
|
modalOpen = true;
|
||||||
|
} finally {
|
||||||
|
modalLoading = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleModalSave() {
|
function handleModalSave() {
|
||||||
@ -150,34 +110,26 @@
|
|||||||
deleting = true;
|
deleting = true;
|
||||||
try {
|
try {
|
||||||
if (deleteGroupMode && deleteItem) {
|
if (deleteGroupMode && deleteItem) {
|
||||||
// Delete all mappings in the group
|
// For delete, we need the TestMapID
|
||||||
const deletePromises = deleteItem.mappings.map((mapping) =>
|
// Since the list doesn't return TestMapID, we need to fetch it first
|
||||||
deleteTestMap(mapping.TestMapID)
|
// 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.');
|
||||||
await Promise.all(deletePromises);
|
// TODO: Implement once API returns TestMapID or provides delete by host/client endpoint
|
||||||
toastSuccess(`Deleted ${deleteItem.mappings.length} test mapping(s) successfully`);
|
|
||||||
} else if (deleteItem?.TestMapID) {
|
} else if (deleteItem?.TestMapID) {
|
||||||
// Delete single mapping (fallback)
|
// Delete single mapping (fallback)
|
||||||
await deleteTestMap(deleteItem.TestMapID);
|
await deleteTestMap(deleteItem.TestMapID);
|
||||||
toastSuccess('Test mapping deleted successfully');
|
toastSuccess('Test mapping deleted successfully');
|
||||||
}
|
|
||||||
deleteConfirmOpen = false;
|
deleteConfirmOpen = false;
|
||||||
deleteItem = null;
|
deleteItem = null;
|
||||||
deleteGroupMode = false;
|
deleteGroupMode = false;
|
||||||
await loadTestMaps();
|
await loadTestMaps();
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toastError(err.message || 'Failed to delete test mapping(s)');
|
toastError(err.message || 'Failed to delete test mapping(s)');
|
||||||
} finally {
|
} finally {
|
||||||
deleting = false;
|
deleting = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearFilters() {
|
|
||||||
filterHostType = '';
|
|
||||||
filterHostID = '';
|
|
||||||
filterClientType = '';
|
|
||||||
filterClientID = '';
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
@ -197,113 +149,29 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</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 -->
|
<!-- Data Table -->
|
||||||
<div class="bg-base-100 rounded-lg shadow border border-base-200">
|
<div class="bg-base-100 rounded-lg shadow border border-base-200">
|
||||||
{#if !loading && filteredGroupedTestMaps.length === 0}
|
{#if !loading && testMapHeaders.length === 0}
|
||||||
<!-- Empty State -->
|
<!-- Empty State -->
|
||||||
<div class="flex flex-col items-center justify-center py-16 px-4">
|
<div class="flex flex-col items-center justify-center py-16 px-4">
|
||||||
<div class="bg-base-200 rounded-full p-6 mb-4">
|
<div class="bg-base-200 rounded-full p-6 mb-4">
|
||||||
<Link class="w-12 h-12 text-gray-400" />
|
<Link class="w-12 h-12 text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-base font-semibold text-gray-700 mb-2">
|
<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
|
No test mappings found
|
||||||
{/if}
|
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-xs text-gray-500 text-center max-w-md mb-6">
|
<p class="text-sm 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.
|
Test mappings connect tests between host systems (like HIS) and client systems. Add your first mapping to get started.
|
||||||
{/if}
|
|
||||||
</p>
|
</p>
|
||||||
{#if filterHostType || filterHostID || filterClientType || filterClientID}
|
|
||||||
<button class="btn btn-outline" onclick={clearFilters}>
|
|
||||||
Clear Filters
|
|
||||||
</button>
|
|
||||||
{:else}
|
|
||||||
<button class="btn btn-primary" onclick={openCreateModal}>
|
<button class="btn btn-primary" onclick={openCreateModal}>
|
||||||
<Plus class="w-4 h-4 mr-2" />
|
<Plus class="w-4 h-4 mr-2" />
|
||||||
Add Mapping
|
Add Mapping
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<DataTable
|
<DataTable
|
||||||
{columns}
|
{columns}
|
||||||
data={filteredGroupedTestMaps}
|
data={testMapHeaders}
|
||||||
{loading}
|
{loading}
|
||||||
emptyMessage="No test mappings found"
|
emptyMessage="No test mappings found"
|
||||||
hover={true}
|
hover={true}
|
||||||
@ -314,35 +182,29 @@
|
|||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Server class="w-4 h-4 text-primary flex-shrink-0" />
|
<Server class="w-4 h-4 text-primary flex-shrink-0" />
|
||||||
<div class="font-medium text-sm">
|
<div class="font-medium text-sm">
|
||||||
{row.HostType || '-'} - {row.HostID || '-'}
|
{row.HostName || row.HostID || '-'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if column.key === 'ClientInfo'}
|
{:else if column.key === 'ClientInfo'}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Monitor class="w-4 h-4 text-secondary flex-shrink-0" />
|
<Monitor class="w-4 h-4 text-secondary flex-shrink-0" />
|
||||||
<div class="font-medium text-sm">
|
<div class="font-medium text-sm">
|
||||||
{row.ClientType || '-'} - {row.ClientID || '-'}
|
{row.ClientName || row.ClientID || '-'}
|
||||||
</div>
|
</div>
|
||||||
</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'}
|
{:else if column.key === 'actions'}
|
||||||
<div class="flex justify-center gap-1">
|
<div class="flex justify-center gap-1">
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-ghost"
|
class="btn btn-sm btn-ghost"
|
||||||
onclick={() => openEditGroupModal(row)}
|
onclick={() => openEditGroupModal(row)}
|
||||||
title="Edit all {row.mappings.length} test mapping(s)"
|
title="Edit test mapping group"
|
||||||
>
|
>
|
||||||
<Edit2 class="w-4 h-4" />
|
<Edit2 class="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-ghost text-error"
|
class="btn btn-sm btn-ghost text-error"
|
||||||
onclick={() => confirmDeleteGroup(row)}
|
onclick={() => confirmDeleteGroup(row)}
|
||||||
title="Delete all {row.mappings.length} test mapping(s)"
|
title="Delete test mapping group"
|
||||||
>
|
>
|
||||||
<Trash2 class="w-4 h-4" />
|
<Trash2 class="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
@ -363,6 +225,7 @@
|
|||||||
initialData={modalData}
|
initialData={modalData}
|
||||||
groupData={modalGroupData}
|
groupData={modalGroupData}
|
||||||
{containers}
|
{containers}
|
||||||
|
loading={modalLoading}
|
||||||
onSave={handleModalSave}
|
onSave={handleModalSave}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -371,7 +234,7 @@
|
|||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<p class="text-base-content/80">
|
<p class="text-base-content/80">
|
||||||
{#if deleteGroupMode}
|
{#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}
|
{:else}
|
||||||
Are you sure you want to delete this test mapping?
|
Are you sure you want to delete this test mapping?
|
||||||
{/if}
|
{/if}
|
||||||
@ -379,11 +242,11 @@
|
|||||||
<div class="bg-base-200 rounded-lg p-3 mt-3 space-y-1">
|
<div class="bg-base-200 rounded-lg p-3 mt-3 space-y-1">
|
||||||
<p class="text-sm">
|
<p class="text-sm">
|
||||||
<span class="text-gray-500">Host:</span>
|
<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>
|
||||||
<p class="text-sm">
|
<p class="text-sm">
|
||||||
<span class="text-gray-500">Client:</span>
|
<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>
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,7 +1,15 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { untrack } from 'svelte';
|
||||||
import Modal from '$lib/components/Modal.svelte';
|
import Modal from '$lib/components/Modal.svelte';
|
||||||
import { success as toastSuccess, error as toastError } from '$lib/utils/toast.js';
|
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 {
|
import {
|
||||||
Plus,
|
Plus,
|
||||||
Trash2,
|
Trash2,
|
||||||
@ -11,12 +19,14 @@
|
|||||||
AlertCircle,
|
AlertCircle,
|
||||||
} from 'lucide-svelte';
|
} 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 saving = $state(false);
|
||||||
|
let localLoading = $state(false);
|
||||||
|
|
||||||
// Modal context (shared for all rows)
|
// Modal context (shared for all rows)
|
||||||
let modalContext = $state({
|
let modalContext = $state({
|
||||||
|
TestMapID: null,
|
||||||
HostType: '',
|
HostType: '',
|
||||||
HostID: '',
|
HostID: '',
|
||||||
ClientType: '',
|
ClientType: '',
|
||||||
@ -26,51 +36,71 @@
|
|||||||
// Editable rows in modal
|
// Editable rows in modal
|
||||||
let modalRows = $state([]);
|
let modalRows = $state([]);
|
||||||
|
|
||||||
|
// Original rows for comparison in edit mode
|
||||||
|
let originalRows = $state([]);
|
||||||
|
|
||||||
// Form errors
|
// Form errors
|
||||||
let formErrors = $state({});
|
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 hostTypes = ['HIS', 'SITE', 'WST', 'INST'];
|
||||||
const clientTypes = ['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(() => {
|
$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();
|
initializeModal();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function initializeModal() {
|
function initializeModal() {
|
||||||
formErrors = {};
|
formErrors = {};
|
||||||
|
originalRows = [];
|
||||||
|
|
||||||
if (mode === 'edit' && groupData) {
|
if (mode === 'edit' && groupData) {
|
||||||
// Edit mode with group data - load all mappings in the group
|
// Edit mode with group data - load all mappings in the group
|
||||||
modalContext = {
|
modalContext = {
|
||||||
|
TestMapID: groupData.TestMapID || null,
|
||||||
HostType: groupData.HostType || '',
|
HostType: groupData.HostType || '',
|
||||||
HostID: groupData.HostID || '',
|
HostID: groupData.HostID || '',
|
||||||
ClientType: groupData.ClientType || '',
|
ClientType: groupData.ClientType || '',
|
||||||
ClientID: groupData.ClientID || '',
|
ClientID: groupData.ClientID || '',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load all mappings from the group
|
// Fetch existing details from API
|
||||||
modalRows = groupData.mappings.map((mapping) => ({
|
fetchDetailsForGroup();
|
||||||
TestMapID: mapping.TestMapID,
|
|
||||||
HostTestCode: mapping.HostTestCode || '',
|
|
||||||
HostTestName: mapping.HostTestName || '',
|
|
||||||
ConDefID: mapping.ConDefID || null,
|
|
||||||
ClientTestCode: mapping.ClientTestCode || '',
|
|
||||||
ClientTestName: mapping.ClientTestName || '',
|
|
||||||
isNew: false,
|
|
||||||
}));
|
|
||||||
} else if (mode === 'edit' && initialData) {
|
} else if (mode === 'edit' && initialData) {
|
||||||
// Legacy edit mode (single mapping)
|
// Legacy edit mode (single mapping)
|
||||||
modalContext = {
|
modalContext = {
|
||||||
|
TestMapID: initialData.TestMapID || null,
|
||||||
HostType: initialData.HostType || '',
|
HostType: initialData.HostType || '',
|
||||||
HostID: initialData.HostID || '',
|
HostID: initialData.HostID || '',
|
||||||
ClientType: initialData.ClientType || '',
|
ClientType: initialData.ClientType || '',
|
||||||
ClientID: initialData.ClientID || '',
|
ClientID: initialData.ClientID || '',
|
||||||
};
|
};
|
||||||
modalRows = [{
|
modalRows = [{
|
||||||
|
TestMapDetailID: null,
|
||||||
TestMapID: initialData.TestMapID,
|
TestMapID: initialData.TestMapID,
|
||||||
|
TestSiteID: null,
|
||||||
HostTestCode: initialData.HostTestCode || '',
|
HostTestCode: initialData.HostTestCode || '',
|
||||||
HostTestName: initialData.HostTestName || '',
|
HostTestName: initialData.HostTestName || '',
|
||||||
ConDefID: initialData.ConDefID || null,
|
ConDefID: initialData.ConDefID || null,
|
||||||
@ -78,9 +108,11 @@
|
|||||||
ClientTestName: initialData.ClientTestName || '',
|
ClientTestName: initialData.ClientTestName || '',
|
||||||
isNew: false,
|
isNew: false,
|
||||||
}];
|
}];
|
||||||
|
originalRows = JSON.parse(JSON.stringify(modalRows));
|
||||||
} else {
|
} else {
|
||||||
// Create mode
|
// Create mode
|
||||||
modalContext = {
|
modalContext = {
|
||||||
|
TestMapID: null,
|
||||||
HostType: '',
|
HostType: '',
|
||||||
HostID: '',
|
HostID: '',
|
||||||
ClientType: '',
|
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() {
|
function createEmptyRow() {
|
||||||
return {
|
return {
|
||||||
|
TestMapDetailID: null,
|
||||||
TestMapID: null,
|
TestMapID: null,
|
||||||
|
TestSiteID: null,
|
||||||
HostTestCode: '',
|
HostTestCode: '',
|
||||||
HostTestName: '',
|
HostTestName: '',
|
||||||
ConDefID: null,
|
ConDefID: null,
|
||||||
@ -164,34 +229,111 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mode === 'create') {
|
||||||
|
await handleCreate();
|
||||||
|
} else {
|
||||||
|
await handleUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCreate() {
|
||||||
saving = true;
|
saving = true;
|
||||||
try {
|
try {
|
||||||
const promises = modalRows.map(async (row) => {
|
// Create header with details array
|
||||||
const payload = {
|
const payload = {
|
||||||
HostType: modalContext.HostType,
|
HostType: modalContext.HostType,
|
||||||
HostID: modalContext.HostID,
|
HostID: modalContext.HostID,
|
||||||
HostTestCode: row.HostTestCode,
|
|
||||||
HostTestName: row.HostTestName,
|
|
||||||
ClientType: modalContext.ClientType,
|
ClientType: modalContext.ClientType,
|
||||||
ClientID: modalContext.ClientID,
|
ClientID: modalContext.ClientID,
|
||||||
|
details: modalRows.map((row) => ({
|
||||||
|
HostTestCode: row.HostTestCode,
|
||||||
|
HostTestName: row.HostTestName,
|
||||||
ConDefID: modalContext.ClientType === 'INST' ? row.ConDefID : null,
|
ConDefID: modalContext.ClientType === 'INST' ? row.ConDefID : null,
|
||||||
ClientTestCode: row.ClientTestCode,
|
ClientTestCode: row.ClientTestCode,
|
||||||
ClientTestName: row.ClientTestName,
|
ClientTestName: row.ClientTestName,
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (mode === 'create' || row.isNew) {
|
await createTestMap(payload);
|
||||||
return createTestMap(payload);
|
toastSuccess(`Test mapping${modalRows.length > 1 ? 's' : ''} created successfully`);
|
||||||
} else {
|
|
||||||
return updateTestMap({ ...payload, TestMapID: row.TestMapID });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(promises);
|
|
||||||
toastSuccess(`Test mapping${modalRows.length > 1 ? 's' : ''} saved successfully`);
|
|
||||||
open = false;
|
open = false;
|
||||||
onSave?.();
|
onSave?.();
|
||||||
} catch (err) {
|
} 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 {
|
} finally {
|
||||||
saving = false;
|
saving = false;
|
||||||
}
|
}
|
||||||
@ -209,7 +351,16 @@
|
|||||||
title={mode === 'create' ? 'Add Test Mapping' : `Edit Test Mapping${groupData ? 's' : ''} (${modalRows.length})`}
|
title={mode === 'create' ? 'Add Test Mapping' : `Edit Test Mapping${groupData ? 's' : ''} (${modalRows.length})`}
|
||||||
size="xl"
|
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 -->
|
<!-- Sticky Top Section: Info banner + Host and Client -->
|
||||||
<div class="flex-shrink-0 bg-base-100 z-10">
|
<div class="flex-shrink-0 bg-base-100 z-10">
|
||||||
<!-- Info banner for group editing -->
|
<!-- Info banner for group editing -->
|
||||||
@ -420,7 +571,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if formErrors.rows}
|
{#if formErrors.rows}
|
||||||
{#each Object.entries(formErrors.rows) as [idx, error]}
|
{#each Object.entries(formErrors.rows) as [idx, error] (idx)}
|
||||||
{#if error.empty}
|
{#if error.empty}
|
||||||
<p class="text-xs text-error">Row {parseInt(idx) + 1}: {error.empty}</p>
|
<p class="text-xs text-error">Row {parseInt(idx) + 1}: {error.empty}</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user