diff --git a/NEW/src/JdeScoping.DataAccess/DependencyInjection.cs b/NEW/src/JdeScoping.DataAccess/DependencyInjection.cs index 9218a87..eb0f0c8 100644 --- a/NEW/src/JdeScoping.DataAccess/DependencyInjection.cs +++ b/NEW/src/JdeScoping.DataAccess/DependencyInjection.cs @@ -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(); - // Register filter handlers (scoped - one per request) - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - // Register query builder (scoped) + // Note: Filter criteria are extracted from database JSON using SQL functions, + // eliminating the need for filter handler classes. services.AddScoped(); // Register search processing services (scoped) diff --git a/NEW/src/JdeScoping.DataAccess/Extensions/TableValuedParameterExtensions.cs b/NEW/src/JdeScoping.DataAccess/Extensions/TableValuedParameterExtensions.cs deleted file mode 100644 index 35e11ab..0000000 --- a/NEW/src/JdeScoping.DataAccess/Extensions/TableValuedParameterExtensions.cs +++ /dev/null @@ -1,156 +0,0 @@ -using System.Data; -using Dapper; -using JdeScoping.DataAccess.Models; - -namespace JdeScoping.DataAccess.Extensions; - -/// -/// Extension methods for SearchModel including table-valued parameters and query helpers. -/// -public static class TableValuedParameterExtensions -{ - /// - /// 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. - /// - /// Search model to evaluate. - /// True if work order step data should be searched. - public static bool ShouldSearchSteps(this SearchModel model) - { - return model.MinimumDt.HasValue - || model.MaximumDt.HasValue - || model.ProfitCenterFilterEnabled - || model.WorkCenterFilterEnabled - || model.OperatorFilterEnabled; - } - - /// - /// Creates a table-valued parameter for work order filtering. - /// - /// The search model. - /// A Dapper table-valued parameter with WorkOrderNumber column. - 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"); - } - - /// - /// Creates a table-valued parameter for item number filtering. - /// - /// The search model. - /// A Dapper table-valued parameter with ItemNumber column. - 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"); - } - - /// - /// Creates a table-valued parameter for profit center filtering. - /// - /// The search model. - /// A Dapper table-valued parameter with Code column. - 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"); - } - - /// - /// Creates a table-valued parameter for work center filtering. - /// - /// The search model. - /// A Dapper table-valued parameter with Code column. - 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"); - } - - /// - /// Creates a table-valued parameter for operator filtering. - /// - /// The search model. - /// A Dapper table-valued parameter with UserName column. - 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"); - } - - /// - /// Creates a table-valued parameter for component lot filtering. - /// - /// The search model. - /// A Dapper table-valued parameter with ComponentLotNumber and ItemNumber columns. - 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"); - } - - /// - /// Creates a table-valued parameter for item/operation/MIS filtering. - /// - /// The search model. - /// A Dapper table-valued parameter with ItemNumber, OperationNumber, MisNumber, and MisRevision columns. - 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"); - } -} diff --git a/NEW/src/JdeScoping.DataAccess/FilterHandlers/ComponentLotFilterHandler.cs b/NEW/src/JdeScoping.DataAccess/FilterHandlers/ComponentLotFilterHandler.cs deleted file mode 100644 index 3a91e8c..0000000 --- a/NEW/src/JdeScoping.DataAccess/FilterHandlers/ComponentLotFilterHandler.cs +++ /dev/null @@ -1,98 +0,0 @@ -using JdeScoping.DataAccess.Extensions; -using JdeScoping.DataAccess.Models; -using SqlKata.Compilers; - -namespace JdeScoping.DataAccess.FilterHandlers; - -/// -/// Filter handler for component lot filtering. -/// Generates WorkOrderComponent/LotUsage joins and sets CARDEX flag on #Temp_WO. -/// -public sealed class ComponentLotFilterHandler : FilterHandlerBase -{ - /// - public override int Priority => 30; - - /// - public override bool IsEnabled(SearchModel model) => model.ComponentLotFilterEnabled; - - /// - public override FilterResult Apply(SearchModel model, SqlServerCompiler compiler) - { - var setupSql = new List(); - var parameters = new Dictionary - { - ["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); - } -} diff --git a/NEW/src/JdeScoping.DataAccess/FilterHandlers/FilterHandlerBase.cs b/NEW/src/JdeScoping.DataAccess/FilterHandlers/FilterHandlerBase.cs deleted file mode 100644 index 872592b..0000000 --- a/NEW/src/JdeScoping.DataAccess/FilterHandlers/FilterHandlerBase.cs +++ /dev/null @@ -1,42 +0,0 @@ -using JdeScoping.DataAccess.Interfaces; -using JdeScoping.DataAccess.Models; -using SqlKata.Compilers; - -namespace JdeScoping.DataAccess.FilterHandlers; - -/// -/// Base class for filter handlers providing common functionality. -/// -public abstract class FilterHandlerBase : IFilterHandler -{ - /// - public abstract int Priority { get; } - - /// - public abstract bool IsEnabled(SearchModel model); - - /// - public abstract FilterResult Apply(SearchModel model, SqlServerCompiler compiler); - - /// - /// Creates an empty filter result with no SQL or parameters. - /// - protected static FilterResult EmptyResult() - => new FilterResult([], new Dictionary()); - - /// - /// Creates a filter result with setup SQL and optional parameters. - /// - protected static FilterResult WithSetupSql( - IReadOnlyList setupSql, - IDictionary? parameters = null) - => new FilterResult(setupSql, parameters ?? new Dictionary()); - - /// - /// Creates a filter result with a single setup SQL statement. - /// - protected static FilterResult WithSetupSql( - string sql, - IDictionary? parameters = null) - => new FilterResult([sql], parameters ?? new Dictionary()); -} diff --git a/NEW/src/JdeScoping.DataAccess/FilterHandlers/ItemNumberFilterHandler.cs b/NEW/src/JdeScoping.DataAccess/FilterHandlers/ItemNumberFilterHandler.cs deleted file mode 100644 index 39539f1..0000000 --- a/NEW/src/JdeScoping.DataAccess/FilterHandlers/ItemNumberFilterHandler.cs +++ /dev/null @@ -1,46 +0,0 @@ -using JdeScoping.DataAccess.Extensions; -using JdeScoping.DataAccess.Models; -using SqlKata.Compilers; - -namespace JdeScoping.DataAccess.FilterHandlers; - -/// -/// Filter handler for item number filtering. -/// Generates #P_ItemNumbers temp table. -/// -public sealed class ItemNumberFilterHandler : FilterHandlerBase -{ - /// - public override int Priority => 20; - - /// - public override bool IsEnabled(SearchModel model) => model.ItemNumberFilterEnabled; - - /// - public override FilterResult Apply(SearchModel model, SqlServerCompiler compiler) - { - var parameters = new Dictionary - { - ["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); - } -} diff --git a/NEW/src/JdeScoping.DataAccess/FilterHandlers/ItemOperationMisFilterHandler.cs b/NEW/src/JdeScoping.DataAccess/FilterHandlers/ItemOperationMisFilterHandler.cs deleted file mode 100644 index 52cf1f6..0000000 --- a/NEW/src/JdeScoping.DataAccess/FilterHandlers/ItemOperationMisFilterHandler.cs +++ /dev/null @@ -1,55 +0,0 @@ -using JdeScoping.DataAccess.Extensions; -using JdeScoping.DataAccess.Models; -using SqlKata.Compilers; - -namespace JdeScoping.DataAccess.FilterHandlers; - -/// -/// Filter handler for item/operation/MIS filtering. -/// Generates #P_PartOperations temp table. -/// -public sealed class ItemOperationMisFilterHandler : FilterHandlerBase -{ - /// - public override int Priority => 70; - - /// - public override bool IsEnabled(SearchModel model) => model.ItemOperationMisFilterEnabled; - - /// - public override FilterResult Apply(SearchModel model, SqlServerCompiler compiler) - { - var parameters = new Dictionary - { - ["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); - } -} diff --git a/NEW/src/JdeScoping.DataAccess/FilterHandlers/OperatorFilterHandler.cs b/NEW/src/JdeScoping.DataAccess/FilterHandlers/OperatorFilterHandler.cs deleted file mode 100644 index b1b1675..0000000 --- a/NEW/src/JdeScoping.DataAccess/FilterHandlers/OperatorFilterHandler.cs +++ /dev/null @@ -1,55 +0,0 @@ -using JdeScoping.DataAccess.Extensions; -using JdeScoping.DataAccess.Models; -using SqlKata.Compilers; - -namespace JdeScoping.DataAccess.FilterHandlers; - -/// -/// Filter handler for operator (user) filtering. -/// Generates #P_OperatorIDs temp table with JdeUser lookup. -/// -public sealed class OperatorFilterHandler : FilterHandlerBase -{ - /// - public override int Priority => 60; - - /// - public override bool IsEnabled(SearchModel model) => model.OperatorFilterEnabled; - - /// - public override FilterResult Apply(SearchModel model, SqlServerCompiler compiler) - { - var parameters = new Dictionary - { - ["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); - } -} diff --git a/NEW/src/JdeScoping.DataAccess/FilterHandlers/ProfitCenterFilterHandler.cs b/NEW/src/JdeScoping.DataAccess/FilterHandlers/ProfitCenterFilterHandler.cs deleted file mode 100644 index eb11e19..0000000 --- a/NEW/src/JdeScoping.DataAccess/FilterHandlers/ProfitCenterFilterHandler.cs +++ /dev/null @@ -1,62 +0,0 @@ -using JdeScoping.DataAccess.Extensions; -using JdeScoping.DataAccess.Models; -using SqlKata.Compilers; - -namespace JdeScoping.DataAccess.FilterHandlers; - -/// -/// 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. -/// -public sealed class ProfitCenterFilterHandler : FilterHandlerBase -{ - /// - public override int Priority => 40; - - /// - public override bool IsEnabled(SearchModel model) => model.ProfitCenterFilterEnabled; - - /// - public override FilterResult Apply(SearchModel model, SqlServerCompiler compiler) - { - var setupSql = new List(); - var parameters = new Dictionary - { - ["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); - } -} diff --git a/NEW/src/JdeScoping.DataAccess/FilterHandlers/TimespanFilterHandler.cs b/NEW/src/JdeScoping.DataAccess/FilterHandlers/TimespanFilterHandler.cs deleted file mode 100644 index 19f0da7..0000000 --- a/NEW/src/JdeScoping.DataAccess/FilterHandlers/TimespanFilterHandler.cs +++ /dev/null @@ -1,38 +0,0 @@ -using JdeScoping.DataAccess.Models; -using SqlKata.Compilers; - -namespace JdeScoping.DataAccess.FilterHandlers; - -/// -/// 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. -/// -public sealed class TimespanFilterHandler : FilterHandlerBase -{ - /// - public override int Priority => 80; - - /// - public override bool IsEnabled(SearchModel model) => model.TimespanFilterEnabled; - - /// - public override FilterResult Apply(SearchModel model, SqlServerCompiler compiler) - { - var parameters = new Dictionary(); - - 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); - } -} diff --git a/NEW/src/JdeScoping.DataAccess/FilterHandlers/WorkCenterFilterHandler.cs b/NEW/src/JdeScoping.DataAccess/FilterHandlers/WorkCenterFilterHandler.cs deleted file mode 100644 index 3b2e1d3..0000000 --- a/NEW/src/JdeScoping.DataAccess/FilterHandlers/WorkCenterFilterHandler.cs +++ /dev/null @@ -1,64 +0,0 @@ -using JdeScoping.DataAccess.Extensions; -using JdeScoping.DataAccess.Models; -using SqlKata.Compilers; - -namespace JdeScoping.DataAccess.FilterHandlers; - -/// -/// Filter handler for work center filtering. -/// Generates MERGE into #P_WorkCenters temp table. -/// Note: This handler shares the #P_WorkCenters table with ProfitCenterFilterHandler. -/// -public sealed class WorkCenterFilterHandler : FilterHandlerBase -{ - /// - public override int Priority => 50; - - /// - public override bool IsEnabled(SearchModel model) => model.WorkCenterFilterEnabled; - - /// - public override FilterResult Apply(SearchModel model, SqlServerCompiler compiler) - { - var setupSql = new List(); - var parameters = new Dictionary - { - ["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); - } -} diff --git a/NEW/src/JdeScoping.DataAccess/FilterHandlers/WorkOrderFilterHandler.cs b/NEW/src/JdeScoping.DataAccess/FilterHandlers/WorkOrderFilterHandler.cs deleted file mode 100644 index 6ee9dfb..0000000 --- a/NEW/src/JdeScoping.DataAccess/FilterHandlers/WorkOrderFilterHandler.cs +++ /dev/null @@ -1,75 +0,0 @@ -using JdeScoping.DataAccess.Extensions; -using JdeScoping.DataAccess.Models; -using SqlKata.Compilers; - -namespace JdeScoping.DataAccess.FilterHandlers; - -/// -/// Filter handler for work order number filtering. -/// Generates MERGE into #Temp_WO with ManuallySpecified flag and split order detection. -/// -public sealed class WorkOrderFilterHandler : FilterHandlerBase -{ - /// - public override int Priority => 10; - - /// - public override bool IsEnabled(SearchModel model) => model.WorkOrderFilterEnabled; - - /// - public override FilterResult Apply(SearchModel model, SqlServerCompiler compiler) - { - var setupSql = new List(); - var parameters = new Dictionary - { - ["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); - } -} diff --git a/NEW/src/JdeScoping.DataAccess/Interfaces/IFilterHandler.cs b/NEW/src/JdeScoping.DataAccess/Interfaces/IFilterHandler.cs deleted file mode 100644 index 117addd..0000000 --- a/NEW/src/JdeScoping.DataAccess/Interfaces/IFilterHandler.cs +++ /dev/null @@ -1,30 +0,0 @@ -using JdeScoping.DataAccess.Models; -using SqlKata.Compilers; - -namespace JdeScoping.DataAccess.Interfaces; - -/// -/// Interface for filter handlers that build SQL query fragments. -/// -public interface IFilterHandler -{ - /// - /// Determines if this filter is active for the given search model. - /// - /// The search model to check. - /// True if the filter is enabled and should be applied. - bool IsEnabled(SearchModel model); - - /// - /// Applies the filter, returning setup SQL and parameters. - /// - /// The search model containing filter criteria. - /// The SqlKata compiler for SQL generation. - /// Filter result containing setup SQL and parameters. - FilterResult Apply(SearchModel model, SqlServerCompiler compiler); - - /// - /// Priority for handler execution order (lower = earlier). - /// - int Priority { get; } -} diff --git a/NEW/src/JdeScoping.DataAccess/Models/FilterEntries/ComponentLotFilterEntry.cs b/NEW/src/JdeScoping.DataAccess/Models/FilterEntries/ComponentLotFilterEntry.cs deleted file mode 100644 index 5a42545..0000000 --- a/NEW/src/JdeScoping.DataAccess/Models/FilterEntries/ComponentLotFilterEntry.cs +++ /dev/null @@ -1,22 +0,0 @@ -using JdeScoping.DataAccess.Attributes; - -namespace JdeScoping.DataAccess.Models.FilterEntries; - -/// -/// Component lot search filter entry. -/// -[OutputTable(TabName = "Component Lot Filter", ShowHeader = true, TableName = "Component_Lot_Filter")] -public sealed record ComponentLotFilterEntry -{ - /// - /// Component lot number. - /// - [OutputColumn(Order = 10, HeaderText = "Lot Number")] - public string LotNumber { get; init; } = string.Empty; - - /// - /// Component lot item number. - /// - [OutputColumn(Order = 20, HeaderText = "Item Number")] - public string ItemNumber { get; init; } = string.Empty; -} diff --git a/NEW/src/JdeScoping.DataAccess/Models/FilterEntries/ItemNumberFilterEntry.cs b/NEW/src/JdeScoping.DataAccess/Models/FilterEntries/ItemNumberFilterEntry.cs deleted file mode 100644 index a0ddd02..0000000 --- a/NEW/src/JdeScoping.DataAccess/Models/FilterEntries/ItemNumberFilterEntry.cs +++ /dev/null @@ -1,22 +0,0 @@ -using JdeScoping.DataAccess.Attributes; - -namespace JdeScoping.DataAccess.Models.FilterEntries; - -/// -/// Item number search filter entry. -/// -[OutputTable(TabName = "Item Number Filter", ShowHeader = true, TableName = "Item_Number_Filter")] -public sealed record ItemNumberFilterEntry -{ - /// - /// Item number. - /// - [OutputColumn(Order = 10, HeaderText = "Item Number")] - public string ItemNumber { get; init; } = string.Empty; - - /// - /// Item description. - /// - [OutputColumn(Order = 20, HeaderText = "Item Description")] - public string ItemDescription { get; init; } = string.Empty; -} diff --git a/NEW/src/JdeScoping.DataAccess/Models/FilterEntries/ItemOperationMisFilterEntry.cs b/NEW/src/JdeScoping.DataAccess/Models/FilterEntries/ItemOperationMisFilterEntry.cs deleted file mode 100644 index d4177af..0000000 --- a/NEW/src/JdeScoping.DataAccess/Models/FilterEntries/ItemOperationMisFilterEntry.cs +++ /dev/null @@ -1,34 +0,0 @@ -using JdeScoping.DataAccess.Attributes; - -namespace JdeScoping.DataAccess.Models.FilterEntries; - -/// -/// Item/operation/MIS search filter entry. -/// -[OutputTable(TabName = "Item/Operation/MIS Filter", ShowHeader = true, TableName = "Item_Operation_MIS_Filter")] -public sealed record ItemOperationMisFilterEntry -{ - /// - /// Part's item number. - /// - [OutputColumn(Order = 10, HeaderText = "Item Number")] - public string ItemNumber { get; init; } = string.Empty; - - /// - /// Operation's job step number. - /// - [OutputColumn(Order = 20, HeaderText = "Operation Number")] - public string OperationNumber { get; init; } = string.Empty; - - /// - /// MIS number. - /// - [OutputColumn(Order = 30, HeaderText = "MIS Number")] - public string MisNumber { get; init; } = string.Empty; - - /// - /// MIS revision. - /// - [OutputColumn(Order = 40, HeaderText = "MIS Revision")] - public string MisRevision { get; init; } = string.Empty; -} diff --git a/NEW/src/JdeScoping.DataAccess/Models/FilterEntries/OperatorFilterEntry.cs b/NEW/src/JdeScoping.DataAccess/Models/FilterEntries/OperatorFilterEntry.cs deleted file mode 100644 index 34fca5f..0000000 --- a/NEW/src/JdeScoping.DataAccess/Models/FilterEntries/OperatorFilterEntry.cs +++ /dev/null @@ -1,27 +0,0 @@ -using JdeScoping.DataAccess.Attributes; - -namespace JdeScoping.DataAccess.Models.FilterEntries; - -/// -/// Operator search filter entry. -/// -[OutputTable(TabName = "Operator Filter", ShowHeader = true, TableName = "Operator_Filter")] -public sealed record OperatorFilterEntry -{ - /// - /// Operator unique JDE address number. - /// - public long AddressNumber { get; init; } - - /// - /// Operator login user ID. - /// - [OutputColumn(Order = 10, HeaderText = "Username")] - public string UserId { get; init; } = string.Empty; - - /// - /// Operator full name (FIRST + LAST). - /// - [OutputColumn(Order = 20, HeaderText = "Name")] - public string FullName { get; init; } = string.Empty; -} diff --git a/NEW/src/JdeScoping.DataAccess/Models/FilterEntries/ProfitCenterFilterEntry.cs b/NEW/src/JdeScoping.DataAccess/Models/FilterEntries/ProfitCenterFilterEntry.cs deleted file mode 100644 index 40acf00..0000000 --- a/NEW/src/JdeScoping.DataAccess/Models/FilterEntries/ProfitCenterFilterEntry.cs +++ /dev/null @@ -1,22 +0,0 @@ -using JdeScoping.DataAccess.Attributes; - -namespace JdeScoping.DataAccess.Models.FilterEntries; - -/// -/// Profit center search filter entry. -/// -[OutputTable(TabName = "Profit Center Filter", ShowHeader = true, TableName = "Profit_Center_Filter")] -public sealed record ProfitCenterFilterEntry -{ - /// - /// Profit center code. - /// - [OutputColumn(Order = 10, HeaderText = "Profit Center")] - public string Code { get; init; } = string.Empty; - - /// - /// Profit center description. - /// - [OutputColumn(Order = 20, HeaderText = "Description")] - public string Description { get; init; } = string.Empty; -} diff --git a/NEW/src/JdeScoping.DataAccess/Models/FilterEntries/WorkCenterFilterEntry.cs b/NEW/src/JdeScoping.DataAccess/Models/FilterEntries/WorkCenterFilterEntry.cs deleted file mode 100644 index 6035852..0000000 --- a/NEW/src/JdeScoping.DataAccess/Models/FilterEntries/WorkCenterFilterEntry.cs +++ /dev/null @@ -1,22 +0,0 @@ -using JdeScoping.DataAccess.Attributes; - -namespace JdeScoping.DataAccess.Models.FilterEntries; - -/// -/// Work center search filter entry. -/// -[OutputTable(TabName = "Work Center Filter", ShowHeader = true, TableName = "Work_Center_Filter")] -public sealed record WorkCenterFilterEntry -{ - /// - /// Work center code. - /// - [OutputColumn(Order = 10, HeaderText = "Work Center")] - public string Code { get; init; } = string.Empty; - - /// - /// Work center description. - /// - [OutputColumn(Order = 20, HeaderText = "Description")] - public string Description { get; init; } = string.Empty; -} diff --git a/NEW/src/JdeScoping.DataAccess/Models/FilterEntries/WorkOrderFilterEntry.cs b/NEW/src/JdeScoping.DataAccess/Models/FilterEntries/WorkOrderFilterEntry.cs deleted file mode 100644 index 94737a3..0000000 --- a/NEW/src/JdeScoping.DataAccess/Models/FilterEntries/WorkOrderFilterEntry.cs +++ /dev/null @@ -1,22 +0,0 @@ -using JdeScoping.DataAccess.Attributes; - -namespace JdeScoping.DataAccess.Models.FilterEntries; - -/// -/// Work order search filter entry. -/// -[OutputTable(TabName = "Work Order Filter", ShowHeader = true, TableName = "Work_Order_Filter")] -public sealed record WorkOrderFilterEntry -{ - /// - /// Work order number. - /// - [OutputColumn(Order = 10, HeaderText = "Work Order Number")] - public long WorkOrderNumber { get; init; } - - /// - /// Work order item number. - /// - [OutputColumn(Order = 20, HeaderText = "Item Number")] - public string ItemNumber { get; init; } = string.Empty; -} diff --git a/NEW/src/JdeScoping.DataAccess/Models/FilterResult.cs b/NEW/src/JdeScoping.DataAccess/Models/FilterResult.cs deleted file mode 100644 index 1671de2..0000000 --- a/NEW/src/JdeScoping.DataAccess/Models/FilterResult.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace JdeScoping.DataAccess.Models; - -/// -/// Result of applying a filter handler. -/// -/// List of SQL statements for filter setup (temp tables, etc.). -/// Dictionary of parameter names and values. -public sealed record FilterResult( - IReadOnlyList SetupSql, - IDictionary Parameters); diff --git a/NEW/src/JdeScoping.DataAccess/Models/SearchModel.cs b/NEW/src/JdeScoping.DataAccess/Models/SearchModel.cs index 32b03e3..dd5892d 100644 --- a/NEW/src/JdeScoping.DataAccess/Models/SearchModel.cs +++ b/NEW/src/JdeScoping.DataAccess/Models/SearchModel.cs @@ -1,10 +1,11 @@ -using JdeScoping.DataAccess.Models.FilterEntries; using JdeScoping.DataAccess.Models.Results; namespace JdeScoping.DataAccess.Models; /// /// 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. /// public class SearchModel { @@ -38,96 +39,6 @@ public class SearchModel /// public DateTime? EndDt { get; set; } - /// - /// Minimum timestamp to include. - /// - public DateTime? MinimumDt { get; set; } - - /// - /// Maximum timestamp to include. - /// - public DateTime? MaximumDt { get; set; } - - /// - /// Whether or not timespan filter is enabled. - /// - public bool TimespanFilterEnabled => MinimumDt.HasValue || MaximumDt.HasValue; - - /// - /// Collection of work order numbers to include. - /// - public List WorkOrderFilter { get; set; } = []; - - /// - /// Whether or not work order filter is enabled. - /// - public bool WorkOrderFilterEnabled => WorkOrderFilter is { Count: > 0 }; - - /// - /// Collection of item numbers to include. - /// - public List ItemNumberFilter { get; set; } = []; - - /// - /// Whether or not item number filter is enabled. - /// - public bool ItemNumberFilterEnabled => ItemNumberFilter is { Count: > 0 }; - - /// - /// Collection of included profit centers. - /// - public List ProfitCenterFilter { get; set; } = []; - - /// - /// Whether or not profit center filter is enabled. - /// - public bool ProfitCenterFilterEnabled => ProfitCenterFilter is { Count: > 0 }; - - /// - /// Collection of included work centers. - /// - public List WorkCenterFilter { get; set; } = []; - - /// - /// Whether or not work center filter is enabled. - /// - public bool WorkCenterFilterEnabled => WorkCenterFilter is { Count: > 0 }; - - /// - /// Collection of included operator IDs. - /// - public List OperatorFilter { get; set; } = []; - - /// - /// Whether or not operator filter is enabled. - /// - public bool OperatorFilterEnabled => OperatorFilter is { Count: > 0 }; - - /// - /// Collection of included upper level lot numbers. - /// - public List ComponentLotFilter { get; set; } = []; - - /// - /// Whether or not component lot filter is enabled. - /// - public bool ComponentLotFilterEnabled => ComponentLotFilter is { Count: > 0 }; - - /// - /// List of part/operation combinations for MIS filtering. - /// - public List ItemOperationMisFilter { get; set; } = []; - - /// - /// Whether or not item/operation/mis filter is enabled. - /// - public bool ItemOperationMisFilterEnabled => ItemOperationMisFilter is { Count: > 0 }; - - /// - /// Whether or not to extract MIS data. - /// - public bool ExtractMisData { get; set; } - /// /// Work order search results. /// diff --git a/NEW/src/JdeScoping.DataAccess/QueryBuilders/MisQueryBuilder.cs b/NEW/src/JdeScoping.DataAccess/QueryBuilders/MisQueryBuilder.cs index 2823425..147c8e5 100644 --- a/NEW/src/JdeScoping.DataAccess/QueryBuilders/MisQueryBuilder.cs +++ b/NEW/src/JdeScoping.DataAccess/QueryBuilders/MisQueryBuilder.cs @@ -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(); - - 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 ""; - } } diff --git a/NEW/src/JdeScoping.DataAccess/Services/SearchProcessor.cs b/NEW/src/JdeScoping.DataAccess/Services/SearchProcessor.cs index 740ec09..880c267 100644 --- a/NEW/src/JdeScoping.DataAccess/Services/SearchProcessor.cs +++ b/NEW/src/JdeScoping.DataAccess/Services/SearchProcessor.cs @@ -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( + "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(); - - 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); } diff --git a/NEW/tests/JdeScoping.DataAccess.Tests/Extensions/TableValuedParameterExtensionsTests.cs b/NEW/tests/JdeScoping.DataAccess.Tests/Extensions/TableValuedParameterExtensionsTests.cs deleted file mode 100644 index eb8452a..0000000 --- a/NEW/tests/JdeScoping.DataAccess.Tests/Extensions/TableValuedParameterExtensionsTests.cs +++ /dev/null @@ -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; - -/// -/// Unit tests for TableValuedParameterExtensions. -/// -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 - - /// - /// Extracts the underlying DataTable from a Dapper table-valued parameter. - /// Uses reflection to access internal fields across different Dapper versions. - /// - 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 -} diff --git a/NEW/tests/JdeScoping.DataAccess.Tests/FilterHandlers/ComponentLotFilterHandlerTests.cs b/NEW/tests/JdeScoping.DataAccess.Tests/FilterHandlers/ComponentLotFilterHandlerTests.cs deleted file mode 100644 index d69149d..0000000 --- a/NEW/tests/JdeScoping.DataAccess.Tests/FilterHandlers/ComponentLotFilterHandlerTests.cs +++ /dev/null @@ -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; - -/// -/// Unit tests for ComponentLotFilterHandler. -/// -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); - } -} diff --git a/NEW/tests/JdeScoping.DataAccess.Tests/FilterHandlers/WorkOrderFilterHandlerTests.cs b/NEW/tests/JdeScoping.DataAccess.Tests/FilterHandlers/WorkOrderFilterHandlerTests.cs deleted file mode 100644 index 5f29f3a..0000000 --- a/NEW/tests/JdeScoping.DataAccess.Tests/FilterHandlers/WorkOrderFilterHandlerTests.cs +++ /dev/null @@ -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; - -/// -/// Unit tests for WorkOrderFilterHandler. -/// -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); - } -}