refactor(data-access): remove TVP code and simplify SearchModel
- Remove all List<*FilterEntry> properties and *FilterEnabled computed properties from SearchModel - Delete TableValuedParameterExtensions.cs - Delete entire FilterEntries folder and all filter entry model classes - Delete FilterHandlers folder and all filter handler classes - Delete IFilterHandler interface and FilterResult model - Update MisQueryBuilder to use SQL extraction functions instead of model properties - Update SearchProcessor to get ExtractMisData from database using fn_GetSearchExtractMisData - Update DependencyInjection to remove filter handler registrations - Delete obsolete tests for TVP extensions and filter handlers Filter criteria are now stored as JSON in Search.Criteria column and extracted using SQL functions (fn_GetSearch*) during query execution.
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
using JdeScoping.Core.Interfaces;
|
||||
using JdeScoping.DataAccess;
|
||||
using JdeScoping.DataAccess.Options;
|
||||
using JdeScoping.DataAccess.FilterHandlers;
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataAccess.QueryBuilders;
|
||||
using JdeScoping.DataAccess.Repositories;
|
||||
@@ -45,17 +44,9 @@ public static class DataAccessDependencyInjection
|
||||
// Register SqlKata compiler (singleton, thread-safe)
|
||||
services.AddSingleton<SqlServerCompiler>();
|
||||
|
||||
// Register filter handlers (scoped - one per request)
|
||||
services.AddScoped<IFilterHandler, WorkOrderFilterHandler>();
|
||||
services.AddScoped<IFilterHandler, ItemNumberFilterHandler>();
|
||||
services.AddScoped<IFilterHandler, ProfitCenterFilterHandler>();
|
||||
services.AddScoped<IFilterHandler, WorkCenterFilterHandler>();
|
||||
services.AddScoped<IFilterHandler, OperatorFilterHandler>();
|
||||
services.AddScoped<IFilterHandler, ComponentLotFilterHandler>();
|
||||
services.AddScoped<IFilterHandler, ItemOperationMisFilterHandler>();
|
||||
services.AddScoped<IFilterHandler, TimespanFilterHandler>();
|
||||
|
||||
// Register query builder (scoped)
|
||||
// Note: Filter criteria are extracted from database JSON using SQL functions,
|
||||
// eliminating the need for filter handler classes.
|
||||
services.AddScoped<ISearchQueryBuilder, SqlKataSearchQueryBuilder>();
|
||||
|
||||
// Register search processing services (scoped)
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
using System.Data;
|
||||
using Dapper;
|
||||
using JdeScoping.DataAccess.Models;
|
||||
|
||||
namespace JdeScoping.DataAccess.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for SearchModel including table-valued parameters and query helpers.
|
||||
/// </summary>
|
||||
public static class TableValuedParameterExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if work order step data should be searched for the given search model.
|
||||
/// Steps are searched when time-based or resource-based filters are applied.
|
||||
/// </summary>
|
||||
/// <param name="model">Search model to evaluate.</param>
|
||||
/// <returns>True if work order step data should be searched.</returns>
|
||||
public static bool ShouldSearchSteps(this SearchModel model)
|
||||
{
|
||||
return model.MinimumDt.HasValue
|
||||
|| model.MaximumDt.HasValue
|
||||
|| model.ProfitCenterFilterEnabled
|
||||
|| model.WorkCenterFilterEnabled
|
||||
|| model.OperatorFilterEnabled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a table-valued parameter for work order filtering.
|
||||
/// </summary>
|
||||
/// <param name="model">The search model.</param>
|
||||
/// <returns>A Dapper table-valued parameter with WorkOrderNumber column.</returns>
|
||||
public static SqlMapper.ICustomQueryParameter CreateWorkOrderFilterParameter(this SearchModel model)
|
||||
{
|
||||
var dataTable = new DataTable();
|
||||
dataTable.Columns.Add("WorkOrderNumber", typeof(long));
|
||||
|
||||
foreach (var entry in model.WorkOrderFilter)
|
||||
{
|
||||
dataTable.Rows.Add(entry.WorkOrderNumber);
|
||||
}
|
||||
|
||||
return dataTable.AsTableValuedParameter("dbo.WorkOrderFilterParameter");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a table-valued parameter for item number filtering.
|
||||
/// </summary>
|
||||
/// <param name="model">The search model.</param>
|
||||
/// <returns>A Dapper table-valued parameter with ItemNumber column.</returns>
|
||||
public static SqlMapper.ICustomQueryParameter CreateItemNumberFilterParameter(this SearchModel model)
|
||||
{
|
||||
var dataTable = new DataTable();
|
||||
dataTable.Columns.Add("ItemNumber", typeof(string));
|
||||
|
||||
foreach (var entry in model.ItemNumberFilter)
|
||||
{
|
||||
dataTable.Rows.Add(entry.ItemNumber);
|
||||
}
|
||||
|
||||
return dataTable.AsTableValuedParameter("dbo.ItemNumberFilterParameter");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a table-valued parameter for profit center filtering.
|
||||
/// </summary>
|
||||
/// <param name="model">The search model.</param>
|
||||
/// <returns>A Dapper table-valued parameter with Code column.</returns>
|
||||
public static SqlMapper.ICustomQueryParameter CreateProfitCenterFilterParameter(this SearchModel model)
|
||||
{
|
||||
var dataTable = new DataTable();
|
||||
dataTable.Columns.Add("Code", typeof(string));
|
||||
|
||||
foreach (var entry in model.ProfitCenterFilter)
|
||||
{
|
||||
dataTable.Rows.Add(entry.Code);
|
||||
}
|
||||
|
||||
return dataTable.AsTableValuedParameter("dbo.ProfitCenterFilterParameter");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a table-valued parameter for work center filtering.
|
||||
/// </summary>
|
||||
/// <param name="model">The search model.</param>
|
||||
/// <returns>A Dapper table-valued parameter with Code column.</returns>
|
||||
public static SqlMapper.ICustomQueryParameter CreateWorkCenterFilterParameter(this SearchModel model)
|
||||
{
|
||||
var dataTable = new DataTable();
|
||||
dataTable.Columns.Add("Code", typeof(string));
|
||||
|
||||
foreach (var entry in model.WorkCenterFilter)
|
||||
{
|
||||
dataTable.Rows.Add(entry.Code);
|
||||
}
|
||||
|
||||
return dataTable.AsTableValuedParameter("dbo.WorkCenterFilterParameter");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a table-valued parameter for operator filtering.
|
||||
/// </summary>
|
||||
/// <param name="model">The search model.</param>
|
||||
/// <returns>A Dapper table-valued parameter with UserName column.</returns>
|
||||
public static SqlMapper.ICustomQueryParameter CreateOperatorFilterParameter(this SearchModel model)
|
||||
{
|
||||
var dataTable = new DataTable();
|
||||
dataTable.Columns.Add("UserName", typeof(string));
|
||||
|
||||
foreach (var entry in model.OperatorFilter)
|
||||
{
|
||||
dataTable.Rows.Add(entry.UserId);
|
||||
}
|
||||
|
||||
return dataTable.AsTableValuedParameter("dbo.OperatorFilterParameter");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a table-valued parameter for component lot filtering.
|
||||
/// </summary>
|
||||
/// <param name="model">The search model.</param>
|
||||
/// <returns>A Dapper table-valued parameter with ComponentLotNumber and ItemNumber columns.</returns>
|
||||
public static SqlMapper.ICustomQueryParameter CreateComponentLotFilterParameter(this SearchModel model)
|
||||
{
|
||||
var dataTable = new DataTable();
|
||||
dataTable.Columns.Add("ComponentLotNumber", typeof(string));
|
||||
dataTable.Columns.Add("ItemNumber", typeof(string));
|
||||
|
||||
foreach (var entry in model.ComponentLotFilter)
|
||||
{
|
||||
dataTable.Rows.Add(entry.LotNumber, entry.ItemNumber);
|
||||
}
|
||||
|
||||
return dataTable.AsTableValuedParameter("dbo.ComponentLotFilterParameter");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a table-valued parameter for item/operation/MIS filtering.
|
||||
/// </summary>
|
||||
/// <param name="model">The search model.</param>
|
||||
/// <returns>A Dapper table-valued parameter with ItemNumber, OperationNumber, MisNumber, and MisRevision columns.</returns>
|
||||
public static SqlMapper.ICustomQueryParameter CreateItemOperationMisFilterParameter(this SearchModel model)
|
||||
{
|
||||
var dataTable = new DataTable();
|
||||
dataTable.Columns.Add("ItemNumber", typeof(string));
|
||||
dataTable.Columns.Add("OperationNumber", typeof(string));
|
||||
dataTable.Columns.Add("MisNumber", typeof(string));
|
||||
dataTable.Columns.Add("MisRevision", typeof(string));
|
||||
|
||||
foreach (var entry in model.ItemOperationMisFilter)
|
||||
{
|
||||
dataTable.Rows.Add(entry.ItemNumber, entry.OperationNumber, entry.MisNumber, entry.MisRevision);
|
||||
}
|
||||
|
||||
return dataTable.AsTableValuedParameter("dbo.ItemOperationMisFilterParameter");
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
using JdeScoping.DataAccess.Extensions;
|
||||
using JdeScoping.DataAccess.Models;
|
||||
using SqlKata.Compilers;
|
||||
|
||||
namespace JdeScoping.DataAccess.FilterHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// Filter handler for component lot filtering.
|
||||
/// Generates WorkOrderComponent/LotUsage joins and sets CARDEX flag on #Temp_WO.
|
||||
/// </summary>
|
||||
public sealed class ComponentLotFilterHandler : FilterHandlerBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override int Priority => 30;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsEnabled(SearchModel model) => model.ComponentLotFilterEnabled;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override FilterResult Apply(SearchModel model, SqlServerCompiler compiler)
|
||||
{
|
||||
var setupSql = new List<string>();
|
||||
var parameters = new Dictionary<string, object>
|
||||
{
|
||||
["p_ComponentLotFilter"] = model.CreateComponentLotFilterParameter()
|
||||
};
|
||||
|
||||
// Add downstream product for manually specified component lots
|
||||
const string componentLotMergeSql = """
|
||||
--Add downstream product for manually specified component lots
|
||||
WITH CLN_CTE AS(
|
||||
SELECT DISTINCT l.LotNumber,
|
||||
l.ShortItemNumber,
|
||||
l.BranchCode
|
||||
FROM @p_ComponentLotFilter AS pclf INNER JOIN
|
||||
dbo.Lot AS l ON (LTRIM(RTRIM(pclf.ComponentLotNumber)) = l.LotNumber AND LTRIM(RTRIM(pclf.ItemNumber)) = l.ItemNumber)
|
||||
),
|
||||
CLN_WO AS(
|
||||
SELECT wo.WorkOrderNumber,
|
||||
wo.BranchCode,
|
||||
wo.LotNumber,
|
||||
wo.ShortItemNumber
|
||||
FROM CLN_CTE cln INNER JOIN
|
||||
dbo.WorkOrderComponent AS woc ON (cln.LotNumber = woc.LotNumber AND cln.ShortItemNumber = woc.ShortItemNumber AND cln.BranchCode = woc.BranchCode) INNER JOIN
|
||||
dbo.WorkOrder AS wo ON (woc.WorkOrderNumber = wo.WorkOrderNumber)
|
||||
UNION ALL
|
||||
SELECT wo.WorkOrderNumber,
|
||||
wo.BranchCode,
|
||||
wo.LotNumber,
|
||||
wo.ShortItemNumber
|
||||
FROM CLN_CTE cln INNER JOIN
|
||||
dbo.LotUsage AS lu ON(cln.LotNumber = lu.LotNumber AND cln.ShortItemNumber = lu.ShortItemNumber AND cln.BranchCode = lu.BranchCode) INNER JOIN
|
||||
dbo.WorkOrder AS wo ON(lu.WorkOrderNumber = wo.WorkOrderNumber)
|
||||
),
|
||||
CLN_FILTERED_WO AS(
|
||||
SELECT DISTINCT cln.WorkOrderNumber,
|
||||
cln.BranchCode,
|
||||
cln.LotNumber,
|
||||
cln.ShortItemNumber
|
||||
FROM CLN_WO cln
|
||||
)
|
||||
MERGE INTO #Temp_WO AS TARGET
|
||||
USING CLN_FILTERED_WO AS SOURCE
|
||||
ON (TARGET.WorkOrderNumber = SOURCE.WorkOrderNumber AND TARGET.BranchCode = SOURCE.BranchCode)
|
||||
WHEN MATCHED THEN
|
||||
UPDATE SET TARGET.CARDEX = 1
|
||||
WHEN NOT MATCHED BY TARGET THEN
|
||||
INSERT (WorkOrderNumber, LotNumber, BranchCode, ShortItemNumber, CARDEX)
|
||||
VALUES (SOURCE.WorkOrderNumber, COALESCE(SOURCE.LotNumber, CAST(SOURCE.WorkOrderNumber AS VARCHAR(8))), SOURCE.BranchCode, SOURCE.ShortItemNumber, 1);
|
||||
""";
|
||||
setupSql.Add(componentLotMergeSql);
|
||||
|
||||
// Add any work orders split from flagged work orders
|
||||
const string splitOrdersSql = """
|
||||
--Add any work orders split from flagged work orders
|
||||
WITH SP_WO AS
|
||||
(
|
||||
SELECT DISTINCT wo.WorkOrderNumber,
|
||||
wo.LotNumber,
|
||||
wo.BranchCode,
|
||||
wo.ShortItemNumber
|
||||
FROM dbo.WorkOrder AS wo INNER JOIN
|
||||
#Temp_WO AS tw_o ON (wo.ParentWorkOrderNumber = CAST(tw_o.WorkOrderNumber AS VARCHAR(8)) AND wo.BranchCode = tw_o.BranchCode)
|
||||
)
|
||||
MERGE #Temp_WO AS TARGET
|
||||
USING SP_WO AS SOURCE
|
||||
ON (TARGET.WorkOrderNumber = SOURCE.WorkOrderNumber AND TARGET.BranchCode = SOURCE.BranchCode)
|
||||
WHEN MATCHED THEN
|
||||
UPDATE SET TARGET.SplitOrder = 1
|
||||
WHEN NOT MATCHED BY TARGET THEN
|
||||
INSERT (WorkOrderNumber, LotNumber, BranchCode, ShortItemNumber, SplitOrder)
|
||||
VALUES (SOURCE.WorkOrderNumber, SOURCE.LotNumber, SOURCE.BranchCode, SOURCE.ShortItemNumber, 1);
|
||||
""";
|
||||
setupSql.Add(splitOrdersSql);
|
||||
|
||||
return WithSetupSql(setupSql, parameters);
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataAccess.Models;
|
||||
using SqlKata.Compilers;
|
||||
|
||||
namespace JdeScoping.DataAccess.FilterHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for filter handlers providing common functionality.
|
||||
/// </summary>
|
||||
public abstract class FilterHandlerBase : IFilterHandler
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public abstract int Priority { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract bool IsEnabled(SearchModel model);
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract FilterResult Apply(SearchModel model, SqlServerCompiler compiler);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an empty filter result with no SQL or parameters.
|
||||
/// </summary>
|
||||
protected static FilterResult EmptyResult()
|
||||
=> new FilterResult([], new Dictionary<string, object>());
|
||||
|
||||
/// <summary>
|
||||
/// Creates a filter result with setup SQL and optional parameters.
|
||||
/// </summary>
|
||||
protected static FilterResult WithSetupSql(
|
||||
IReadOnlyList<string> setupSql,
|
||||
IDictionary<string, object>? parameters = null)
|
||||
=> new FilterResult(setupSql, parameters ?? new Dictionary<string, object>());
|
||||
|
||||
/// <summary>
|
||||
/// Creates a filter result with a single setup SQL statement.
|
||||
/// </summary>
|
||||
protected static FilterResult WithSetupSql(
|
||||
string sql,
|
||||
IDictionary<string, object>? parameters = null)
|
||||
=> new FilterResult([sql], parameters ?? new Dictionary<string, object>());
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
using JdeScoping.DataAccess.Extensions;
|
||||
using JdeScoping.DataAccess.Models;
|
||||
using SqlKata.Compilers;
|
||||
|
||||
namespace JdeScoping.DataAccess.FilterHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// Filter handler for item number filtering.
|
||||
/// Generates #P_ItemNumbers temp table.
|
||||
/// </summary>
|
||||
public sealed class ItemNumberFilterHandler : FilterHandlerBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override int Priority => 20;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsEnabled(SearchModel model) => model.ItemNumberFilterEnabled;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override FilterResult Apply(SearchModel model, SqlServerCompiler compiler)
|
||||
{
|
||||
var parameters = new Dictionary<string, object>
|
||||
{
|
||||
["p_ItemNumberFilter"] = model.CreateItemNumberFilterParameter()
|
||||
};
|
||||
|
||||
const string setupSql = """
|
||||
--Setup item number filter temp table
|
||||
IF OBJECT_ID('tempdb.dbo.#P_ItemNumbers', 'U') IS NOT NULL
|
||||
BEGIN
|
||||
DROP TABLE #P_ItemNumbers;
|
||||
END
|
||||
CREATE TABLE #P_ItemNumbers (
|
||||
ItemNumber VARCHAR(25) NOT NULL,
|
||||
PRIMARY KEY CLUSTERED(ItemNumber)
|
||||
);
|
||||
|
||||
INSERT INTO #P_ItemNumbers(ItemNumber)
|
||||
SELECT DISTINCT LTRIM(RTRIM(pinf.ItemNumber))
|
||||
FROM @p_ItemNumberFilter AS pinf
|
||||
WHERE LTRIM(RTRIM(pinf.ItemNumber)) IS NOT NULL;
|
||||
""";
|
||||
|
||||
return WithSetupSql(setupSql, parameters);
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
using JdeScoping.DataAccess.Extensions;
|
||||
using JdeScoping.DataAccess.Models;
|
||||
using SqlKata.Compilers;
|
||||
|
||||
namespace JdeScoping.DataAccess.FilterHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// Filter handler for item/operation/MIS filtering.
|
||||
/// Generates #P_PartOperations temp table.
|
||||
/// </summary>
|
||||
public sealed class ItemOperationMisFilterHandler : FilterHandlerBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override int Priority => 70;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsEnabled(SearchModel model) => model.ItemOperationMisFilterEnabled;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override FilterResult Apply(SearchModel model, SqlServerCompiler compiler)
|
||||
{
|
||||
var parameters = new Dictionary<string, object>
|
||||
{
|
||||
["p_ItemOperationMisFilter"] = model.CreateItemOperationMisFilterParameter()
|
||||
};
|
||||
|
||||
const string setupSql = """
|
||||
--Setup item/operation/mis filter temp table
|
||||
IF OBJECT_ID('tempdb.dbo.#P_PartOperations', 'U') IS NOT NULL
|
||||
BEGIN
|
||||
DROP TABLE #P_PartOperations;
|
||||
END
|
||||
CREATE TABLE #P_PartOperations(
|
||||
ItemNumber VARCHAR(32) NOT NULL,
|
||||
OperationNumber VARCHAR(32) NOT NULL,
|
||||
MisNumber VARCHAR(32) NOT NULL,
|
||||
MisRevision VARCHAR(32) NOT NULL,
|
||||
PRIMARY KEY CLUSTERED(ItemNumber, OperationNumber, MisNumber, MisRevision)
|
||||
);
|
||||
|
||||
INSERT INTO #P_PartOperations(ItemNumber, OperationNumber, MisNumber, MisRevision)
|
||||
SELECT DISTINCT LTRIM(RTRIM(piomf.ItemNumber)),
|
||||
LTRIM(RTRIM(piomf.OperationNumber)),
|
||||
LTRIM(RTRIM(piomf.MisNumber)),
|
||||
LTRIM(RTRIM(piomf.MisRevision))
|
||||
FROM @p_ItemOperationMisFilter AS piomf
|
||||
WHERE LTRIM(RTRIM(piomf.ItemNumber)) IS NOT NULL AND
|
||||
LTRIM(RTRIM(piomf.OperationNumber)) IS NOT NULL AND
|
||||
LTRIM(RTRIM(piomf.MisNumber)) IS NOT NULL AND
|
||||
LTRIM(RTRIM(piomf.MisRevision)) IS NOT NULL;
|
||||
""";
|
||||
|
||||
return WithSetupSql(setupSql, parameters);
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
using JdeScoping.DataAccess.Extensions;
|
||||
using JdeScoping.DataAccess.Models;
|
||||
using SqlKata.Compilers;
|
||||
|
||||
namespace JdeScoping.DataAccess.FilterHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// Filter handler for operator (user) filtering.
|
||||
/// Generates #P_OperatorIDs temp table with JdeUser lookup.
|
||||
/// </summary>
|
||||
public sealed class OperatorFilterHandler : FilterHandlerBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override int Priority => 60;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsEnabled(SearchModel model) => model.OperatorFilterEnabled;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override FilterResult Apply(SearchModel model, SqlServerCompiler compiler)
|
||||
{
|
||||
var parameters = new Dictionary<string, object>
|
||||
{
|
||||
["p_OperatorFilter"] = model.CreateOperatorFilterParameter()
|
||||
};
|
||||
|
||||
const string setupSql = """
|
||||
--Setup operator filter temp table
|
||||
IF OBJECT_ID('tempdb.dbo.#P_OperatorIDs', 'U') IS NOT NULL
|
||||
BEGIN
|
||||
DROP TABLE #P_OperatorIDs;
|
||||
END
|
||||
CREATE TABLE #P_OperatorIDs(
|
||||
AddressNumber BIGINT NOT NULL,
|
||||
UserID VARCHAR(10) NULL,
|
||||
PRIMARY KEY CLUSTERED(AddressNumber)
|
||||
);
|
||||
|
||||
WITH O_CTE AS(
|
||||
SELECT ju.AddressNumber,
|
||||
ju.UserID,
|
||||
ROW_NUMBER() OVER(PARTITION BY ju.AddressNumber ORDER BY ju.UserID DESC) RN
|
||||
FROM @p_OperatorFilter AS pof INNER JOIN
|
||||
dbo.JdeUser AS ju ON (LTRIM(RTRIM(pof.UserName)) = ju.UserID)
|
||||
)
|
||||
INSERT INTO #P_OperatorIDs(AddressNumber, UserID)
|
||||
SELECT o.AddressNumber,
|
||||
o.UserID
|
||||
FROM O_CTE o
|
||||
WHERE o.RN = 1;
|
||||
""";
|
||||
|
||||
return WithSetupSql(setupSql, parameters);
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
using JdeScoping.DataAccess.Extensions;
|
||||
using JdeScoping.DataAccess.Models;
|
||||
using SqlKata.Compilers;
|
||||
|
||||
namespace JdeScoping.DataAccess.FilterHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// Filter handler for profit center filtering.
|
||||
/// Generates #P_WorkCenters temp table via OrgHierarchy join.
|
||||
/// Note: This handler creates the shared #P_WorkCenters table that WorkCenterFilterHandler also uses.
|
||||
/// </summary>
|
||||
public sealed class ProfitCenterFilterHandler : FilterHandlerBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override int Priority => 40;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsEnabled(SearchModel model) => model.ProfitCenterFilterEnabled;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override FilterResult Apply(SearchModel model, SqlServerCompiler compiler)
|
||||
{
|
||||
var setupSql = new List<string>();
|
||||
var parameters = new Dictionary<string, object>
|
||||
{
|
||||
["p_ProfitCenterFilter"] = model.CreateProfitCenterFilterParameter()
|
||||
};
|
||||
|
||||
// Create the work centers temp table if it doesn't exist
|
||||
// (It may already exist if WorkCenterFilterHandler ran first, but that has lower priority)
|
||||
const string createTableSql = """
|
||||
--Setup profit center / work center filter temp table
|
||||
IF OBJECT_ID('tempdb.dbo.#P_WorkCenters', 'U') IS NOT NULL
|
||||
BEGIN
|
||||
DROP TABLE #P_WorkCenters;
|
||||
END
|
||||
CREATE TABLE #P_WorkCenters (
|
||||
Code VARCHAR(12) NOT NULL,
|
||||
PRIMARY KEY CLUSTERED(Code)
|
||||
);
|
||||
""";
|
||||
setupSql.Add(createTableSql);
|
||||
|
||||
// Insert work centers from profit center lookup via OrgHierarchy
|
||||
const string mergeSql = """
|
||||
WITH WCF_CTE AS(
|
||||
SELECT LTRIM(RTRIM(oh.WorkCenterCode)) AS Code
|
||||
FROM @p_ProfitCenterFilter AS ppcf INNER JOIN
|
||||
dbo.OrgHierarchy AS oh ON (LTRIM(RTRIM(ppcf.Code)) = oh.ProfitCenterCode)
|
||||
)
|
||||
MERGE INTO #P_WorkCenters AS TARGET
|
||||
USING WCF_CTE AS SOURCE
|
||||
ON (TARGET.Code = SOURCE.Code)
|
||||
WHEN NOT MATCHED BY TARGET THEN
|
||||
INSERT(Code)
|
||||
VALUES(SOURCE.Code);
|
||||
""";
|
||||
setupSql.Add(mergeSql);
|
||||
|
||||
return WithSetupSql(setupSql, parameters);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
using JdeScoping.DataAccess.Models;
|
||||
using SqlKata.Compilers;
|
||||
|
||||
namespace JdeScoping.DataAccess.FilterHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// Filter handler for timespan (date range) filtering.
|
||||
/// Provides parameters for @p_MinimumDT and @p_MaximumDT used in WHERE clauses.
|
||||
/// Note: This handler doesn't generate setup SQL - it only provides parameters
|
||||
/// that are used by the query builder's WHERE clause generation.
|
||||
/// </summary>
|
||||
public sealed class TimespanFilterHandler : FilterHandlerBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override int Priority => 80;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsEnabled(SearchModel model) => model.TimespanFilterEnabled;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override FilterResult Apply(SearchModel model, SqlServerCompiler compiler)
|
||||
{
|
||||
var parameters = new Dictionary<string, object>();
|
||||
|
||||
if (model.MinimumDt.HasValue)
|
||||
{
|
||||
parameters["p_MinimumDT"] = model.MinimumDt.Value;
|
||||
}
|
||||
|
||||
if (model.MaximumDt.HasValue)
|
||||
{
|
||||
parameters["p_MaximumDT"] = model.MaximumDt.Value;
|
||||
}
|
||||
|
||||
// No setup SQL - timespan parameters are used in query WHERE clauses
|
||||
return WithSetupSql([], parameters);
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
using JdeScoping.DataAccess.Extensions;
|
||||
using JdeScoping.DataAccess.Models;
|
||||
using SqlKata.Compilers;
|
||||
|
||||
namespace JdeScoping.DataAccess.FilterHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// Filter handler for work center filtering.
|
||||
/// Generates MERGE into #P_WorkCenters temp table.
|
||||
/// Note: This handler shares the #P_WorkCenters table with ProfitCenterFilterHandler.
|
||||
/// </summary>
|
||||
public sealed class WorkCenterFilterHandler : FilterHandlerBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override int Priority => 50;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsEnabled(SearchModel model) => model.WorkCenterFilterEnabled;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override FilterResult Apply(SearchModel model, SqlServerCompiler compiler)
|
||||
{
|
||||
var setupSql = new List<string>();
|
||||
var parameters = new Dictionary<string, object>
|
||||
{
|
||||
["p_WorkCenterFilter"] = model.CreateWorkCenterFilterParameter()
|
||||
};
|
||||
|
||||
// If profit center filter is not enabled, we need to create the table
|
||||
if (!model.ProfitCenterFilterEnabled)
|
||||
{
|
||||
const string createTableSql = """
|
||||
--Setup work center filter temp table
|
||||
IF OBJECT_ID('tempdb.dbo.#P_WorkCenters', 'U') IS NOT NULL
|
||||
BEGIN
|
||||
DROP TABLE #P_WorkCenters;
|
||||
END
|
||||
CREATE TABLE #P_WorkCenters (
|
||||
Code VARCHAR(12) NOT NULL,
|
||||
PRIMARY KEY CLUSTERED(Code)
|
||||
);
|
||||
""";
|
||||
setupSql.Add(createTableSql);
|
||||
}
|
||||
|
||||
// Merge work center codes into the temp table
|
||||
const string mergeSql = """
|
||||
WITH WCF_CTE AS(
|
||||
SELECT DISTINCT pwcf.Code
|
||||
FROM @p_WorkCenterFilter AS pwcf
|
||||
WHERE LTRIM(RTRIM(pwcf.Code)) IS NOT NULL
|
||||
)
|
||||
MERGE INTO #P_WorkCenters AS TARGET
|
||||
USING WCF_CTE AS SOURCE
|
||||
ON (TARGET.Code = SOURCE.Code)
|
||||
WHEN NOT MATCHED BY TARGET THEN
|
||||
INSERT(Code)
|
||||
VALUES(SOURCE.Code);
|
||||
""";
|
||||
setupSql.Add(mergeSql);
|
||||
|
||||
return WithSetupSql(setupSql, parameters);
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
using JdeScoping.DataAccess.Extensions;
|
||||
using JdeScoping.DataAccess.Models;
|
||||
using SqlKata.Compilers;
|
||||
|
||||
namespace JdeScoping.DataAccess.FilterHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// Filter handler for work order number filtering.
|
||||
/// Generates MERGE into #Temp_WO with ManuallySpecified flag and split order detection.
|
||||
/// </summary>
|
||||
public sealed class WorkOrderFilterHandler : FilterHandlerBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override int Priority => 10;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsEnabled(SearchModel model) => model.WorkOrderFilterEnabled;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override FilterResult Apply(SearchModel model, SqlServerCompiler compiler)
|
||||
{
|
||||
var setupSql = new List<string>();
|
||||
var parameters = new Dictionary<string, object>
|
||||
{
|
||||
["p_WorkOrderFilter"] = model.CreateWorkOrderFilterParameter()
|
||||
};
|
||||
|
||||
// Add manually specified work order numbers to flagged list
|
||||
const string mergeWorkOrdersSql = """
|
||||
--Add manually specified work order numbers to flagged list
|
||||
WITH WOP_CTE AS(
|
||||
SELECT DISTINCT wo.WorkOrderNumber,
|
||||
wo.LotNumber,
|
||||
wo.BranchCode,
|
||||
wo.ShortItemNumber
|
||||
FROM dbo.WorkOrder AS wo INNER JOIN
|
||||
@p_WorkOrderFilter AS pwof ON (wo.WorkOrderNumber = pwof.WorkOrderNumber)
|
||||
)
|
||||
MERGE #Temp_WO AS TARGET
|
||||
USING WOP_CTE AS SOURCE
|
||||
ON (TARGET.WorkOrderNumber = SOURCE.WorkOrderNumber)
|
||||
WHEN MATCHED THEN
|
||||
UPDATE SET TARGET.ManuallySpecified = 1
|
||||
WHEN NOT MATCHED BY TARGET THEN
|
||||
INSERT(WorkOrderNumber, LotNumber, BranchCode, ShortItemNumber, ManuallySpecified)
|
||||
VALUES(SOURCE.WorkOrderNumber, SOURCE.LotNumber, SOURCE.BranchCode, SOURCE.ShortItemNumber, 1);
|
||||
""";
|
||||
setupSql.Add(mergeWorkOrdersSql);
|
||||
|
||||
// Add any work orders split from flagged work orders
|
||||
const string splitOrdersSql = """
|
||||
--Add any work orders split from flagged work orders
|
||||
WITH SP_WO AS
|
||||
(
|
||||
SELECT DISTINCT wo.WorkOrderNumber,
|
||||
wo.LotNumber,
|
||||
wo.BranchCode,
|
||||
wo.ShortItemNumber
|
||||
FROM dbo.WorkOrder AS wo INNER JOIN
|
||||
#Temp_WO AS tw_o ON (wo.ParentWorkOrderNumber = CAST(tw_o.WorkOrderNumber AS VARCHAR(8)) AND wo.BranchCode = tw_o.BranchCode)
|
||||
)
|
||||
MERGE #Temp_WO AS TARGET
|
||||
USING SP_WO AS SOURCE
|
||||
ON (TARGET.WorkOrderNumber = SOURCE.WorkOrderNumber AND TARGET.BranchCode = SOURCE.BranchCode)
|
||||
WHEN MATCHED THEN
|
||||
UPDATE SET TARGET.SplitOrder = 1
|
||||
WHEN NOT MATCHED BY TARGET THEN
|
||||
INSERT (WorkOrderNumber, LotNumber, BranchCode, ShortItemNumber, SplitOrder)
|
||||
VALUES (SOURCE.WorkOrderNumber, SOURCE.LotNumber, SOURCE.BranchCode, SOURCE.ShortItemNumber, 1);
|
||||
""";
|
||||
setupSql.Add(splitOrdersSql);
|
||||
|
||||
return WithSetupSql(setupSql, parameters);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
using JdeScoping.DataAccess.Models;
|
||||
using SqlKata.Compilers;
|
||||
|
||||
namespace JdeScoping.DataAccess.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for filter handlers that build SQL query fragments.
|
||||
/// </summary>
|
||||
public interface IFilterHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines if this filter is active for the given search model.
|
||||
/// </summary>
|
||||
/// <param name="model">The search model to check.</param>
|
||||
/// <returns>True if the filter is enabled and should be applied.</returns>
|
||||
bool IsEnabled(SearchModel model);
|
||||
|
||||
/// <summary>
|
||||
/// Applies the filter, returning setup SQL and parameters.
|
||||
/// </summary>
|
||||
/// <param name="model">The search model containing filter criteria.</param>
|
||||
/// <param name="compiler">The SqlKata compiler for SQL generation.</param>
|
||||
/// <returns>Filter result containing setup SQL and parameters.</returns>
|
||||
FilterResult Apply(SearchModel model, SqlServerCompiler compiler);
|
||||
|
||||
/// <summary>
|
||||
/// Priority for handler execution order (lower = earlier).
|
||||
/// </summary>
|
||||
int Priority { get; }
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
using JdeScoping.DataAccess.Attributes;
|
||||
|
||||
namespace JdeScoping.DataAccess.Models.FilterEntries;
|
||||
|
||||
/// <summary>
|
||||
/// Component lot search filter entry.
|
||||
/// </summary>
|
||||
[OutputTable(TabName = "Component Lot Filter", ShowHeader = true, TableName = "Component_Lot_Filter")]
|
||||
public sealed record ComponentLotFilterEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Component lot number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 10, HeaderText = "Lot Number")]
|
||||
public string LotNumber { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Component lot item number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 20, HeaderText = "Item Number")]
|
||||
public string ItemNumber { get; init; } = string.Empty;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
using JdeScoping.DataAccess.Attributes;
|
||||
|
||||
namespace JdeScoping.DataAccess.Models.FilterEntries;
|
||||
|
||||
/// <summary>
|
||||
/// Item number search filter entry.
|
||||
/// </summary>
|
||||
[OutputTable(TabName = "Item Number Filter", ShowHeader = true, TableName = "Item_Number_Filter")]
|
||||
public sealed record ItemNumberFilterEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Item number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 10, HeaderText = "Item Number")]
|
||||
public string ItemNumber { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Item description.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 20, HeaderText = "Item Description")]
|
||||
public string ItemDescription { get; init; } = string.Empty;
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
using JdeScoping.DataAccess.Attributes;
|
||||
|
||||
namespace JdeScoping.DataAccess.Models.FilterEntries;
|
||||
|
||||
/// <summary>
|
||||
/// Item/operation/MIS search filter entry.
|
||||
/// </summary>
|
||||
[OutputTable(TabName = "Item/Operation/MIS Filter", ShowHeader = true, TableName = "Item_Operation_MIS_Filter")]
|
||||
public sealed record ItemOperationMisFilterEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Part's item number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 10, HeaderText = "Item Number")]
|
||||
public string ItemNumber { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Operation's job step number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 20, HeaderText = "Operation Number")]
|
||||
public string OperationNumber { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// MIS number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 30, HeaderText = "MIS Number")]
|
||||
public string MisNumber { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// MIS revision.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 40, HeaderText = "MIS Revision")]
|
||||
public string MisRevision { get; init; } = string.Empty;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
using JdeScoping.DataAccess.Attributes;
|
||||
|
||||
namespace JdeScoping.DataAccess.Models.FilterEntries;
|
||||
|
||||
/// <summary>
|
||||
/// Operator search filter entry.
|
||||
/// </summary>
|
||||
[OutputTable(TabName = "Operator Filter", ShowHeader = true, TableName = "Operator_Filter")]
|
||||
public sealed record OperatorFilterEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Operator unique JDE address number.
|
||||
/// </summary>
|
||||
public long AddressNumber { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Operator login user ID.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 10, HeaderText = "Username")]
|
||||
public string UserId { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Operator full name (FIRST + LAST).
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 20, HeaderText = "Name")]
|
||||
public string FullName { get; init; } = string.Empty;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
using JdeScoping.DataAccess.Attributes;
|
||||
|
||||
namespace JdeScoping.DataAccess.Models.FilterEntries;
|
||||
|
||||
/// <summary>
|
||||
/// Profit center search filter entry.
|
||||
/// </summary>
|
||||
[OutputTable(TabName = "Profit Center Filter", ShowHeader = true, TableName = "Profit_Center_Filter")]
|
||||
public sealed record ProfitCenterFilterEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Profit center code.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 10, HeaderText = "Profit Center")]
|
||||
public string Code { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Profit center description.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 20, HeaderText = "Description")]
|
||||
public string Description { get; init; } = string.Empty;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
using JdeScoping.DataAccess.Attributes;
|
||||
|
||||
namespace JdeScoping.DataAccess.Models.FilterEntries;
|
||||
|
||||
/// <summary>
|
||||
/// Work center search filter entry.
|
||||
/// </summary>
|
||||
[OutputTable(TabName = "Work Center Filter", ShowHeader = true, TableName = "Work_Center_Filter")]
|
||||
public sealed record WorkCenterFilterEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Work center code.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 10, HeaderText = "Work Center")]
|
||||
public string Code { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Work center description.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 20, HeaderText = "Description")]
|
||||
public string Description { get; init; } = string.Empty;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
using JdeScoping.DataAccess.Attributes;
|
||||
|
||||
namespace JdeScoping.DataAccess.Models.FilterEntries;
|
||||
|
||||
/// <summary>
|
||||
/// Work order search filter entry.
|
||||
/// </summary>
|
||||
[OutputTable(TabName = "Work Order Filter", ShowHeader = true, TableName = "Work_Order_Filter")]
|
||||
public sealed record WorkOrderFilterEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Work order number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 10, HeaderText = "Work Order Number")]
|
||||
public long WorkOrderNumber { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Work order item number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 20, HeaderText = "Item Number")]
|
||||
public string ItemNumber { get; init; } = string.Empty;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace JdeScoping.DataAccess.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Result of applying a filter handler.
|
||||
/// </summary>
|
||||
/// <param name="SetupSql">List of SQL statements for filter setup (temp tables, etc.).</param>
|
||||
/// <param name="Parameters">Dictionary of parameter names and values.</param>
|
||||
public sealed record FilterResult(
|
||||
IReadOnlyList<string> SetupSql,
|
||||
IDictionary<string, object> Parameters);
|
||||
@@ -1,10 +1,11 @@
|
||||
using JdeScoping.DataAccess.Models.FilterEntries;
|
||||
using JdeScoping.DataAccess.Models.Results;
|
||||
|
||||
namespace JdeScoping.DataAccess.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Reporting search data model.
|
||||
/// Filter criteria are stored as JSON in the Search.Criteria column
|
||||
/// and are extracted using SQL functions (fn_GetSearch*) during query execution.
|
||||
/// </summary>
|
||||
public class SearchModel
|
||||
{
|
||||
@@ -38,96 +39,6 @@ public class SearchModel
|
||||
/// </summary>
|
||||
public DateTime? EndDt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Minimum timestamp to include.
|
||||
/// </summary>
|
||||
public DateTime? MinimumDt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum timestamp to include.
|
||||
/// </summary>
|
||||
public DateTime? MaximumDt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not timespan filter is enabled.
|
||||
/// </summary>
|
||||
public bool TimespanFilterEnabled => MinimumDt.HasValue || MaximumDt.HasValue;
|
||||
|
||||
/// <summary>
|
||||
/// Collection of work order numbers to include.
|
||||
/// </summary>
|
||||
public List<WorkOrderFilterEntry> WorkOrderFilter { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not work order filter is enabled.
|
||||
/// </summary>
|
||||
public bool WorkOrderFilterEnabled => WorkOrderFilter is { Count: > 0 };
|
||||
|
||||
/// <summary>
|
||||
/// Collection of item numbers to include.
|
||||
/// </summary>
|
||||
public List<ItemNumberFilterEntry> ItemNumberFilter { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not item number filter is enabled.
|
||||
/// </summary>
|
||||
public bool ItemNumberFilterEnabled => ItemNumberFilter is { Count: > 0 };
|
||||
|
||||
/// <summary>
|
||||
/// Collection of included profit centers.
|
||||
/// </summary>
|
||||
public List<ProfitCenterFilterEntry> ProfitCenterFilter { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not profit center filter is enabled.
|
||||
/// </summary>
|
||||
public bool ProfitCenterFilterEnabled => ProfitCenterFilter is { Count: > 0 };
|
||||
|
||||
/// <summary>
|
||||
/// Collection of included work centers.
|
||||
/// </summary>
|
||||
public List<WorkCenterFilterEntry> WorkCenterFilter { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not work center filter is enabled.
|
||||
/// </summary>
|
||||
public bool WorkCenterFilterEnabled => WorkCenterFilter is { Count: > 0 };
|
||||
|
||||
/// <summary>
|
||||
/// Collection of included operator IDs.
|
||||
/// </summary>
|
||||
public List<OperatorFilterEntry> OperatorFilter { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not operator filter is enabled.
|
||||
/// </summary>
|
||||
public bool OperatorFilterEnabled => OperatorFilter is { Count: > 0 };
|
||||
|
||||
/// <summary>
|
||||
/// Collection of included upper level lot numbers.
|
||||
/// </summary>
|
||||
public List<ComponentLotFilterEntry> ComponentLotFilter { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not component lot filter is enabled.
|
||||
/// </summary>
|
||||
public bool ComponentLotFilterEnabled => ComponentLotFilter is { Count: > 0 };
|
||||
|
||||
/// <summary>
|
||||
/// List of part/operation combinations for MIS filtering.
|
||||
/// </summary>
|
||||
public List<ItemOperationMisFilterEntry> ItemOperationMisFilter { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not item/operation/mis filter is enabled.
|
||||
/// </summary>
|
||||
public bool ItemOperationMisFilterEnabled => ItemOperationMisFilter is { Count: > 0 };
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to extract MIS data.
|
||||
/// </summary>
|
||||
public bool ExtractMisData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Work order search results.
|
||||
/// </summary>
|
||||
|
||||
@@ -73,12 +73,18 @@ public sealed class MisQueryBuilder
|
||||
""";
|
||||
}
|
||||
|
||||
private string BuildMisCteSql(SearchModel model)
|
||||
private static string BuildMisCteSql()
|
||||
{
|
||||
var joins = BuildMisJoins(model);
|
||||
var whereClause = BuildMisWhereClause(model);
|
||||
// The SQL uses temp tables (#P_ItemNumbers, #P_WorkCenters, #P_OperatorIDs)
|
||||
// and variables (@MinDt, @MaxDt) that were already populated by the main search query builder.
|
||||
// This query uses dynamic join/filter conditions based on what filters exist.
|
||||
return """
|
||||
-- Build MIS extraction using existing temp tables and date variables
|
||||
DECLARE @HasMisItemFilter BIT = CASE WHEN EXISTS(SELECT 1 FROM #P_ItemNumbers) THEN 1 ELSE 0 END;
|
||||
DECLARE @HasMisWorkCenterFilter BIT = CASE WHEN EXISTS(SELECT 1 FROM #P_WorkCenters) THEN 1 ELSE 0 END;
|
||||
DECLARE @HasMisOperatorFilter BIT = CASE WHEN EXISTS(SELECT 1 FROM #P_OperatorIDs) THEN 1 ELSE 0 END;
|
||||
DECLARE @HasMisTimespanFilter BIT = CASE WHEN @MinDt IS NOT NULL OR @MaxDt IS NOT NULL THEN 1 ELSE 0 END;
|
||||
|
||||
return $"""
|
||||
WITH MIS_CTE AS(
|
||||
SELECT DISTINCT wo.WorkOrderNumber,
|
||||
wo.ItemNumber,
|
||||
@@ -90,11 +96,20 @@ public sealed class MisQueryBuilder
|
||||
wos.EndDT,
|
||||
wos.FunctionCode,
|
||||
wos.FunctionOperationDescription
|
||||
FROM dbo.WorkOrderStep AS wos INNER JOIN
|
||||
dbo.WorkOrder AS wo ON (wos.WorkOrderNumber = wo.WorkOrderNumber AND LTRIM(wos.BranchCode) = wo.BranchCode) LEFT OUTER JOIN
|
||||
dbo.WorkOrderTime AS wot ON (wos.WorkOrderNumber = wot.WorkOrderNumber AND LTRIM(wos.BranchCode) = wot.BranchCode AND wos.StepNumber = wot.StepNumber){joins}{whereClause}
|
||||
FROM dbo.WorkOrderStep AS wos
|
||||
INNER JOIN dbo.WorkOrder AS wo ON (wos.WorkOrderNumber = wo.WorkOrderNumber AND LTRIM(wos.BranchCode) = wo.BranchCode)
|
||||
LEFT OUTER JOIN dbo.WorkOrderTime AS wot ON (wos.WorkOrderNumber = wot.WorkOrderNumber AND LTRIM(wos.BranchCode) = wot.BranchCode AND wos.StepNumber = wot.StepNumber)
|
||||
LEFT OUTER JOIN #P_ItemNumbers p_in ON (@HasMisItemFilter = 0 OR wo.ItemNumber = p_in.ItemNumber)
|
||||
LEFT OUTER JOIN #P_WorkCenters p_wc ON (@HasMisWorkCenterFilter = 0 OR wos.WorkCenterCode = p_wc.Code)
|
||||
LEFT OUTER JOIN #P_OperatorIDs p_oi ON (@HasMisOperatorFilter = 0 OR wot.AddressNumber = p_oi.AddressNumber)
|
||||
WHERE (@HasMisItemFilter = 0 OR p_in.ItemNumber IS NOT NULL)
|
||||
AND (@HasMisWorkCenterFilter = 0 OR p_wc.Code IS NOT NULL)
|
||||
AND (@HasMisOperatorFilter = 0 OR p_oi.AddressNumber IS NOT NULL)
|
||||
AND (@HasMisTimespanFilter = 0 OR
|
||||
((@MinDt IS NULL OR wos.EndDT >= @MinDt OR wot.GlDate >= @MinDt) AND
|
||||
(@MaxDt IS NULL OR wos.EndDT <= @MaxDt OR wot.GlDate <= @MaxDt)))
|
||||
)
|
||||
INSERT INTO #TempMISData
|
||||
INSERT INTO #TempMisData
|
||||
(
|
||||
WorkOrderNumber,
|
||||
ItemNumber,
|
||||
@@ -149,56 +164,4 @@ public sealed class MisQueryBuilder
|
||||
c.FunctionCode, c.FunctionOperationDescription) AS mm;
|
||||
""";
|
||||
}
|
||||
|
||||
private static string BuildMisJoins(SearchModel model)
|
||||
{
|
||||
var joins = new List<string>();
|
||||
|
||||
if (model.ItemNumberFilterEnabled)
|
||||
{
|
||||
joins.Add(" INNER JOIN\r\n #P_ItemNumbers p_in ON (wo.ItemNumber = p_in.ItemNumber)");
|
||||
}
|
||||
|
||||
if (model.ProfitCenterFilterEnabled || model.WorkCenterFilterEnabled)
|
||||
{
|
||||
joins.Add(" INNER JOIN \r\n #P_WorkCenters p_wc ON (wos.WorkCenterCode = p_wc.Code)");
|
||||
}
|
||||
|
||||
if (model.OperatorFilterEnabled)
|
||||
{
|
||||
joins.Add(" INNER JOIN \r\n #P_OperatorIDs p_oi ON (wot.AddressNumber = p_oi.AddressNumber)");
|
||||
}
|
||||
|
||||
return string.Join("", joins);
|
||||
}
|
||||
|
||||
private static string BuildMisWhereClause(SearchModel model)
|
||||
{
|
||||
if (model.MinimumDt.HasValue && model.MaximumDt.HasValue)
|
||||
{
|
||||
return """
|
||||
|
||||
WHERE (((wos.EndDT <= @p_MaximumDT) AND (wos.EndDT >= @p_MinimumDT)) OR
|
||||
((wot.GlDate <= @p_MaximumDT) AND (wot.GlDate >= @p_MinimumDT)))
|
||||
""";
|
||||
}
|
||||
|
||||
if (model.MinimumDt.HasValue && !model.MaximumDt.HasValue)
|
||||
{
|
||||
return """
|
||||
|
||||
WHERE (wos.EndDT >= @p_MinimumDT OR wot.GlDate >= @p_MinimumDT)
|
||||
""";
|
||||
}
|
||||
|
||||
if (!model.MinimumDt.HasValue && model.MaximumDt.HasValue)
|
||||
{
|
||||
return """
|
||||
|
||||
WHERE (wos.EndDT <= @p_MaximumDT OR wot.GlDate <= @p_MaximumDT)
|
||||
""";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,8 +145,13 @@ public sealed class SearchProcessor
|
||||
model.Results = results.ToList();
|
||||
_logger.LogInformation("Search {SearchId} returned {ResultCount} results", model.Id, model.Results.Count);
|
||||
|
||||
// Extract MIS data if requested
|
||||
if (model.ExtractMisData)
|
||||
// Extract MIS data if requested (check ExtractMisData from database using extraction function)
|
||||
var extractMisData = await connection.QuerySingleOrDefaultAsync<bool?>(
|
||||
"SELECT dbo.fn_GetSearchExtractMisData(@SearchId)",
|
||||
new { SearchId = model.Id },
|
||||
commandTimeout: _options.QueryTimeoutSeconds) ?? false;
|
||||
|
||||
if (extractMisData)
|
||||
{
|
||||
await ExecuteMisExtractionAsync(model, connection, ct);
|
||||
}
|
||||
@@ -161,24 +166,14 @@ public sealed class SearchProcessor
|
||||
{
|
||||
_logger.LogDebug("Extracting MIS data for search {SearchId}", model.Id);
|
||||
|
||||
// Build and execute MIS setup SQL
|
||||
var misSetupStatements = _misQueryBuilder.BuildMisExtractionSql(model);
|
||||
var misParameters = new Dictionary<string, object>();
|
||||
|
||||
if (model.MinimumDt.HasValue)
|
||||
{
|
||||
misParameters["p_MinimumDT"] = model.MinimumDt.Value;
|
||||
}
|
||||
if (model.MaximumDt.HasValue)
|
||||
{
|
||||
misParameters["p_MaximumDT"] = model.MaximumDt.Value;
|
||||
}
|
||||
// Build and execute MIS setup SQL (uses temp tables and variables from main query)
|
||||
var misSetupStatements = _misQueryBuilder.BuildMisExtractionSql(model.Id);
|
||||
|
||||
foreach (var sql in misSetupStatements)
|
||||
{
|
||||
await connection.ExecuteAsync(
|
||||
sql,
|
||||
misParameters,
|
||||
new { SearchId = model.Id },
|
||||
commandTimeout: _options.QueryTimeoutSeconds);
|
||||
}
|
||||
|
||||
|
||||
-480
@@ -1,480 +0,0 @@
|
||||
using System.Data;
|
||||
using System.Reflection;
|
||||
using Dapper;
|
||||
using JdeScoping.DataAccess.Extensions;
|
||||
using JdeScoping.DataAccess.Models;
|
||||
using JdeScoping.DataAccess.Models.FilterEntries;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace JdeScoping.DataAccess.Tests.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for TableValuedParameterExtensions.
|
||||
/// </summary>
|
||||
public sealed class TableValuedParameterExtensionsTests
|
||||
{
|
||||
#region CreateWorkOrderFilterParameter Tests
|
||||
|
||||
[Fact]
|
||||
public void CreateWorkOrderFilterParameter_ProducesCorrectSchema()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
WorkOrderFilter =
|
||||
[
|
||||
new WorkOrderFilterEntry { WorkOrderNumber = 12345, ItemNumber = "ABC" }
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateWorkOrderFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.ShouldNotBeNull();
|
||||
dataTable.Columns.Count.ShouldBe(1);
|
||||
dataTable.Columns.Contains("WorkOrderNumber").ShouldBeTrue();
|
||||
dataTable.Columns["WorkOrderNumber"]!.DataType.ShouldBe(typeof(long));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateWorkOrderFilterParameter_WithEmptyCollection_ProducesEmptyDataTable()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
WorkOrderFilter = []
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateWorkOrderFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.ShouldNotBeNull();
|
||||
dataTable.Rows.Count.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateWorkOrderFilterParameter_PopulatesCorrectData()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
WorkOrderFilter =
|
||||
[
|
||||
new WorkOrderFilterEntry { WorkOrderNumber = 12345 },
|
||||
new WorkOrderFilterEntry { WorkOrderNumber = 67890 }
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateWorkOrderFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.Rows.Count.ShouldBe(2);
|
||||
dataTable.Rows[0]["WorkOrderNumber"].ShouldBe(12345L);
|
||||
dataTable.Rows[1]["WorkOrderNumber"].ShouldBe(67890L);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CreateItemNumberFilterParameter Tests
|
||||
|
||||
[Fact]
|
||||
public void CreateItemNumberFilterParameter_ProducesCorrectSchema()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
ItemNumberFilter =
|
||||
[
|
||||
new ItemNumberFilterEntry { ItemNumber = "ABC123" }
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateItemNumberFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.ShouldNotBeNull();
|
||||
dataTable.Columns.Count.ShouldBe(1);
|
||||
dataTable.Columns.Contains("ItemNumber").ShouldBeTrue();
|
||||
dataTable.Columns["ItemNumber"]!.DataType.ShouldBe(typeof(string));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateItemNumberFilterParameter_WithEmptyCollection_ProducesEmptyDataTable()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
ItemNumberFilter = []
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateItemNumberFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.Rows.Count.ShouldBe(0);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CreateProfitCenterFilterParameter Tests
|
||||
|
||||
[Fact]
|
||||
public void CreateProfitCenterFilterParameter_ProducesCorrectSchema()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
ProfitCenterFilter =
|
||||
[
|
||||
new ProfitCenterFilterEntry { Code = "PC001" }
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateProfitCenterFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.Columns.Count.ShouldBe(1);
|
||||
dataTable.Columns.Contains("Code").ShouldBeTrue();
|
||||
dataTable.Columns["Code"]!.DataType.ShouldBe(typeof(string));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateProfitCenterFilterParameter_WithEmptyCollection_ProducesEmptyDataTable()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
ProfitCenterFilter = []
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateProfitCenterFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.Rows.Count.ShouldBe(0);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CreateWorkCenterFilterParameter Tests
|
||||
|
||||
[Fact]
|
||||
public void CreateWorkCenterFilterParameter_ProducesCorrectSchema()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
WorkCenterFilter =
|
||||
[
|
||||
new WorkCenterFilterEntry { Code = "WC001" }
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateWorkCenterFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.Columns.Count.ShouldBe(1);
|
||||
dataTable.Columns.Contains("Code").ShouldBeTrue();
|
||||
dataTable.Columns["Code"]!.DataType.ShouldBe(typeof(string));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateWorkCenterFilterParameter_WithEmptyCollection_ProducesEmptyDataTable()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
WorkCenterFilter = []
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateWorkCenterFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.Rows.Count.ShouldBe(0);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CreateComponentLotFilterParameter Tests
|
||||
|
||||
[Fact]
|
||||
public void CreateComponentLotFilterParameter_ProducesCorrectSchema()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
ComponentLotFilter =
|
||||
[
|
||||
new ComponentLotFilterEntry { LotNumber = "LOT001", ItemNumber = "ITEM001" }
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateComponentLotFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.Columns.Count.ShouldBe(2);
|
||||
dataTable.Columns.Contains("ComponentLotNumber").ShouldBeTrue();
|
||||
dataTable.Columns.Contains("ItemNumber").ShouldBeTrue();
|
||||
dataTable.Columns["ComponentLotNumber"]!.DataType.ShouldBe(typeof(string));
|
||||
dataTable.Columns["ItemNumber"]!.DataType.ShouldBe(typeof(string));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateComponentLotFilterParameter_WithEmptyCollection_ProducesEmptyDataTable()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
ComponentLotFilter = []
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateComponentLotFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.Rows.Count.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateComponentLotFilterParameter_PopulatesCorrectData()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
ComponentLotFilter =
|
||||
[
|
||||
new ComponentLotFilterEntry { LotNumber = "LOT001", ItemNumber = "ITEM001" },
|
||||
new ComponentLotFilterEntry { LotNumber = "LOT002", ItemNumber = "ITEM002" }
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateComponentLotFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.Rows.Count.ShouldBe(2);
|
||||
dataTable.Rows[0]["ComponentLotNumber"].ShouldBe("LOT001");
|
||||
dataTable.Rows[0]["ItemNumber"].ShouldBe("ITEM001");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CreateOperatorFilterParameter Tests
|
||||
|
||||
[Fact]
|
||||
public void CreateOperatorFilterParameter_ProducesCorrectSchema()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
OperatorFilter =
|
||||
[
|
||||
new OperatorFilterEntry { UserId = "USER01", AddressNumber = 123 }
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateOperatorFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.Columns.Count.ShouldBe(1);
|
||||
dataTable.Columns.Contains("UserName").ShouldBeTrue();
|
||||
dataTable.Columns["UserName"]!.DataType.ShouldBe(typeof(string));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateOperatorFilterParameter_WithEmptyCollection_ProducesEmptyDataTable()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
OperatorFilter = []
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateOperatorFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.Rows.Count.ShouldBe(0);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CreateItemOperationMisFilterParameter Tests
|
||||
|
||||
[Fact]
|
||||
public void CreateItemOperationMisFilterParameter_ProducesCorrectSchema()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
ItemOperationMisFilter =
|
||||
[
|
||||
new ItemOperationMisFilterEntry
|
||||
{
|
||||
ItemNumber = "ITEM001",
|
||||
OperationNumber = "010",
|
||||
MisNumber = "MIS001",
|
||||
MisRevision = "A"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateItemOperationMisFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.Columns.Count.ShouldBe(4);
|
||||
dataTable.Columns.Contains("ItemNumber").ShouldBeTrue();
|
||||
dataTable.Columns.Contains("OperationNumber").ShouldBeTrue();
|
||||
dataTable.Columns.Contains("MisNumber").ShouldBeTrue();
|
||||
dataTable.Columns.Contains("MisRevision").ShouldBeTrue();
|
||||
|
||||
dataTable.Columns["ItemNumber"]!.DataType.ShouldBe(typeof(string));
|
||||
dataTable.Columns["OperationNumber"]!.DataType.ShouldBe(typeof(string));
|
||||
dataTable.Columns["MisNumber"]!.DataType.ShouldBe(typeof(string));
|
||||
dataTable.Columns["MisRevision"]!.DataType.ShouldBe(typeof(string));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateItemOperationMisFilterParameter_WithEmptyCollection_ProducesEmptyDataTable()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
ItemOperationMisFilter = []
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateItemOperationMisFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.Rows.Count.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateItemOperationMisFilterParameter_PopulatesCorrectData()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
ItemOperationMisFilter =
|
||||
[
|
||||
new ItemOperationMisFilterEntry
|
||||
{
|
||||
ItemNumber = "ITEM001",
|
||||
OperationNumber = "010",
|
||||
MisNumber = "MIS001",
|
||||
MisRevision = "A"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateItemOperationMisFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.Rows.Count.ShouldBe(1);
|
||||
dataTable.Rows[0]["ItemNumber"].ShouldBe("ITEM001");
|
||||
dataTable.Rows[0]["OperationNumber"].ShouldBe("010");
|
||||
dataTable.Rows[0]["MisNumber"].ShouldBe("MIS001");
|
||||
dataTable.Rows[0]["MisRevision"].ShouldBe("A");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the underlying DataTable from a Dapper table-valued parameter.
|
||||
/// Uses reflection to access internal fields across different Dapper versions.
|
||||
/// </summary>
|
||||
private static DataTable ExtractDataTable(SqlMapper.ICustomQueryParameter param)
|
||||
{
|
||||
// The TableValuedParameter wraps a DataTable - try multiple field/property names
|
||||
// across different Dapper versions
|
||||
var type = param.GetType();
|
||||
var bindingFlags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;
|
||||
|
||||
// Try field names used in different Dapper versions
|
||||
var fieldNames = new[] { "_table", "table", "Table", "_dataTable", "dataTable" };
|
||||
foreach (var fieldName in fieldNames)
|
||||
{
|
||||
var field = type.GetField(fieldName, bindingFlags);
|
||||
if (field != null && field.FieldType == typeof(DataTable))
|
||||
{
|
||||
var value = field.GetValue(param);
|
||||
if (value is DataTable dt)
|
||||
return dt;
|
||||
}
|
||||
}
|
||||
|
||||
// Try property names
|
||||
var propertyNames = new[] { "Table", "DataTable", "table", "_table" };
|
||||
foreach (var propName in propertyNames)
|
||||
{
|
||||
var prop = type.GetProperty(propName, bindingFlags);
|
||||
if (prop != null && prop.PropertyType == typeof(DataTable))
|
||||
{
|
||||
var value = prop.GetValue(param);
|
||||
if (value is DataTable dt)
|
||||
return dt;
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort: scan all fields
|
||||
foreach (var field in type.GetFields(bindingFlags))
|
||||
{
|
||||
if (field.FieldType == typeof(DataTable))
|
||||
{
|
||||
var value = field.GetValue(param);
|
||||
if (value is DataTable dt)
|
||||
return dt;
|
||||
}
|
||||
}
|
||||
|
||||
// Scan all properties
|
||||
foreach (var prop in type.GetProperties(bindingFlags))
|
||||
{
|
||||
if (prop.PropertyType == typeof(DataTable))
|
||||
{
|
||||
var value = prop.GetValue(param);
|
||||
if (value is DataTable dt)
|
||||
return dt;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Could not extract DataTable from {type.FullName}. " +
|
||||
$"Fields: {string.Join(", ", type.GetFields(bindingFlags).Select(f => f.Name))}. " +
|
||||
$"Properties: {string.Join(", ", type.GetProperties(bindingFlags).Select(p => p.Name))}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
-196
@@ -1,196 +0,0 @@
|
||||
using JdeScoping.DataAccess.FilterHandlers;
|
||||
using JdeScoping.DataAccess.Models;
|
||||
using JdeScoping.DataAccess.Models.FilterEntries;
|
||||
using Shouldly;
|
||||
using SqlKata.Compilers;
|
||||
using Xunit;
|
||||
|
||||
namespace JdeScoping.DataAccess.Tests.FilterHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for ComponentLotFilterHandler.
|
||||
/// </summary>
|
||||
public sealed class ComponentLotFilterHandlerTests
|
||||
{
|
||||
private readonly SqlServerCompiler _compiler = new();
|
||||
private readonly ComponentLotFilterHandler _handler = new();
|
||||
|
||||
[Fact]
|
||||
public void IsEnabled_WithComponentLotFilters_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
ComponentLotFilter =
|
||||
[
|
||||
new ComponentLotFilterEntry { LotNumber = "LOT001", ItemNumber = "ITEM001" }
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _handler.IsEnabled(model);
|
||||
|
||||
// Assert
|
||||
result.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsEnabled_WithEmptyComponentLotFilters_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
ComponentLotFilter = []
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _handler.IsEnabled(model);
|
||||
|
||||
// Assert
|
||||
result.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsEnabled_WithNullComponentLotFilters_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel();
|
||||
|
||||
// Act
|
||||
var result = _handler.IsEnabled(model);
|
||||
|
||||
// Assert
|
||||
result.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_GeneratedSql_ContainsWorkOrderComponentJoin()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
ComponentLotFilter =
|
||||
[
|
||||
new ComponentLotFilterEntry { LotNumber = "LOT001", ItemNumber = "ITEM001" }
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _handler.Apply(model, _compiler);
|
||||
|
||||
// Assert
|
||||
result.ShouldNotBeNull();
|
||||
result.SetupSql.ShouldNotBeEmpty();
|
||||
|
||||
var allSql = string.Join("\n", result.SetupSql);
|
||||
allSql.ShouldContain("dbo.WorkOrderComponent AS woc");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_GeneratedSql_ContainsLotUsageJoin()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
ComponentLotFilter =
|
||||
[
|
||||
new ComponentLotFilterEntry { LotNumber = "LOT001", ItemNumber = "ITEM001" }
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _handler.Apply(model, _compiler);
|
||||
|
||||
// Assert
|
||||
var allSql = string.Join("\n", result.SetupSql);
|
||||
allSql.ShouldContain("dbo.LotUsage AS lu");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_GeneratedSql_SetsCARDEXFlag()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
ComponentLotFilter =
|
||||
[
|
||||
new ComponentLotFilterEntry { LotNumber = "LOT001", ItemNumber = "ITEM001" }
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _handler.Apply(model, _compiler);
|
||||
|
||||
// Assert
|
||||
var allSql = string.Join("\n", result.SetupSql);
|
||||
// CARDEX flag is set (not PartsList) per the ComponentLotFilterHandler implementation
|
||||
allSql.ShouldContain("TARGET.CARDEX = 1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_GeneratedSql_DoesNotSetPartsListFlag()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
ComponentLotFilter =
|
||||
[
|
||||
new ComponentLotFilterEntry { LotNumber = "LOT001", ItemNumber = "ITEM001" }
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _handler.Apply(model, _compiler);
|
||||
|
||||
// Assert
|
||||
var allSql = string.Join("\n", result.SetupSql);
|
||||
// ComponentLotFilterHandler sets CARDEX, not PartsList
|
||||
allSql.ShouldNotContain("PartsList = 1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_GeneratedSql_ContainsSplitOrderLogic()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
ComponentLotFilter =
|
||||
[
|
||||
new ComponentLotFilterEntry { LotNumber = "LOT001", ItemNumber = "ITEM001" }
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _handler.Apply(model, _compiler);
|
||||
|
||||
// Assert
|
||||
var allSql = string.Join("\n", result.SetupSql);
|
||||
allSql.ShouldContain("SplitOrder");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_Parameters_ContainsComponentLotFilterParameter()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
ComponentLotFilter =
|
||||
[
|
||||
new ComponentLotFilterEntry { LotNumber = "LOT001", ItemNumber = "ITEM001" }
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _handler.Apply(model, _compiler);
|
||||
|
||||
// Assert
|
||||
result.Parameters.ShouldContainKey("p_ComponentLotFilter");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Priority_ReturnsExpectedValue()
|
||||
{
|
||||
// Assert
|
||||
_handler.Priority.ShouldBe(30);
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
using JdeScoping.DataAccess.FilterHandlers;
|
||||
using JdeScoping.DataAccess.Models;
|
||||
using JdeScoping.DataAccess.Models.FilterEntries;
|
||||
using Shouldly;
|
||||
using SqlKata.Compilers;
|
||||
using Xunit;
|
||||
|
||||
namespace JdeScoping.DataAccess.Tests.FilterHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for WorkOrderFilterHandler.
|
||||
/// </summary>
|
||||
public sealed class WorkOrderFilterHandlerTests
|
||||
{
|
||||
private readonly SqlServerCompiler _compiler = new();
|
||||
private readonly WorkOrderFilterHandler _handler = new();
|
||||
|
||||
[Fact]
|
||||
public void IsEnabled_WithWorkOrderFilters_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
WorkOrderFilter =
|
||||
[
|
||||
new WorkOrderFilterEntry { WorkOrderNumber = 12345, ItemNumber = "ABC123" }
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _handler.IsEnabled(model);
|
||||
|
||||
// Assert
|
||||
result.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsEnabled_WithEmptyWorkOrderFilters_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
WorkOrderFilter = []
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _handler.IsEnabled(model);
|
||||
|
||||
// Assert
|
||||
result.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsEnabled_WithNullWorkOrderFilters_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel();
|
||||
|
||||
// Act
|
||||
var result = _handler.IsEnabled(model);
|
||||
|
||||
// Assert
|
||||
result.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_GeneratedSql_ContainsMerge()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
WorkOrderFilter =
|
||||
[
|
||||
new WorkOrderFilterEntry { WorkOrderNumber = 12345, ItemNumber = "ABC123" }
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _handler.Apply(model, _compiler);
|
||||
|
||||
// Assert
|
||||
result.ShouldNotBeNull();
|
||||
result.SetupSql.ShouldNotBeEmpty();
|
||||
|
||||
var allSql = string.Join("\n", result.SetupSql);
|
||||
allSql.ShouldContain("MERGE #Temp_WO AS TARGET");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_GeneratedSql_ContainsManuallySpecified()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
WorkOrderFilter =
|
||||
[
|
||||
new WorkOrderFilterEntry { WorkOrderNumber = 12345, ItemNumber = "ABC123" }
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _handler.Apply(model, _compiler);
|
||||
|
||||
// Assert
|
||||
var allSql = string.Join("\n", result.SetupSql);
|
||||
allSql.ShouldContain("ManuallySpecified = 1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_GeneratedSql_ContainsSplitOrderLogic()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
WorkOrderFilter =
|
||||
[
|
||||
new WorkOrderFilterEntry { WorkOrderNumber = 12345, ItemNumber = "ABC123" }
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _handler.Apply(model, _compiler);
|
||||
|
||||
// Assert
|
||||
var allSql = string.Join("\n", result.SetupSql);
|
||||
allSql.ShouldContain("SplitOrder");
|
||||
allSql.ShouldContain("ParentWorkOrderNumber");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_Parameters_ContainsWorkOrderFilterParameter()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
WorkOrderFilter =
|
||||
[
|
||||
new WorkOrderFilterEntry { WorkOrderNumber = 12345, ItemNumber = "ABC123" }
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _handler.Apply(model, _compiler);
|
||||
|
||||
// Assert
|
||||
result.Parameters.ShouldContainKey("p_WorkOrderFilter");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Priority_ReturnsExpectedValue()
|
||||
{
|
||||
// Assert
|
||||
_handler.Priority.ShouldBe(10);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user